Nest 启动!

💰Nest 通关秘籍

官方文档 | 官方中文 | 第三方中文

为啥要用 nest

众所周知 express 是基于中间件的洋葱模型处理请求、响应的库,它并没有提供组织代码的架构特性,代码可以写的很随意。而为了更好的可维护性,我们都会用 Nest 这种一站式企业级开发框架。就像 java 里会用 Spring 框架一样。Nest 底层是 express 但也不完全是,它内部实现是基于 interface 的,而且提供了 @nestjs/platform-express、@nestjs/platform-fastify 这两个 adapter 包。这样就可以轻松的切换 express、fastify 或者其他的 http 请求处理的库。

启动

1
2
npm install -g @nestjs/cli
nest new 项目名

[Nest Cli](https://juejin.cn/book/7226988578700525605/section/7227320664863014968) 有创建新项目的 <font style="color:rgb(37, 41, 51);">nest new</font>,有创建某些代码的 <font style="color:rgb(37, 41, 51);">nest generate</font>,还有编译构建的 <font style="color:rgb(37, 41, 51);">nest build</font>,开发模式的 <font style="color:rgb(37, 41, 51);">nest start</font> 等。非常全面

5 种 http/https 的数据传输方式:

其中前两种是 url 中的:

  • url param: url 中的参数 (<font style="color:rgb(37, 41, 51);">url/:param</font>),Nest 中使用 @Param 来取
1
2
3
4
@Get(':id')
urlParam(@Param('id') id: string) {
return `received: id=${id}`;
}
  • query:url 中 ? 后的字符串(<font style="color:rgb(37, 41, 51);">url?name=小把&age=99</font>),Nest 中使用 @Query 来取
1
2
3
4
@Get('find')
query(@Query('name') name: string, @Query('age') age: string) {
return `received: name=${name} - age=${age}`;
}

后三种是 body 中的:

  • form urlencoded: 类似 query 字符串,只不过是放在 body 中。Nest 中使用 @Body 来取,axios 中需要指定 content type 为 application/x-www-form-urlencoded,并且对数据用 qs 或者 query-string 库做 url encode
  • json: json 格式的数据。Nest 中使用 @Body 来取,axios 中不需要单独指定 content type 为 application/json,axios 内部会处理。
1
2
3
4
@Post()
formUrlencodedOrJson(@Body() createPersonDto: CreatePersonDto) {
return `received: ${JSON.stringify(createPersonDto)}`;
}
  • form data:通过 —– 作为 boundary 分隔的数据。主要用于传输文件,Nest 中要使用 FilesInterceptor 来处理其中的 binary 字段,用 @UseInterceptors 来启用,其余字段用 @Body 来取。axios 中需要指定 content type 为 multipart/form-data,并且用 FormData 对象来封装传输的内容。
1
2
3
4
5
6
7
8
9
10
@Post('/file')
@UseInterceptors(AnyFilesInterceptor({ dest: 'uploads/' }))
formData(
@Body() createPersonDto: CreatePersonDto,
@UploadedFiles() files: Array<Express.Multer.File>,
) {
console.log('[file]===>', files);
return `received: ${JSON.stringify(createPersonDto)}`;
}

IOC<font style="color:rgb(37, 41, 51);">(Inverse Of Control)控制反转</font>

后端系统中,会有很多对象:

  • <font style="color:rgb(37, 41, 51);">Controller</font> 对象:接收 http 请求,调用 Service,返回响应
  • <font style="color:rgb(37, 41, 51);">Service</font> 对象:实现业务逻辑 Reflector
  • <font style="color:rgb(37, 41, 51);">Repository</font> 对象:实现对数据库的增删改查
  • <font style="color:rgb(37, 41, 51);">DataSource</font> 对象: 数据库链接
  • <font style="color:rgb(37, 41, 51);">Config</font> 对象 配置信息

这些对象有着错综复杂的关系:Controller 依赖了 Service 实现业务逻辑,Service 依赖了 Repository 来做增删改查,Repository 依赖 DataSource 来建立连接,DataSource 又需要从 Config 对象拿到用户名密码等信息。如下:

1
2
3
4
5
const config = new Config({ username: "xxx", password: "xxx" });
const dataSource = new DataSource(config);
const repository = new Repository(dataSource);
const service = new Service(repository);
const controller = new Controller(service);

手动创建并组装对象比较麻烦,那么就有了 IOC 机制,它的出现就是为了解决后端复杂依赖。IOC 机制 是在 class 上标识哪些是可以被注入的,它的依赖是什么,然后从入口开始扫描这些对象和依赖,自动创建和组装对象。(不用手动再 new 啦)

Nest 里通过 <font style="color:rgb(37, 41, 51);">@Controller</font> 声明可以被注入的 controller,通过 <font style="color:rgb(37, 41, 51);">@Injectable</font> 声明可以被注入也可以注入别的对象的 provider,然后在 <font style="color:rgb(37, 41, 51);">@Module</font> 声明的模块里引入。

并且 Nest 还提供了 Module 和 Module 之间的<font style="color:rgb(37, 41, 51);">import</font>,可以导入别的模块<font style="color:rgb(37, 41, 51);">exports</font>provider 来注入。

Provider

一般情况下,provider 是通过 <font style="color:rgb(37, 41, 51);">@Injectable</font> 声明,然后在 <font style="color:rgb(37, 41, 51);">@Module</font>_providers_ 数组里注册的 class

常用 providers 的写法:

而下面则是默认写法。provide 可以理解为 token 可以是 string 或 class。再通过 <font style="color:rgb(37, 41, 51);">useClass</font> 指定注入的对象的类:

默认的 token 就是 class,这样不用使用 <font style="color:rgb(37, 41, 51);">@Inject</font> 来指定注入的 token。以下是两种 provider 使用方式,构造器注入和声明注入:

注入类型

  • <font style="color:rgb(37, 41, 51);">useClass</font> 指定注入的 class,
  • 还可以用 <font style="color:rgb(37, 41, 51);">useValue</font> 直接指定注入的对象。
  • 如果想动态生成对象,可以使用 <font style="color:rgb(37, 41, 51);">useFactory</font>,它的参数也注入 IOC 容器中的对象,然后动态返回 provider 的对象。还可以是异步的。
  • **inject**** 属性用于 useFactory。如果 Provider 是通过工厂函数创建的,那么 inject 属性就是一个Provide** 数组,包含了这个工厂函数的依赖项列表:说白了就是这个 <u>useFactory</u> 的参数列表。
  • 如果想起别名,可以用 <font style="color:rgb(37, 41, 51);">useExisting</font> 给已有的 token,指定一个新 token。

全局模块和生命周期

模块可以通过 **<font style="color:rgb(37, 41, 51);">@Global</font>** 声明为全局的,这样它 <font style="color:rgb(37, 41, 51);">exports</font><font style="color:rgb(37, 41, 51);">provider</font> 就可以在各处使用了,不需要 <font style="color:rgb(37, 41, 51);">imports</font>

provider、controller、module 都支持启动和销毁的生命周期函数,并且这些生命周期函数都支持 async 的方式:

  • <font style="color:rgb(37, 41, 51);">OnModuleInit</font>
  • <font style="color:rgb(37, 41, 51);">OnApplicationBootstrap</font>
  • <font style="color:rgb(37, 41, 51);">OnModuleDestroy</font>
  • <font style="color:rgb(37, 41, 51);">BeforeApplicationShutdown</font>
  • <font style="color:rgb(37, 41, 51);">OnApplicationShutdown</font>

可以在其中做一些初始化、销毁的逻辑,比如 <font style="color:rgb(37, 41, 51);">onApplicationShutwon</font> 里通过 <font style="color:rgb(37, 41, 51);">moduleRef.get</font> 取出一些 <font style="color:rgb(37, 41, 51);">provider</font>,执行关闭连接等销毁逻辑。

1
2
3
4
5
6
7
export class CccModule implements OnApplicationShutdown {
constructor(private moduleRef: ModuleRef) {}
onApplicationShutdown() {
const cccService = this.moduleRef.get<CccService>(CccService);
console.log("[--------]===>", cccService.findAll());
}
}

AOP 架构

AOP 则是把通用逻辑抽离出来,通过切面的方式添加到某个地方,可以复用和动态增删切面逻辑。具有松耦合、易于维护和扩展的特性

Nest 基于 express 这种 http 平台做了一层封装,应用了 MVC、IOC、AOP 等架构思想。

MVC 就是 Model、View Controller 的划分,请求先经过 Controller,然后调用 Model 层的 Service、Repository 完成业务逻辑,最后返回对应的 View。

IOC 是指 Nest 会自动扫描带有 <font style="color:rgb(37, 41, 51);">@Controller</font><font style="color:rgb(37, 41, 51);">@Injectable</font> 装饰器的类,创建它们的对象,并根据依赖关系自动注入它依赖的对象,免去了手动创建和组装对象的麻烦。

Nest 的**<font style="color:rgb(37, 41, 51);">Middleware(中间件)、Guard(守卫)、Interceptor(拦截器)、Pipe(管道)、ExceptionFilter(异常处理)</font>**都是 AOP 思想的实现,只不过是不同位置的切面,它们都可以灵活的作用在某个路由或者全部路由,这就是 AOP 的优势。

调用顺序: <font style="color:rgb(37, 41, 51);">Middleware</font> 是 Express 的概念,在最外层,到了某个路由之后,会先调用 <font style="color:rgb(37, 41, 51);">Guard</font><font style="color:rgb(37, 41, 51);">Guard</font> 用于判断路由有没有权限访问,然后会调用 <font style="color:rgb(37, 41, 51);">Interceptor</font>,对 <font style="color:rgb(37, 41, 51);">Contoller</font> 前后扩展一些逻辑,在到达目标 <font style="color:rgb(37, 41, 51);">Controller</font> 之前,还会调用 <font style="color:rgb(37, 41, 51);">Pipe</font> 来对参数做检验和转换。所有的 <font style="color:rgb(37, 41, 51);">HttpException</font> 的异常都会被 <font style="color:rgb(37, 41, 51);">ExceptionFilter</font> 处理,返回不同的响应。

Middleware(中间件)

全局中间件算是 Express 的那种中间件,在请求之前和之后加入一些处理逻辑,每个请求都会走到这里。

用法是 Module 实现 <font style="color:rgb(37, 41, 51);">NestModule</font>configure 方法,调用 applyforRoutes 指定什么中间件作用于什么路由。

app.use 也可以应用中间件,但更建议在 AppModule 里的 configure 方法里指定。

但是它也不全是是 Express 的 middleware,虽然都有 request、response、next 参数,但是它可以从 Nest 的 IOC 容器注入依赖,还可以指定作用于哪些路由

1
2
3
4
5
6
7
8
9
@Injectable()
export class AaaMiddleware implements NestMiddleware {
@Inject(AppService)
private readonly appService: AppService;
use(req: Request, res: Response, next: () => void) {
console.log("[--]===>", this.appService.getHello());
next();
}
}

Nest 还有个 <font style="color:rgb(37, 41, 51);">@Next</font> 装饰器,这个是用于调用下个 handler (controller function) 处理的,当用了这个装饰器之后,Nest 就不会把 handler 返回值作为响应了。

1
2
3
4
5
6
7
8
9
10
@Get('/hello')
getHello1(@Next() next): string {
next();
return `hello1`; // 不会返回
}

@Get('/hello')
getHello2(): string {
return `hello2`; // 会返回
}

Guard(守卫)

Guard 要实现 <font style="color:rgb(37, 41, 51);">CanActivate</font> 接口,实现 canActivate 方法,可以从 context 拿到请求的信息,然后做一些权限验证等处理之后返回 true 或者 false。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Injectable()
export class AaaGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const requiredRoles = this.reflector.get<Role[]>(
'roles',
context.getHandler(),
);
if (!requiredRoles) {
return true;
}
const { user } = context.switchToHttp().getRequest();
return requiredRoles.some((role) => user && user.roles?.includes(role));
}
}

Interceptor(拦截器)

Interceptor 要实现 <font style="color:rgb(37, 41, 51);">NestInterceptor</font> 接口,实现 intercept 方法,调用 next.handle() 就会调用目标 Controller,可以在之前和之后加入一些处理逻辑。

nest 的 interceptor 还可以用了 rxjs 的 operator 来处理响应,常用的 operator 如下:

  • <font style="color:rgb(37, 41, 51);">tap</font>: 不修改响应数据,执行一些额外逻辑,比如记录日志、更新缓存等
  • <font style="color:rgb(37, 41, 51);">map</font>:对响应数据做修改,一般都是改成 {code, data, message} 的格式
  • <font style="color:rgb(37, 41, 51);">catchError</font>:在 exception filter 之前处理抛出的异常,可以记录或者抛出别的异常
  • <font style="color:rgb(37, 41, 51);">timeout</font>:处理响应超时的情况,抛出一个 TimeoutError,配合 catchErrror 可以返回超时的响应
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
private readonly logger = new Logger(TimeoutInterceptor.name);
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
timeout(3000),
catchError((err) => {
if (err instanceof TimeoutError) {
this.logger.error(err.message, err.stack);
// return throwError(() => new RequestTimeoutException());
return throwError(
() => new HttpException("timeout!!!", HttpStatus.FOUND)
);
}
return throwError(() => err);
})
);
}
}

此外,interceptor 也是可以注入依赖的,你可以通过注入模块内的各种 provider。

全局 interceptor 可以通过 <font style="color:rgb(37, 41, 51);">APP_INTERCEPTOR</font> 的 token 声明,这种能注入依赖,而app.useGlobalInterceptors 无法注入依赖。**<font style="color:rgb(37, 41, 51);">同理 filter、guard、pipe 一样遵循以上规则</font>**

1
2
3
4
5
6
7
8
9
10
11
12
import { APP_INTERCEPTOR } from "@nestjs/core";
@Module({
controllers: [AppController],
providers: [
AppService,
{
provide: APP_INTERCEPTOR,
useClass: globalInterceptor,
},
],
})
export class AppModule {}

interceptor 与 middleware 的区别

它们功能类似,但也有不同,interceptor 可以拿到目标 class、handler 等,进而通过 reflector 拿到它的 metadata 等信息。也可以调用 rxjs 的 operator 来处理响应,interceptor 更适合处理具体的业务逻辑middleware 更适合处理通用的逻辑

Pipe(管道)

Pipe 要实现 <font style="color:rgb(37, 41, 51);">PipeTransform</font> 接口,实现 transform 方法,里面可以对传入的参数值 value 做参数验证,比如格式、类型是否正确,不正确就抛出异常。也可以做转换,返回转换后的值。

在 pipe 里可以拿到 装饰器 handler 参数 的各种信息,基于这些来实现校验和转换就是很简单的事情了。

1
2
3
4
5
6
@Injectable()
export class AaaPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
return metadata.type === 'query' ? value : 'null';
}
}

metadata.**metatype** 可以拿到参数的 Ts 类型

内置的有 9 个 Pipe,从名字就能看出它们的意思:一般用在某个参数中。

  • <font style="color:rgb(37, 41, 51);">ParseIntPipe</font>
  • <font style="color:rgb(37, 41, 51);">ParseBoolPipe</font>
  • <font style="color:rgb(37, 41, 51);">ParseUUIDPipe</font>
  • <font style="color:rgb(37, 41, 51);">DefaultValuePipe</font>
  • <font style="color:rgb(37, 41, 51);">ParseEnumPipe</font>
  • <font style="color:rgb(37, 41, 51);">ParseFloatPipe</font>
  • <font style="color:rgb(37, 41, 51);">ParseFilePipe</font>
  • <font style="color:rgb(37, 41, 51);">ValidationPipe</font>
  • <font style="color:rgb(37, 41, 51);">ParseArrayPipe</font>

以上 Pipe 几乎所有都可以处理 Get 请求的参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

@Get('pipe')
getPipe(
@Query(
'num',
new ParseIntPipe({
exceptionFactory(error) {
throw new HttpException('bzj' + error, HttpStatus.NOT_IMPLEMENTED);
},
optional: true,
}),
)
num: string,
@Query('float', new ParseFloatPipe({ optional: true })) float: number,
@Query('arr', new ParseArrayPipe({ separator: '-' })) arr: Array<number>,
@Query('enum', new ParseEnumPipe(Abc)) enums: Abc,
@Query('default', new DefaultValuePipe('default')) del: string,
) {
return `
num -${num} /
float -${float} /
arr -${arr} /
enum -${enums} /
default -${del}
`;
}
}

而 Post 请求,接收 post 请求的方式是声明一个 dto class(数据传输对象)。对它做验证要用 使用 **<font style="color:rgb(37, 41, 51);">ValidationPipe</font>** 得安装以下依赖 <font style="color:rgb(37, 41, 51);">npm install -D class-validator class-transformer</font> (ParseArrayPipe 也需要这个两依赖)。以下简单实现了一个 ValidationPipe

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { plainToInstance } from "class-transformer";
import { validate } from "class-validator";
@Injectable()
export class MyValidationPipe implements PipeTransform {
async transform(value: any, metadata: ArgumentMetadata) {
// metetype 为参数的Ts类型
const { metatype } = metadata;
if (!metadata) return value;
// class-transformer 将把普通对象转换为 dto class 的实例对象。
const object = plainToInstance(metatype, value);
// class-validator 验证该对象
const errors = await validate(object);
if (errors.length > 0) {
throw new BadRequestException("参数验证失败");
}
return value;
}
}

class-validator 验证 dto 的方法汇总

class-validator 支持很多种验证规则,比如邮箱、域名、长度、值的范围等,而且错误消息也可以自定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import {
Contains,
IsDate,
IsEmail,
IsFQDN,
IsInt,
Length,
Max,
Min,
} from "class-validator";

export class Dto {
@Length(10, 20) // 长度区间
title: string;

@Contains("hello") // 包括
text: string;

@IsInt() // 整数
rating: number;

@Min(0) // 最小
@Max(10) // 最大
num: number;

@IsEmail() // 是否是邮件
email: string;

@IsFQDN() // 是否是域名
site: string;

@IsDate() // 是否是日期
createDate: string;
}

错误内容自定:

1
2
3
4
5
6
@Length(10, 20, {
message({targetName, property, value, constraints}) {
return `${targetName} 类的 ${property} 属性的值 ${value} 不满足约束: ${constraints}`
}
})
title: string;

ExceptionFilter(异常处理)

首先要实现 <font style="color:rgb(37, 41, 51);">ExceptionFilter</font> 接口,实现 catch 方法,就可以拦截异常了,但是要拦截什么异常还需要用 <font style="color:rgb(37, 41, 51);">@Catch</font> 装饰器来声明,拦截了异常之后,可以返回对应的响应,给用户更友好的提示。

Nest 内置了很多 http 相关的异常,都是 HttpException 的子类:

  • <font style="color:rgb(37, 41, 51);">BadRequestException</font>
  • <font style="color:rgb(37, 41, 51);">UnauthorizedException</font>
  • <font style="color:rgb(37, 41, 51);">NotFoundException</font>
  • <font style="color:rgb(37, 41, 51);">ForbiddenException</font>
  • <font style="color:rgb(37, 41, 51);">NotAcceptableException</font>
  • <font style="color:rgb(37, 41, 51);">RequestTimeoutException</font>
  • <font style="color:rgb(37, 41, 51);">ConflictException</font>
  • <font style="color:rgb(37, 41, 51);">GoneException</font>
  • <font style="color:rgb(37, 41, 51);">PayloadTooLargeException</font>
  • <font style="color:rgb(37, 41, 51);">UnsupportedMediaTypeException</font>
  • <font style="color:rgb(37, 41, 51);">UnprocessableException</font>
  • <font style="color:rgb(37, 41, 51);">InternalServerErrorException</font>
  • <font style="color:rgb(37, 41, 51);">NotImplementedException</font>
  • <font style="color:rgb(37, 41, 51);">BadGatewayException</font>
  • <font style="color:rgb(37, 41, 51);">ServiceUnavailableException</font>
  • <font style="color:rgb(37, 41, 51);">GatewayTimeoutException</font>

装饰器列表

  • <font style="color:rgb(37, 41, 51);">@Module</font>: 声明 Nest 模块
  • <font style="color:rgb(37, 41, 51);">@Controller</font>:声明模块里的 controller
  • <font style="color:rgb(37, 41, 51);">@Injectable</font>:声明模块里可以注入的 provider
  • <font style="color:rgb(37, 41, 51);">@Inject</font>:通过 token 手动指定注入的 provider,token 可以是 class 或者 string
  • <font style="color:rgb(37, 41, 51);">@Optional</font>:声明注入的 provider 是可选的,可以为空,就是没有找到这个 provider 也不会报错
  • <font style="color:rgb(37, 41, 51);">@Global</font>:声明全局模块
  • <font style="color:rgb(37, 41, 51);">@Catch</font>:声明 exception filter 处理的 exception 类型
  • <font style="color:rgb(37, 41, 51);">@UseFilters</font>:路由级别使用 exception filter
  • <font style="color:rgb(37, 41, 51);">@UsePipes</font>:路由级别使用 pipe
  • <font style="color:rgb(37, 41, 51);">@UseInterceptors</font>:路由级别使用 interceptor
  • <font style="color:rgb(37, 41, 51);">@SetMetadata</font>:在 class 或者 handler 上添加 metadata
  • <font style="color:rgb(37, 41, 51);">@Get、@Post、@Put、@Delete、@Patch、@Options、@Head</font>:声明 get、post、put、delete、patch、options、head 的请求方式
  • <font style="color:rgb(37, 41, 51);">@Param</font>:取出 url 中的参数,比如 /aaa/:id 中的 id
  • <font style="color:rgb(37, 41, 51);">@Query:</font> 取出 query 部分的参数,比如 /aaa?name=xx 中的 name
  • <font style="color:rgb(37, 41, 51);">@Body</font>:取出请求 body,通过 dto class 来接收
  • <font style="color:rgb(37, 41, 51);">@Headers</font>:取出某个或全部请求头
  • <font style="color:rgb(37, 41, 51);">@Session</font>:取出 session 对象,需要启用 express-session 中间件
  • <font style="color:rgb(37, 41, 51);">@HostParm</font>: 取出 host 里的参数
  • <font style="color:rgb(37, 41, 51);">@Req、@Request</font>:注入 request 对象
  • <font style="color:rgb(37, 41, 51);">@Res、@Response</font>:注入 response 对象,一旦注入了这个 Nest 就不会把返回值作为响应了,除非指定 passthrough 为 true
  • <font style="color:rgb(37, 41, 51);">@Next</font>:注入调用下一个 handler 的 next 方法
  • <font style="color:rgb(37, 41, 51);">@HttpCode</font>: 修改响应的状态码
  • <font style="color:rgb(37, 41, 51);">@Header</font>:修改响应头
  • <font style="color:rgb(37, 41, 51);">@Headers</font>:查询响应头
  • <font style="color:rgb(37, 41, 51);">@Redirect</font>:指定重定向的 url
  • <font style="color:rgb(37, 41, 51);">@Render</font>:指定渲染用的模版引擎

自定义装饰器

class 装饰器和方法装饰器一样,组合多个方法装饰器,可以使用 **<font style="color:rgb(37, 41, 51);">applyDecorators</font>**

1
2
3
4
5
6
7
export function GetSetMetaDataDecorator(path: string, role: string) {
return applyDecorators(
Get(path),
SetMetadata("aaa", role),
UseGuards(AaaGuard)
);
}

通过 **<font style="color:rgb(37, 41, 51);">createParamDecorator</font>** 来创建参数装饰器, 它能拿到 <font style="color:rgb(37, 41, 51);">ExecutionContext</font>,进而拿到 reqeust、response,可以实现很多内置装饰器的功能,比如 @Query、@Headers 等装饰器。

1
2
3
4
5
6
7
8
9
10
11
12
13
export const MyHeaders = createParamDecorator(
(key: string, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return key ? request.headers[key] : request.headers;
}
);

export const MyQuery = createParamDecorator(
(key: string, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return key ? request.query[key] : request.query;
}
);

ExecutionContext: 切换不同上下文

为了让 Filter、Guard、Exception Filter 支持 <font style="color:rgb(37, 41, 51);">http、ws、rpc(tcp微服务)</font> 等场景下复用,Nest 设计了 <font style="color:rgb(37, 41, 51);">ArgumentHost</font><font style="color:rgb(37, 41, 51);">ExecutionContext</font> 类。

ArgumentHost 可以通过 getArgs 或者 getArgByIndex 拿到上下文参数,比如 request、response、next 等。

更推荐的方式是根据 getType 的结果分别 switchToHttp、switchToWs、swtichToRpc,然后再取对应的 argument。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { Response } from "express";
@Catch(AaaException)
export class AaaFilter implements ExceptionFilter {
catch(exception: AaaException, host: ArgumentsHost) {
console.log("[e]===>", exception, host);
if (host.getType() === "http") {
const ctx = host.switchToHttp();
const request = ctx.getRequest<Request>();
const response = ctx.getResponse<Response>();
response.status(500).json({ aaa: exception.aaa, bbb: exception.bbb });
} else if (host.getType() === "ws") {
// wx..
} else if (host.getType() === "rpc") {
// rpc..
}
}
}

而作为<font style="color:rgb(37, 41, 51);">ArgumentHost</font> 子类的 <font style="color:rgb(37, 41, 51);">ExecutionContext</font> 还提供 getClass、getHandler 方法,可以结合 reflector 来取出其中的 metadata

forwardRef 处理循环依赖

Module 之间可以相互 imports,Provider 之间可以相互注入,这两者都会形成循环依赖。

解决方式就是两边都用 forwardRef 来包裹下。

它的原理就是 nest 会先创建 Module、Provider,之后再把引用转发到对方,也就是 forward ref。

1
2
3
4
5
6
7
8
9
10
11
12
13
// AaaModule 依赖 BbbModule
@Module({
imports: [forwardRef(() => BbbModule)],
})
export class AaaModule {}

// DaddService 依赖 CccService
@Injectable()
export class DaddService {
constructor(
@Inject(forwardRef(() => CccService)) private cccService: CccService
);
}

动态模块

Module 可以传入 options 动态产生,这叫做动态 Module,你还可以把传入的 options 作为 provider 注入到别的对象里。在 typeorm、mongoose 等模块中会大量使用

Nest 约定(建议) 的动态产生 Module 的方法名有 registerforRootforFeature 3 种。

  • register:用一次注册一次
  • forRoot:只注册一次,用多次,一般在 AppModule 引入
  • forFeature:用了 forRoot 之后,用 forFeature 传入局部配置,一般在具体模块里 imports

并且这些方法都可以写 xxxAsync 版本,也就是传入 useFactory 等 option,内部注册异步 provider。

1
2
3
4
5
6
7
8
9
10
11
@Module({})
export class BbbModule {
static register(options: Record<string, any>): DynamicModule {
return {
module: BbbModule,
controllers: [BbbController],
providers: [{ provide: "CONFIG_OPTIONS", useValue: options }, BbbService],
exports: [],
};
}
}

这个过程也可以用 <font style="color:rgb(37, 41, 51);">ConfigurableModuleBuilder</font> 来生成。通过 <font style="color:rgb(37, 41, 51);">setClassMethodName</font> 设置方法名,通过 <font style="color:rgb(37, 41, 51);">setExtras</font> 设置额外的 options 处理逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ccc.module-definition.ts 中生成
import { ConfigurableModuleBuilder } from "@nestjs/common";
export interface CccModuleOptions {
name: string;
age: number;
}
export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } =
new ConfigurableModuleBuilder<CccModuleOptions>()
.setClassMethodName("register")
.setExtras({ isGlobal: true }, (definition, extras) => ({
...definition,
global: extras.isGlobal,
}))
.build();

// ccc.module.ts 中使用
import { CccController } from "./ccc.controller";
import { ConfigurableModuleClass } from "./ccc.module-definition";
@Module({
controllers: [CccController],
})
export class CccModule extends ConfigurableModuleClass {}

文件上传

Nest 的文件上传也是基于 **<font style="color:rgb(37, 41, 51);">multer</font>** 实现的,它对 multer api 封装了一层,提供了 <font style="color:rgb(37, 41, 51);">FileInterceptor</font><font style="color:rgb(37, 41, 51);">FilesInterceptor</font><font style="color:rgb(37, 41, 51);">FileFieldsInterceptor</font><font style="color:rgb(37, 41, 51);">AnyFilesInterceptor</font> 的拦截器,分别用到了 multer 包的 single、array、fields、any 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// FileInterceptor 单参数单图片上传
@Post('file')
@UseInterceptors(FileInterceptor('file', { dest: 'uploads' }))
uploadFile(
@UploadedFile() file: Express.Multer.File,
@Body() body,
) {
console.log('[body]===>', body);
console.log('[file]===>', file);
}

// FilesInterceptor 单参数多图片上传
@Post('file')
@UseInterceptors(FilesInterceptor('files', 3, { dest: 'uploads' }))
uploadFile(
@UploadedFiles() file: Express.Multer.File,
@Body() body,
) {
console.log('[body]===>', body);
console.log('[file]===>', file);
}

// FilesInterceptor 多参数多图上传
@Post('files')
@UseInterceptors(
FileFieldsInterceptor(
[
{ name: 'aaa', maxCount: 2 },
{ name: 'bbb', maxCount: 3 },
],
{ dest: 'uploads' },
),
)
uploadFiles(
@UploadedFiles() files: Express.Multer.File,
@Body() body,
) {
console.log('[body]===>', body);
console.log('[files]===>', files);
}

// FilesInterceptor 未知参数多图片上传,自动自动判断是否为文件类型
@Post('any')
@UseInterceptors(AnyFilesInterceptor({ dest: 'uploads' }))
uploadAnyFiles(@UploadedFiles() files: Express.Multer.File, @Body() body) {
console.log('[body]===>', body);
console.log('[files]===>', files);
}

使用前推荐先安装类型:<font style="color:rgb(37, 41, 51);">npm install -D @types/multer</font>

它们把文件解析出来,放到 request 的某个属性上,然后再用 <font style="color:rgb(37, 41, 51);">@UploadedFile</font><font style="color:rgb(37, 41, 51);">@UploadedFiles</font> 的装饰器取出来传入 handler。

并且这个过程还可以使用 <font style="color:rgb(37, 41, 51);">ParseFilePipe</font> 来做文件的验证,它内置了 <font style="color:rgb(37, 41, 51);">MaxFileSizeValidator</font><font style="color:rgb(37, 41, 51);">FileTypeValidator</font>,你也可以实现自己的 FileValidator。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Post('files')
@UseInterceptors(FilesInterceptor('files', 2, { dest: 'uploads' }))
uploadFiles(
@UploadedFiles(
new ParseFilePipe({
validators: [
new MaxFileSizeValidator({ maxSize: 10000 }),
new FileTypeValidator({ fileType: 'image/jpeg' }),
],
exceptionFactory(error) {
throw new HttpException('xx' + error, HttpStatus.BAD_REQUEST);
},
}),
)
files: Express.Multer.File,
@Body() body,
) {
console.log('[body]===>', body);
console.log('[files]===>', files);
}

Logger 日志

日志打印可以用 Nest 的 Logger,它支持在创建应用的时候指定 logger 是否开启,打印的日志级别,还可以自定义 logger。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { Controller, Get, Logger } from "@nestjs/common";
@Controller()
export class AppController {
private logger = new Logger();
constructor(private readonly appService: AppService) {}

@Get()
getHello(): string {
this.logger.debug("aaa", AppController.name);
this.logger.error("bbb", AppController.name);
this.logger.log("ccc", AppController.name);
this.logger.verbose("ddd", AppController.name);
this.logger.warn("eee", AppController.name);
return this.appService.getHello();
}
}

自定义 Logger 需要实现 LoggerService 接口,或者继承 ConsoleLogger 然后重写部分方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// MyLogger.ts
export class MyLogger implements LoggerService {
log(message: string, context: string) {
console.log(`---log---[${context}]---`, message);
}
error(message: string, context: string) {
console.log(`---error---[${context}]---`, message);
}
warn(message: string, context: string) {
console.log(`---warn---[${context}]---`, message);
}
}

// main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger: new MyLogger(),
});
await app.listen(3000);
}
bootstrap();

如果想在 Logger 注入一些 provider,就需要创建应用时设置 <font style="color:rgb(37, 41, 51);">bufferLogs 为 true</font>,然后用 <font style="color:rgb(37, 41, 51);">app.useLogger(app.get(xxxLogger))</font> 来指定 Logger。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// LoggerInject.ts
@Injectable()
export class LoggerInject extends ConsoleLogger {
@Inject(AppService) private readonly appService: AppService;
log(message, context) {
console.log(this.appService.getHello());
console.log(`[${context}]`, message);
console.log("--------------");
}
}

// main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
bufferLogs: true,
});
app.useLogger(app.get(LoggerInject));
await app.listen(3000);
}
bootstrap();

可以把这个自定义 Logger 封装到全局模块,或者动态模块里。但一般情况下,直接使用 Logger 就可以啦

环境配置

nodejs 本身可以通过 <font style="color:rgb(37, 41, 51);">dotenv</font><font style="color:rgb(37, 41, 51);">js-yaml</font> 读取 .envyaml 的配置文件中的信息。然后通过 NODE_ENVIRONMENT (其他名字也可) 来切换不同路径的配置文件,实现开发、生产环境的配置切换。

<font style="color:rgb(37, 41, 51);">export NODE_ENVIRONMENT='production'</font>

yaml 的格式更适合有层次关系的配置,而 .env 更适合简单的配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 使用dotenv
require("dotenv").config({
path:
process.env.NODE_ENVIRONMENT === "production"
? "./.production.env"
: "./.env",
});
console.log("[process.env]===>", process.env.aaa);
console.log("[process.env]===>", process.env.bbb);

// 使用js-yaml
const yaml = require("js-yaml");
const fs = require("fs");
const config = fs.readFileSync("./hello.yaml");
const yaml_config = yaml.load(config);
console.log("[yarm_config]===>", yaml_config);

而 nest 中有现成的包 <font style="color:rgb(37, 41, 51);">@nestjs/config</font>,使用 ConfigModule.forRoot 可以读取 .env 配置文件,然后注入 ConfigService 来取配置。也可通过 ConfigModule.forFeature 来注册局部配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 注册
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
// 相同变量前面的 env 会覆盖后面的的 env
// envFilePath: [
// path.join(process.cwd(), '.aaa.env'),
// path.join(process.cwd(), '.env'),
// ],
load: [config2, config],
}),
BbbModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

// 使用
@Controller()
export class AppController {
@Inject(ConfigService)
private configService: ConfigService;
@Get()
getHello() {
return {
config: this.configService.get('aaa.bbb.ccc'),
};
}
}

<font style="color:rgb(37, 41, 51);">@nestjs/config</font> 可接受 .env,或是 ts 文件的配置文件,或是 yaml 格式转为 ts 格式也可以

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// .env 配置文件
aaa=1
bbb=2

// 普通ts配置文件
export default () => {
return {
port: 3000,
db: {
host: 'localhost',
port: 3306,
},
};
};

// 将yaml转为ts的配置文件
import { readFile } from 'fs/promises';
import * as yaml from 'js-yaml';
import { join } from 'path';

export default async () => {
const configFilePath = join(process.cwd(), 'aaa.yaml');
const config = await readFile(configFilePath);
return yaml.load(config);
};

env 加入 assets(dist)中

如果用了 .env 文件,需要保证它在 src 下,并且要在 <font style="color:rgb(37, 41, 51);">nest-cli.json</font> 里配置 assetswatchAssets,不然 build 的时候不会复制到 dist 下。

1
2
3
4
5
6
7
"compilerOptions": {
"deleteOutDir": true,
"watchAssets": true,
"assets": [
"*.env"
]
}

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!