主题
nestjs实践
1. nest是什么?
Nest (NestJS) 是一个用于构建高效、可扩展的 Node.js 服务器端应用程序的开发框架。它利用 JavaScript 的渐进增强的能力,使用并完全支持 TypeScript (仍然允许开发者使用纯 JavaScript 进行开发),并结合了 OOP (面向对象编程)、FP (函数式编程)和 FRP (函数响应式编程)。
在底层,Nest 构建在强大的 HTTP 服务器框架上,例如 Express (默认),并且还可以通过配置从而使用 Fastify !
Nest 在这些常见的 Node.js 框架 (Express/Fastify) 之上提高了一个抽象级别,但仍然向开发者直接暴露了底层框架的 API。这使得开发者可以自由地使用适用于底层平台的无数的第三方模块。
处理一个请求,Express与Nest对比示意图:
2. nest基本使用
2.1 创建一个nestjs项目
请确保在您的操作系统上安装了 Node.js (>= 10.13.0,v13 除外)。
bash
## 1. 全局安装nest脚手架
npm i -g @nestjs/cli
## 2. 利用脚手架创建项目
nest new <project-name>
接着生成一个项目, src目录结构如下:
js
src
app.controller.ts // 单个路由的基本控制器。
app.controller.spec.ts // 针对控制器的单元测试。
app.module.ts // 应用根模块
app.service.ts // 具有单一方法的基本服务
main.ts // 项目入口文件,它使用核心函数 NestFactory 来创建 Nest 应用程序的实例。
2.2 nest里面的一些概念
nest里面的装饰器都可以从
@nestjs.common
这个包里面解构出来
1)控制器 controller
被@Controller()
装饰器装饰的类可以看做是一个控制器,控制器负责处理传入的请求并将响应返回给客户端。
js
import { Controller } from '@nestjs/common';
@Controller()
export class AppController {}
2) 服务 service
或者providers
provider
是nest里面的提供者
,最典型的就是service
,provider
是nest里面数据的来源
被@Injectable()
装饰器装饰的类就是一个provider
js
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {}
3)模块 module
module可以看做是
组件
,类似于vue,react等里面的组件,它里面包含controller与provider,默认情况下,一个module里面的provider不能被其他module使用,若要使用,需要做一些导出导入的操作。
被@Module()
装饰器装饰的类可以看做一个模块,模块内整合了controller
与providers
,还可以整合其他模块,通过imports
js
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [], // 整合其他module
controllers: [AppController], // 控制器
providers: [AppService], // service
exports:[] // 里面的的子集providers由该模块提供,并且应该在导入该模块的其他模块中可用
})
export class AppModule {}
官方模块示意图: 不管有多少个模块,整个应用需要有一个根模块,用来整合其他模块,其他每一个模块下面也可有自己的子模块
(1)module之间的关系
2.3 nest快速创建命令
nest提供我们一些命令来快速创建控制器,服务,模块,等等代码块,nest g xxx xxx
若不知道具体命令有哪些, 可以通过命令nest --help
查看:
sh
nest --help
例如: 创建一个use控制器:
bash
nest g co user
## 或者
nest g controller user
这个命令执行完成后, 不仅会在src下创建一个user文件夹(若没有此文件夹会创建),里面创建了一个user.controller.ts
控制器文件以及一个测试文件,并且会在app.module.ts
的controllers
里面自动注入刚创建好的UserController
注意: 默认会在src目录下面创建文件或文件夹, 若想选择创建的位置, 可以带上路径例如:
js
## 此时我们所在的位置是项目根目录
nest g co /app/user ## app目录可以不存在,会自动创建
## 或者
nest g co app/user
## 也可以切换到需要建的目录运行nest g 命令
cd src/app/
nest g co user
其他的一些命令做的事基本类似,大同小异,使用起来非常方便
2.4 路由
1)设置路由
nestjs与egg或者其他的框架不一样,像egg里面,在app文件夹下面会有一个专门的router.js
文件专门用来定整个应用的所有路由与controller之间的关系,在nest里面却不是,nest里面路由是直接定义在controller里面的。例如下面的代码:
js
import { Controller, Get, Post } from '@nestjs/common';
@Controller('nest')
export class AppController {
@Get() // 路由是 /nest
getList(): string {
return '这是第一个路由';
}
@Post('index') // 路由是 /nest/index
add(): string {
return '新增接口';
}
}
若在@controller('nest')
里面写了东西,则代表当前这个controller里面的路由都是nest/xxx, 并且,路由处理函数名称和路由无关,只要在同一个controller里面名称不重复就可以
注意:装饰器后面不能带分号。
通配符路由
nest也支持基于模式的路由。例如,星号用作通配符,将匹配任何字符组合。
js
@Get('ab*cd')
findAll() {
return 'This route uses a wildcard';
}
'ab*cd'
路由路径将匹配、abcd
、ab_cd
等abecd
。字符?
, +
, *
, 和()
可以在路由路径中使用,并且是它们的正则表达式对应物的子集。连字符 ( -)
和点 ( .)
由基于字符串的路径逐字解释。
2)统一路由前缀
在main.js
里面设置统一的路由前缀
js
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix('v1'); // 统一设置路由前缀,这里为v1
await app.listen(3000);
}
bootstrap();
核心代码:app.setGlobalPrefix('v1')
,设置完成后,请求这个nestjs服务的所有路由,都必须为/v1/xxxx
3)请求方式
nest里面提供了一堆装饰器,其中http请求方法(动词)就是由@nestjs/common
这个模块提供的,
js
import {Get, Post, Put, Delete, Options,... } from '@nestjs/common';
Nest 为所有标准 HTTP 方法提供装饰器:@Get()
、@Post()
、@Put()
、@Delete()
、@Patch()
、@Options()
和@Head()
. 此外,@All()
定义一个处理所有这些的端点。
4)获取请求参数
提示: 为了利用
express
类型(如request: Request
的参数示例),请安装@types/express
包。import { Request } from 'express';
http请求有不同的请求方式,get, post, delete, put 等等。服务端如果想取到不同请求的请求参数,大多数情况下,不需要我们手动获取,nest提供了一些装饰器用于很方便的获取不同的参数,如下:
下面是nest提供的装饰器列表以及它们所代表的普通平台特定对象
使用示例:
- 用
@Query()
获取query参数,@Query()
是一个参数装饰器
js
import { Controller, Get, Query } from '@nestjs/common';
@Controller('nest')
export class AppController {
@Get('getList')
getList(@Query() query): string {
console.log(query);
return '这是第一个路由';
}
}
效果
其他的参数装饰器使用方式就是照猫画虎
5)设置响应
(1)设置状态码
默认情况下响应状态代码始终为200
,但 POST
请求除外201
。@HttpCode(...)
我们可以通过在处理程序级别添加装饰器来轻松更改此 行为。
js
import { HttpCode } from '@nestjs/common';
@Post()
@HttpCode(204)
create() {
return 'This action adds a new cat';
}
若需要根据情况设置http状态码,可以通过注入@Res()
装饰器,然后通过res.status(code)
来设置状态码,但是需要注意,若是注入了@Res() res
响应对象,controller里面需要通过res.send
或者res.json
等等底层平台(默认是Express)提供的响应方法来响应,直接return
是不行的。
js
import { Controller, Get, Res, Req } from '@nestjs/common';
import { Request, Response } from 'express';
@Controller('nest')
export class AppController {
@Get('getList')
getList(@Req() req: Request, @Res() res: Response) {
res.status(300);
res.send('OK');
// return 123; (此时,这样是错误的,无法响应客户端)
}
}
(2)设置响应头
要设置自定义响应头,可以使用@Header()
装饰器或特定于库的响应对象(通过res.header()
直接调用
js
import { Header } from '@nestjs/common';
@Post()
@Header('Cache-Control', 'none')
create() {
return 'This action adds a new cat';
}
// 或者
import { Controller, Get, Res, Req } from '@nestjs/common';
import { Request, Response } from 'express';
@Controller('nest')
export class AppController {
@Get('getList')
getList(@Req() req: Request, @Res() res: Response) {
res.setHeader('Cache-Control', 'none');
res.send('OK');
// return 123; (此时,这样是错误的,无法响应客户端)
}
}
2.5 controller
与provider
1)一对一
controller
里面要使用service
, 需要在controller
里面引入service
,并在constructor
注入,就可以通过this调用即可。
js
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service'; // 1. 引入
@Controller()
export class AppController {
// 2. 注入service
constructor(private readonly appService: AppService) {}
@Get()
getList() {
// 3. 使用
return this.appService.findAll();
}
}
2)一个controller
内使用多个provider
若需要在一个controller
里面使用多个service
,只需要先将模块导出exports,然后我们controller对应的modules里面通过imports都引入,在controller的constroctor里面并都注入即可。
js
import { Controller, Get } from '@nestjs/common';
import { AService } from './A.service'; // 1. 引入 AService
import { BService } from './B.service'; // 1. 引入 BService
@Controller()
export class AppController {
// 2. 一次注入多个service
constructor(
private readonly aService: AService,
private readonly bService: BService
) {}
@Get()
getList() {
// 3. 使用
let a = this.aService.findAll();
let b = this.aService.findAll();
return [a, b]
}
}
3)exports
、imports
、@Global()
(1)exports
与imports
一般情况下,一个模块内会包含多个controller
,多个service
,在模块内部,可以像上一步一样在controller正常引用注入service即可使用,但是业务不一定全都这样简单常规,很多时候,一个module往往需要用到其他module里面的一些provider
,这个时候,仅仅通过上一步的方式还不够,可以认为一个module里面的东西,默认只能在模块内部使用,若要将模块内部的东西提供给外部使用,模块内部需要exports
,m外面的模块需要imports
若一个模块里面的controller
需要引入另外模块内的provider
,需要在被引入的模块的module.ts
里面,将这个模块下的service导出,自己模块内imports
,这是框架的规定,否则无法正常启动。
js
// 提供provider的module
// user/user.module.ts
import { Module } from '@nestjs/common';
import { UserController } from './user.controller';
import { UserService } from './user.service';
@Module({
controllers: [UserController],
providers: [UserService],
exports: [UserService], // 只有导出, 才能在模块外部使用
})
export class UserModule {}
js
// 使用provider的module
// app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
import { UserService } from './user/user.service'; // 1. 引入userService
@Controller()
export class AppController {
// 注入service
constructor(
private readonly appService: AppService,
private readonly userService: UserService, // 2. 注入userService
) {}
@Get()
getList() {
const user = this.userService.findOne(); // 3. 使用userService
const app = this.appService.findAll();
return {
user,
app,
};
}
}
(2)@Global()
上面的exports
与imports
虽然可以在一个module内使用其他模块的provider
,但是,也有自己的缺点,就是麻烦 假如某个模块内的provider
需要在100个模块内都使用,按照上面的方法,那么我们在这个模块内通过exports
导出这个provider
,并且在其他100个模块内都需要用imports:[]
将这个模块内的provider
引入才行,这显然是一个体力活,针对这种情况,框架提供了一个装饰器@Global()
,在要被共用的模块(module.ts)里面使用这个装饰器,其他任何模块都可以使用这个模块的provider
,并且不用引入。
这种方式针对与全局通用的模块来说非常友好,例如,帮助程序、数据库连接等。
ts
import { Global, Module } from '@nestjs/common';
import { UserController } from './user.controller';
import { UserService } from './user.service';
@Global() // 将provider编程全局可用的,还需要看exports里面的内容
@Module({
controllers: [UserController],
providers: [UserService],
exports: [UserService], // 若不写,默认全部provider都变成全局的,写谁,谁编程全局可用
})
export class UserModule {}
3)日志输出
(1)常规日志输出
默认情况下,我们会使用console.log
在控制台打印一些信息,这种方式很普通
nest框架允许我们定义输出信息的格式,例如:
(2)输出带自定义前缀的日志信息
若需要知道是哪个文件输出的日志,可以在在new Logger
时候传入自定义名称,例如:
js
import { Controller, Get, Logger } from '@nestjs/common';
@Controller('user')
export class UserController {
private readonly logger;
constructor() {
this.logger = new Logger('user controller');
}
@Get()
index(): string {
console.log('我是user模块的控制器');
this.logger.log('我是自定义日志');
return 'user index';
}
}
(3)不同类型的日志打印
ts
import { Controller, Get, Logger } from '@nestjs/common';
@Controller('user')
export class UserController {
private readonly logger: Logger;
constructor() {
this.logger = new Logger('user controller');
}
@Get()
index(): string {
console.log('我是user模块的控制器');
this.logger.debug('我是自定义debug日志');
this.logger.log('我是自定义log日志');
this.logger.warn('我是自定义warn日志');
this.logger.error('我是自定义error日志');
this.logger.verbose('我是自定义verbose日志');
return 'user index';
}
}
3. nest面向切面编程
3.1 请求流程【关卡】
从上图可以看到,当接收到一个请求的时候,nest从前到后都会经过那些关卡
- 中间件:https://nestjs.bootcss.com/middlewares
- 守卫:https://nestjs.bootcss.com/guards
- 拦截器:https://nestjs.bootcss.com/interceptors
- 管道:https://nestjs.bootcss.com/pipes
- 控制器:https://nestjs.bootcss.com/controllers
- 过滤器:https://nestjs.bootcss.com/exception-filters
他们都有着自己的作用