跳到主要内容

Nestjs-基本概念

控制器

控制器(Controller)负责处理客户端请求并发送响应内容,在传统的 MVC 架构中控制器就是负责处理指定请求与应用程序的对应关系,路由则决定具体处理哪个请求。

路由

得益于 TypeScript,在 Nest 中我们可以使用类来实现控制器的功能,使用装饰器来实现路由功能。它们分别需要配合 @Controller 和 @Get 饰器来使用,前者是控制器类的装饰,后者是具体方法的装饰器。

比如下面的代码:

import { Controller, Get } from '@nestjs/common';

@Controller('cats')
export class CatsController {
@Get()
findAll(): string {
return 'This action returns all cats';
}
}

上面的代码声明了一个猫咪控制器类,实现了 findAll 方法,当你在浏览器中发送请求到 /cats 时程序就返回给你 This action returns all cats

小提示:可以使用 Nest-cli 工具来自动生成上面的代码:$ nest g controller cats

@Get() 表示 HTTP 请求装饰器。控制器类的装饰器和 HTTP 方法的装饰器共同决定了一个路由规则。findAll 将返回一个状态码为 200 的响应,当然你有两种方法来指定返回的状态码:

标准模式(建议的)使用内置方法时,如果返回一个 JavaScript 对象或者数据,将自动序列化成 JSON,如果是字符串将默认不会序列化,响应的返回状态码默认总是 200,除非是 POST 请求会默认设置成 201。可以使用 @HttpCode() 装饰器来改变它
指定框架也可以使用指定框架的请求处理方法,比如 Express 的响应对象。可以使用 @Res() 装饰器来装饰响应对象使用,这样以来你就可以使用类 Express API 的方式处理响应了:response.status(200).send()

警告:你可以同时使用上面两种方法,但是 Nest 会检测到,同时标准模式会在这个路由上被禁用

请求对象

处理器一般需要访问到请求对象。一般配合 @Req() 装饰器来使用,请求对象包含查询字符串、参数、HTTP 头,请求体等。但是大多数情况只用到其中某个,我们可以单独使用指定的装饰器来装饰请求。

@Request()req
@Response()res
@Next()next
@Session()req.session
@Param(key?: string)req.params / req.params[key]
@Body(key?: string)req.body / req.body[key]
@Query(key?: string)req.query / req.query[key]
@Headers(name?: string)req.headers / req.headers[name]<br>

举个例子:比如我们只需要处理请求的查询字符串(query string),就可以使用 @Query 来装饰入参,这样取到的值就自然是一个 query string 的字典了。

@Get()
getHello(@Query() q: String): string {
console.log(q)
return this.appService.getHello();
}

如果我们的请求是:http://localhost:3000/?test=a

那么控制台将打印一个 { test: 'a' } 字典

小提示:建议安装 @types/express 包来获取 Request 的相关类型提示

资源

除了使用 @Get 装饰器,我们还可以使用其它 HTTP 方法装饰器。比如:@Put()@Delete()@Patch()@Options()@Head(), and @All(),注意 All 并不是 HTTP 的方法,而是 Nest 提供的一个快捷方式,表示接收任何类型的 HTTP 请求。

路由通配符

Nest 支持基于模式的路由规则匹配,比如:星号(*)表示匹配任意的字母组合。

@Get('ab*cd')

The 'ab*cd' 路由将匹配 abcdab_cdabecd 等规则。同时:?+*, and () 通配符(wildcard)都可以使用

通配符说明示例匹配不匹配
*匹配任意数量的任意字符Law*LawLaws, or LawyerGrokLawLa, or aw
*Law*LawGrokLaw, or Lawyer.La, or aw
?匹配任意单个字符?atCatcatBat or batat
[abc]匹配方括号中的任意一个字符[CB]atCat or Batcat or bat
[a-z]匹配字母、数字区间Letter[0-9]Letter0Letter1Letter2 up to Letter9LettersLetter or Letter10

状态码

响应的默认状态码是 200,POST 则是 201,我们可以使用装饰器 @HttpCode(204) 来指定处理器级别的 默认 HttpCode 为 204

@Post()
@HttpCode(204)
create() {
return 'This action adds a new cat';
}

如果想动态指定状态码,就要使用 @Res() 装饰器来注入响应对象,同时调用响应的状态码设置方法。

请求头

同样的我们可以使用 @Header() 来设置自定义的请求头,也可以使用 response.header() 设置

@Post()
@Header('Cache-Control', 'none')
create() {
return 'This action adds a new cat';
}

路由参数

通常我们需要设置一些动态的路由来接收一些客户端的查询参数,通过指定路由参数可以很方便的捕获到 URL 上的动态参数到控制器中。

@Get(':id')
findOne(@Param() params): string {
console.log(params.id);
return `This action returns a #${params.id} cat`;
}

通过使用 @Param() 装饰器可以在方法中直接访问到路由装饰器 @Get() 中的的参数字典,:id 就表示匹配到所有的字符串,可以通过引用 params.id 在方法中访问到。

当然,就像前面学到的参数装饰器也可以指定到具体的某个参数值:

@Get(':id')
findOne(@Param('id') id): string {
return `This action returns a #${id} cat`;
}

路由顺序

路由的注册顺序与控制器类中的方法顺序相关,如果你先装饰了一个 cats/:id 的路由,后面又装饰了一个 cats 路由,那么当用户访问到 GET /cats 时,后面的路由将不会被捕获,因为参数才都是非必选的。

Providers

Provider 主要的设计理念来自于控制反转(Inversion of Control,简称 IOC )模式中的依赖注入(Dependency Injection)特性。使用 @Injectable() 装饰的类就是一个 Provider,装饰器方法会优先于类被解析执行。

到这里我们应该要了解整个 Nest 框架的三层结构,Nest 和传统的 MVC 框架的区别在于它更注重于后端部分(控制器、服务与数据)的架构,视图层相对比较独立,完全可以由用户自定义配置。

Nest 的分层借鉴自 Spring,更细化。随着代码库的增长 MVC 模式中 Modal 和 Controller 会变得含糊不清,导致难于维护。

Services

我们可以自己实现一个名叫 CatsService 的 Service

export interface Cat {
name: string;
age: number;
breed: string;
}
import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';

@Injectable()
export class CatsService {
private readonly cats: Cat[] = [];

create(cat: Cat) {
this.cats.push(cat);
}

findAll(): Cat[] {
return this.cats;
}
}
小提示:也可以使用 CLI 工具自动生成一个 Service $ nest g service cats

有了 Service 我们就可以在控制器中注入并引用到它了

@Controller('cats')
export class CatsController {
constructor(private readonly catsService: CatsService) {}
// 等同于
private readonly catsService: CatsService
constructor(catsService: CatsService) {
this.catsService = catsService
}

@Post()
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}

@Get()
async findAll(): Promise<Cat[]> {
return this.catsService.findAll();
}
}

依赖注入的很多种方法,Nest 使用了构建函数注入的方式,看起来非常直观。这个时候我们就可以发现 Nest 的优点了,至少你能发现 Controller 和 Service 处于完全解耦的状态:Controller 做的事情仅仅是接收请求,并在合适的时候调用到 Service,至于 Service 内部怎么实现的 Controller 完全不在乎。

这样以来有两个好处:其一,Controller 和 Service 的职责边界很清晰,不存在灰色地带;其二,各自只关注自身职责涉及的功能,比方说 Service 通常来写业务逻辑,但它也仅仅只与业务相关。当然你可能会觉得这很理想,时间长了增加了诸如缓存、验证等逻辑后,代码最终会变得无比庞大而难于维护。事实上这也是一个框架应该考虑和抽象出来的,后续 Nest 会有一系列的解决方法,但目前为至我们只需要了解到 Controller 和 Service 的设计原理即可。

依赖注入

constructor(private readonly catsService: CatsService) {}

得益于 TypeScript 类型,Nest 可以通过 CatsService 类型查找到 catsService,依赖被查找并传入到控制器的构造函数中。

通常我们在没有依赖注入的时候如果 A 依赖于 B,那么在 A 初始化或者执行中的某个过程需要先创建 B,这时我们就认为 A 对 B 的依赖是正向的。但是这样解决依赖的办法会得得 A 与 B 的逻辑耦合在一起,依赖越来越多代码就会变的越来越糟糕。如下图所示,齿轮之间是相互依赖的,一损俱损。

控制反转(IOC)模式就是要解决这个问题,它会多引入一个容器(Container)的概念,让一个 IOC 容器去管理 A、B 的依赖并初始化。

当我们去掉容器时,剩下的齿轮成了一个个独立的功能模块。

注入作用域

Providers 有一个和应用程序一样的生命周期。当应用启动,每个依赖都必须被获取到。将会有单独的一章来讲解注入作用域

自定义的 Providers

Nest 有一个内置的 IOC 容器,用来解析 Providers 之间的关系。这个功能相对于 DI 来讲更底层,但是功能却异常强大,@Injectable() 只是冰山一角。事实上,你可以使用值,类和同步或者异步的工厂。

可选的 Providers

有时候,你可以会需要一个依赖,但是这个依赖并不需要一定被容器解析出来。比如我们通常会传入一个配置对象,但是如果不传会使用一个默认值代替。可以使用 @Optional() 来装饰一个非必选的参数。

@Injectable()
export class HttpService<T> {
constructor(
@Optional()
@Inject('HTTP_OPTIONS')
private readonly httpClient: T
) {}
}

基于属性的注入

前面我们提过了 Nest 实现注入是基于类的构造函数的,但是在一些特殊情况下,基于属性的注入会特别有用。

比如一个顶层的类依赖一个或多个 Providers 时,通过在子类的构造函数中调用 super() 方法并不是很优雅,为了避免这种情况我们可以在属性上使用 @Inject() 装饰器。

@Injectable()
export class HttpService<T> {
@Inject('HTTP_OPTIONS')
private readonly httpClient: T;
}
警告:如果你的类并没有继承其它 Provider,那么一定要使用基于构造函数注入方式

注册 Provider

一般来讲控制器就是 Service 的消费(使用)者,我们需要将这些 Service 注册到 Nest 上,这样就可以让 Nest 帮你完成注入操作。通常我们会使用 @Module 装饰器来完成注册的过程。

import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';
import { CatsService } from './cats/cats.service';

@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class ApplicationModule {}

模块(Module)

模块(Module)是一个使用了 @Module() 装饰的类。@Module() 装饰器提供了一些 Nest 需要使用的元数据,用来组织应用程序的结构。

每个应用都至少有一个根模块,根模块就是 Nest 应用的入口。Nest 会从这里查找出整个应用的依赖/调用

@Module() 装饰器接收一个参数对象,有以下取值:

providers可以被 Nest 的注入器初始化的 providers,至少会在此模块中共享
controllers这个模块需要用到的控制器集合
imports引入的其它模块集合
exports此模块提供的 providers 的子集,其它模块引入此模块时可用

模块默认会封装 providers,如果要在不同模块之间共享 provider 可以在 exports 参数中指定。

功能模块

使用下面的代码可以将相关的控制器和 Service 包装成一个模块:

import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class CatsModule {}
小提示:也可以使用 CLI 来自动生成模块:$ nest g module cats

这样我们就完成了一个模块的封装。

共享的模块

在 Nest 中模块默认是单例的,因此你可在不同的模块之间共享任意 Provider 实例。

模块都是共享的,我们可以通过导出当前模块的指定 Service 来实现其它模块对 Service 的复用。

import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Module({
controllers: [CatsController],
providers: [CatsService],
exports: [CatsService] // 导出
})
export class CatsModule {}

模块的重复导出

给模块包装一层即可实现:

@Module({
imports: [CommonModule],
exports: [CommonModule],
})
export class CoreModule {}

依赖注入

模块的构造函数中也可以注入指定的 providers,通常用在一些配置参数场景。

@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class CatsModule {
constructor(private readonly catsService: CatsService) {}
}

但是模块类本身并不可以装饰成 provider,因为这会造成循环依赖

全局模块

当一些模块在你的应用频繁使用时,可以使用全局模块来避免每次都要调用的问题。Angular 会把 provider 注册到全局作用域上,然而 Nest 会默认将 provider 注册到模块作用域上。如果你没有显示的导出模块的 provider,那么其它地方就无法使用它。

如果你想让一个模块随处可见,那就使用 @Global() 装饰器来装饰这个模块。

@Global()
@Module({
controllers: [CatsController],
providers: [CatsService],
exports: [CatsService],
})
export class CatsModule {}

@Global() 装饰器可以让模块获得全局作用域

动态模块

Nest 模块系统支持动态模块的功能,这将让自定义模块的开发变得容易。

import { Module, DynamicModule } from '@nestjs/common';
import { createDatabaseProviders } from './database.providers';
import { Connection } from './connection.provider';

@Module({
providers: [Connection],
})
export class DatabaseModule {
static forRoot(entities = [], options?): DynamicModule {
const providers = createDatabaseProviders(options, entities);
return {
module: DatabaseModule,
providers: providers,
exports: providers,
};
}
}

模块的静态方法 forRoot 返回一个动态模块,可以是同步或者异步模块。

中间件(Middleware)

中间件就是一个函数,在路由处理器之前调用。这就表示中间件函数可以访问到请求和响应对象以及应用的请求响应周期中的 next() 中间间函数。

Nest 中间件实际上和 Express 的中间件是一样的,Express 文档中对中间件的描述如下:

中间件函数主要做以下的事情:

  • 执行任意的代码

  • 对请求/响应做操作

  • 终结请求-响应周期

  • 调用下一个栈中的中间件函数

  • 如果当前的中间间函数没有终结请求响应周期,那么它必须调用 next() 方法将控制权传递给下一个中间件函数。否则请求将被挂起

Nest 允许你使用函数或者类来实现自己的中间件。如果用类实现,则需要使用 @Injectable() 装饰,并且实现 NestMiddleware 接口。

import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response } from 'express';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: Function) {
console.log('Request...');
next();
}
}

依赖注入

中间件也是支持依赖注入的,就像其它支持方式一样,你可以使用构造函数注入依赖。

应用中间件

@Module() 装饰器中并不能指定中间件参数,我们可以在模块类的构 configure() 方法中应用中间件,下面的代码会应用一个 ApplicationModule级别的日志中间件 LoggerMiddleware

@Module({
imports: [CatsModule],
})
export class ApplicationModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes('cats');
}
}

上面的代码 forRoutes 方法表示只将中间件应用在 cats 路由上,还可以是指定的 HTTP 方法,甚至是路由通配符:

.forRoutes({ path: 'cats', method: RequestMethod.GET });
.forRoutes({ path: 'ab*cd', method: RequestMethod.ALL });

当然,你也可以指定不包括某些路由规则:

consumer
.apply(LoggerMiddleware)
.exclude(
{ path: 'cats', method: RequestMethod.GET },
{ path: 'cats', method: RequestMethod.POST }
)
.forRoutes(CatsController);

不过请注意 exclude 方法不能运用在函数式的中间件上,而且这里指定的 path 也不支持通配符,这只是个快捷方法,如果你真的需要某种路由级别的控制,那完全可以把逻辑写在一个单独的中间件中。

函数式的中间件

函数式的中间件可以用一个简单无依赖函数来实现:

export function logger(req, res, next) {
console.log(`Request...`);
next();
};

多个中间件

apply 方法传入多个中间件参数即可:

consumer.apply(cors(), helmet(), logger)
.forRoutes(CatsController);

全局中间件

在实现了 INestApplication 接口的实例上调用 use() 方法即可:

const app = await NestFactory.create(ApplicationModule);
app.use(logger);
await app.listen(3000);

异常过滤器

Nest 框架内部实现了一个异常处理层,专门用来负责应用程序中未处理的异常。

默认情况未处理的异常会被全局过滤异常器 HttpException 或者它的子类处理。如果一个未识别的异常(非 HttpException 或未继承自 HttpException)被抛出,下面的信息将被返回给客户端:

{
"statusCode": 500,
"message": "Internal server error"
}

基础异常

我们可以从控制器的方法中手动抛出一个异常:

@Get()
async findAll() {
throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}

客户端将收到如下信息:

{
"statusCode": 403,
"message": "Forbidden"
}

当然你也可以自定义返回状态值和错误信息:

@Get()
async findAll() {
throw new HttpException({
status: HttpStatus.FORBIDDEN,
error: 'This is a custom message',
}, 403);
}

异常的级别

比较好的做法是实现你自己想要的异常类。

export class ForbiddenException extends HttpException {
constructor() {
super('Forbidden', HttpStatus.FORBIDDEN);
}
}

然后你就可以手动在需要的地方抛出它。

@Get()
async findAll() {
throw new ForbiddenException();
}

HTTP 异常

Nest 内置了以下集成自 HttpException 的异常类:

  • BadRequestException

  • UnauthorizedException

  • NotFoundException

  • ForbiddenException

  • NotAcceptableException

  • RequestTimeoutException

  • ConflictException

  • GoneException

  • PayloadTooLargeException

  • UnsupportedMediaTypeException

  • UnprocessableEntityException

  • InternalServerErrorException

  • NotImplementedException

  • BadGatewayException

  • ServiceUnavailableException

  • GatewayTimeoutException

异常过滤器

如果你想给异常返回值加一些动态的参数,可以使用异常过滤器来实现。例如下面的异常过滤器将会给 HttpException 添加额外的时间缀和路径参数:

import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();

response
.status(status)
.json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
});
}
}

注意:所有的异常过滤器都必须实现泛型接口 ExceptionFilter\<T>。就是说你必须要提供一个 catch(exception: T, host: ArgumentsHost) 方法

参数宿主

上面代码中的 host 参数是一个类型为 ArgumentsHost 的原生请求处理器包装对象。根据应用程序的不同它具有不同的接口。

export interface ArgumentsHost {
getArgs<T extends Array<any> = any[]>(): T;
getArgByIndex<T = any>(index: number): T;
switchToRpc(): RpcArgumentsHost;
switchToHttp(): HttpArgumentsHost;
switchToWs(): WsArgumentsHost;
}

绑定过滤器

可以使用 @UseFilters 装饰器让一个控制器方法具有过滤器处理逻辑。

@Post()
@UseFilters(HttpExceptionFilter)
async create(@Body() createCatDto: CreateCatDto) {
throw new ForbiddenException();
}

当然过滤器可以被使用在不同的作用域上:方法作用域、控制器作用域、全局作用域。比如应用一个控制器作用域的过滤器,可以这么写:

@UseFilters(new HttpExceptionFilter())
export class CatsController {}

全局过滤器可以通过如下代码实现:

async function bootstrap() {
const app = await NestFactory.create(ApplicationModule);
app.useGlobalFilters(new HttpExceptionFilter());
await app.listen(3000);
}
bootstrap();

不过这样注册的全局过滤器无法进入依赖注入,因为它在模块作用域之外。为了解决这个问题,你可以在根模块上面注册一个全局作用域的过滤器。

import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';

@Module({
providers: [
{
provide: APP_FILTER,
useClass: HttpExceptionFilter,
},
],
})
export class ApplicationModule {}

捕获所有异常

@Catch() 装饰器不传入参数就默认捕获所有的异常:

import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common';

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();

const status = exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;

response.status(status).json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
});
}
}

继承

通常你可能并不需要自己实现完全定制化的异常过滤器,可以继承自 BaseExceptionFilter 即可复用内置的过滤器逻辑。

import { Catch, ArgumentsHost } from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';

@Catch()
export class AllExceptionsFilter extends BaseExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
super.catch(exception, host);
}
}

管道(Pipes)

管道(Pipes)是一个用 @Injectable() 装饰过的类,它必须实现 PipeTransform 接口。

从官方的示意图中我们可以看出来管道 pipe 和过滤器 filter 之间的关系:管道偏向于服务端控制器逻辑,过滤器则更适合用客户端逻辑。

过滤器在客户端发送请求处理,管道则在控制器接收请求处理。

管道通常有两种作用:

  • 转换/变形:转换输入数据为目标格式

  • 验证:对输入数据时行验证,如果合法让数据通过管道,否则抛出异常。

管道会处理控制器路由的参数,Nest 会在方法调用前插入管道,管道接收发往该方法的参数,此时就会触发上面两种情况。然后路由处理器会接收转换过的参数数据并处理后续逻辑。

小提示:管道会在异常范围内执行,这表示异常处理层可以处理管道异常。如果管道发生了异常,控制器的执行将会**停止**

内置管道

Nest 内置了两种管道:ValidationPipe 和 ParseIntPipe

import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';

@Injectable()
export class ValidationPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
return value;
}
}

注意这里可能不太好理解,因为我们前面已经在控制器参数上使用了 @body 装饰器,并且使用 TypeScript 的类型声明它为 CreateCatDto,如下:

async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}

但是 TypeScript 类型是静态的、编译时类型,当编译成 JavaScript 后在运行时并没有任何类型校验。这时我们就需要自己去验证,或者借助第三方工具、库来验证。

Nest 官方文档在这一节中使用了 joi 这个验证库。这个验证库的使用需要传入一个 schema,实际上对应着我们的在 Nest 中写的 dto 类型,所以我们只需要给 joi 传入一个 CreateCatDto 类的实例即可。

首页在 ValidationPipe 管道中添加 joi 的验证功能。验证通过就返回,不通过直接抛出异常:

@Injectable()
export class JoiValidationPipe implements PipeTransform {
constructor(private readonly schema: Object) {}

transform(value: any, metadata: ArgumentMetadata) {
const { error } = Joi.validate(value, this.schema);
if (error) {
throw new BadRequestException(SON.stringify(error.details));
}
return value;
}
}

绑定管道

管道有了,我们还需要在控制器方法上绑定它。

@Post()
@UsePipes(new JoiValidationPipe(createCatSchema))
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}

使用 @UsePipes 修饰器即可,传入管道的实例,并构造 schema。此时我们的应用就可以在运行时通过 schema 去校验参数对象的开头了。createCatSchema 的写法可以参考相关文档

const createCatSchema = {
name: Joi.string().required(),
age: Joi.number().required(),
breed: Joi.string().required(),
}

例如上面的 schema,如果客户端发送的 POST 请求中如果缺少任意参数 Nest 都会捕获到这个异常并返回信息:

{
"statusCode": 400,
"error": "Bad Request",
"message": "[{\"message\":\"\\\"name\\\" is required\",\"path\":[\"name\"],\"type\":\"any.required\",\"context\":{\"key\":\"name\",\"label\":\"name\"}}]"
}

注意 message 就是我们在管道中传到异常类 BadRequestException 中的参数。

类验证器

当然上面这种方法看起来没那么优雅,因为毕竟 CreateCatDto 和 createCatSchema 太重复了。Nest 还支持类型验证器,虽然也需要借助于三方库,但是看起来会优雅很多。

首先,要使用类验证器,你需要先安装 class-validator 库。

npm i --save class-validator class-transformer

class-validator 可以让你使用给类变量加装饰器的写法给类添加额外的验证功能。这样以来我们就可以直接在原始的 CreateCatDto 类上添加验证装饰器了,这样看起来就整洁多了,而且还没有重复代码:

import { IsString, IsInt } from 'class-validator';

export class CreateCatDto {
@IsString()
readonly name: string;

@IsInt()
readonly age: number;

@IsString()
readonly breed: string;
}

不过管道验证器中的代码也需要适配一下:

import { validate } from 'class-validator';
import { plainToClass } from 'class-transformer';

@Injectable()
export class ValidationPipe implements PipeTransform<any> {
async transform(value: any, { metatype }: ArgumentMetadata) {
if (!metatype || !this.toValidate(metatype)) {
return value;
}
const object = plainToClass(metatype, value);
const errors = await validate(object);
if (errors.length > 0) {
throw new BadRequestException('Validation failed');
}
return value;
}

private toValidate(metatype: Function): boolean {
const types: Function[] = [String, Boolean, Number, Array, Object];
return !types.includes(metatype);
}
}

注意这次的 transform 是 async 异步的,因为内部需要用到异步验证方法。Nest 是支持你这么做的,因为管道可以是异步的。

然后我们可以插入这个管道,位置可以是方法级别的,也可以是参数级别的。

参数作用域
@Post()
async create(
@Body(new ValidationPipe()) createCatDto: CreateCatDto,
) {
this.catsService.create(createCatDto);
}
方法作用域
@Post()
@UsePipes(new ValidationPipe())
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}

管道修饰器入参可以是类而不必是管道实例:

@Post()
@UsePipes(ValidationPipe)
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}

这样以来将实例化过程留给框架去做并肝启用依赖注入。

由于 ValidationPipe 被尽可能的泛化,所以它可以直接使用在全局作用域上。

async function bootstrap() {
const app = await NestFactory.create(ApplicationModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();

转换用例

我们还可以用管道来进行数据转换,比如说上面的例子中 age 虽然声明的是 int 类型,但是我们知道 HTTP 请求传递的都是纯字符流,所以通常我们还要把期望传进行类型转换。

import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';

@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
transform(value: string, metadata: ArgumentMetadata): number {
const val = parseInt(value, 10);
if (isNaN(val)) {
throw new BadRequestException('Validation failed');
}
return val;
}
}

上面这个管道的功能就是强制转换成 Int 类型,如果转换不成功就抛出异常。我们可以针对性的对传入控制器的某个参数插入这个管道:

@Get(':id')
async findOne(@Param('id', new ParseIntPipe()) id) {
return await this.catsService.findOne(id);
}

内置的验证管道

比较贴心的是 Nest 已经内置了如上面的例子类似的一些通用验证器,你可以以参数的方式去实例化 ValidationPipe。

@Post()
@UsePipes(new ValidationPipe({ transform: true }))
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}

ValidationPipe 接收一个 ValidationPipeOptions 类型的参数,并且这个参数继承自 ValidatorOptions

export interface ValidationPipeOptions extends ValidatorOptions {
transform?: boolean;
disableErrorMessages?: boolean;
exceptionFactory?: (errors: ValidationError[]) => any;
}

ValidatorOptions 又继承了如下所有 class-validator 的参数:

OptionTypeDescription
skipMissingPropertiesbooleanIf set to true, validator will skip validation of all properties that are missing in the validating object.
whitelistbooleanIf set to true, validator will strip validated (returned) object of any properties that do not use any validation decorators.
forbidNonWhitelistedbooleanIf set to true, instead of stripping non-whitelisted properties validator will throw an exception.
forbidUnknownValuesbooleanIf set to true, attempts to validate unknown objects fail immediately.
disableErrorMessagesbooleanIf set to true, validation errors will not be returned to the client.
exceptionFactoryFunctionTakes an array of the validation errors and returns an exception object to be thrown.
groupsstring[]Groups to be used during validation of the object.
dismissDefaultMessagesbooleanIf set to true, the validation will not use default messages. Error message always will be undefined if its not explicitly set.
validationError.targetbooleanIndicates if target should be exposed in ValidationError
validationError.valuebooleanIndicates if validated value should be exposed in ValidationError.

守卫(Guards)

守卫(Guards)是一个使用 @Injectable() 装饰的类,它必须实现 CanActivate 接口。

守卫只有一个职责,就是决定请求是否需要被控制器处理。一般用在权限、角色的场景中。

守卫和中间件的区别在于:中间件很简单,next 方法调用后中间的任务就完成了。但是守卫需要关心上下游,它需要鉴别请求与控制器之间的关系。

守卫会在中间件逻辑之后、拦截器/管道之前执行。

授权守卫

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class AuthGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();
return validateRequest(request);
}
}

canActivate 返回 true,控制器正常执行,false 请求会被 deny

执行上下文

ExecutionContext 不但继承了 ArgumentsHost,还有两个额外方法:

export interface ExecutionContext extends ArgumentsHost {
getClass<T = any>(): Type<T>;
getHandler(): Function;
}

getHandler() 方法会返回一个将被调用的方法处理器,getClass() 返回处理器对应的控制器类。

基于角色的认证

我们来实现一个小型的基于角色的认证系统。

创建一个守卫,先让它返回 true,后面再改:

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class RolesGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
return true;
}
}

绑定守卫

就像过滤器一样,守卫可以是控制器作用域的,也可以是方法作用域或者全局作用域。我们使用 @UseGuards 来引用一个控制器作用域的守卫。

@Controller('cats')
@UseGuards(RolesGuard)
export class CatsController {}

如果想引用到全局作用域可以调用 useGlobalGuards 方法。

const app = await NestFactory.create(ApplicationModule);
app.useGlobalGuards(new RolesGuard());

由于我们在根模块外层引用了全局守卫,这时守卫无法注入依赖。所以我们还需要在要模块上引入。

import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';

@Module({
providers: [
{
provide: APP_GUARD,
useClass: RolesGuard,
},
],
})
export class ApplicationModule {}

反射

虽然现在已经有了守卫,但是它还没有执行上下文。CatsController 应该有一些需要访问到的权限类型。比如:管理员(admin)角色可以访问、其它角色不可以。

这时我们需要对控制器(或方法)添加一些元数据,用来标记这个控制器的权限类型。在 Nest 中我们通常使用 @SetMetadata() 装饰器来完成这个工作。

@Post()
@SetMetadata('roles', ['admin'])
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}

上面的代码表示给 create 方法设置角色的元数据,用来标识 create 方法只能是 roles 关联的一些角色(admin…)才能访问到的。

如果你觉得 SetMetadata 这个装饰器看着不是那么见名知意,也可以实现一个自定义的装饰器。

import { SetMetadata } from '@nestjs/common';

export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

这样就可以用更简洁的方式来声明角色权限关系了:

@Post()
@Roles('admin')
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}

联合在一起使用

我们将使用反射机制来获取控制器上的元数据。

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';
import { Reflector } from '@nestjs/core';

@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}

canActivate(context: ExecutionContext): boolean {
const roles = this.reflector.get<string[]>('roles', context.getHandler());
if (!roles) {
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user;
const hasRole = () => user.roles.some((role) => roles.includes(role));
return user && user.roles && hasRole();
}
}

当 canActivate 方法返回 false 时,Nest 将会抛出一个 ForbiddenException 异常。你也可以手动抛出别的异常:

throw new UnauthorizedException();

拦截器(Interceptors)

拦截器(Interceptors)是一个使用 @Injectable() 装饰的类,它必须实现 NestInterceptor 接口。

拦截器有一系列的功能,这些功能的设计灵感都来自于面向切面的编程(AOP)技术。这使得下面这些功能成为可能:

  • 在函数执行前/后绑定额外的逻辑

  • 转换一个函数的返回值

  • 转换函数抛出的异常

  • 扩展基础函数的行为

  • 根据特定的条件完全的重写一个函数(比如:缓存)

基础

每个拦截器都要实现 intercept() 方法,此方法有两个参数。第一个是 ExecutionContext 实例(这和守卫中的对象一样)。ExecutionContext 继承自 ArgumentsHost。上一节中我们见过,它是一个包装了传递向原始处理器而且根据应用的不同包含不同的参数数组的类

执行上下文

ExecutionContext 通过继承 ArgumentsHost,提供了更多的执行过种中的更多细节,它看起来长这样:

export interface ExecutionContext extends ArgumentsHost {
getClass<T = any>(): Type<T>;
getHandler(): Function;
}

getHandler() 方法返回一个将会被调用的路由处理器的引用。getClass() 方法返回控制器类的类型。例如,如果当前进行着一个 POST 请求,假定它会由 CatsController 的 create() 方法处理,那么 getHandler() 方法将返回 create() 方法的引用,而 getClass() 则会返回 CatsController 的类型(非实例)

调用处理器

第二个参数是一个 CallHandler。CallHandler 接口实现了 handle() 方法,这个方法就是你可以在你拦截器的某个地方调用的路由处理器。如果你的 intercept() 方法中没调用 handle() 方法,那么路由处理器将不会被执行。

不像守卫与过滤器,拦截器对于一次请求响应有完全的控制权与责任。这样的方式意味着 intercept() 方法可以高效地包装请求/响应流。因此,你可以在最终的路由处理器执行前/后实现自己的逻辑。显然,你已经可以通过在 intercept() 方法中的 handle() 调用之前写自己的代码,但是后续的逻辑应该如何处理?因为 handle() 方法返回的是一个 Observable,我们可以使用 RxJS 做到修改后来的响应。使用 AOP 技术,路由处理器的调用被称做一个 切点(Pointcut),这表示一个我们的自定义的逻辑插入的地方。

假如有一个 POST /cats 的请求,这个请求将被 CatsController 中的 create() 方法处理。如果一个没调用 handle() 方法的拦截器在某处被调用,create() 方法将不会被执行。一但 handle() 方法被调用(它的 Observable 已返回),create() 处理器将被触发。一但响应流通过 Observable 接收到,附加的操作可以在注上被执行,最后的结果将返回给调用方。

切面拦截

我们将要研究的第一个例子就是用户登录的交互。下面展示了一个简单的日志拦截器:

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log('Before...');

const now = Date.now();
return next
.handle()
.pipe(
tap(() => console.log(`After... ${Date.now() - now}ms`)),
);
}
}

由于 handle() 方法返回了一个 RxJS 的 Observable 对象,对于修改流我们将有更多的选择。上面的示例中我们使用了 tap() 操作符。它在 Observable 流的正常或异常终止时调用我们的匿名日志记录函数,但不会干扰到响应周期。

绑定拦截器

我们可以使用 @UseInterceptors() 装饰器来绑定一个拦截器,和管道、守卫一样,它即可以是控制器作用域的,也可以是方法作用域的,或者是全局的。

@UseInterceptors(LoggingInterceptor)
export class CatsController {}

上面的实现,在请求进入 CatsController 后,你将看到下面的日志输出。

Before...
After... 1ms

响应映射

我们已经知道了 handle() 方法返回一个 Observable。流包含路由处理器返回的值,因此,我们可以很容易的使用 RxJS 的 map() 操作符改变它。

注意:响应映射功能并不适用于库级别的响应策略(不可以使用 @Res 装饰器)

让我们新建一个 TransformInterceptor,它可以修改每个响应。它将使用 map() 操作符来给响应对象符加 data 属性,并且将这个新的响应返回给客户端。

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

export interface Response<T> {
data: T;
}

@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
return next.handle().pipe(map(data => ({ data })));
}
}

当有请求进入时,响应看起来将会是下面这样:

{
"data": []
}

拦截器对于创建整个应用层面的可复用方案有非常大的意义。比如说,我们需要将所有响应中出现的 null 值改成空字符串 ""。我们可以使用拦截器功能仅用下面一行代码就可以实现

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable()
export class ExcludeNullInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next
.handle()
.pipe(map(value => value === null ? '' : value ));
}
}

异常映射

另外一个有趣的用例是使用 RxJS 的 catchError() 操作符来重写异常捕获:

import {
Injectable,
NestInterceptor,
ExecutionContext,
BadGatewayException,
CallHandler,
} from '@nestjs/common';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';

@Injectable()
export class ErrorsInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next
.handle()
.pipe(
catchError(err => throwError(new BadGatewayException())),
);
}
}

流重写

有一些情况下我们希望完全阻止处理器的调用并返回一个不同的值。比如缓存的实现。让我们来试试使用缓存拦截器来实现它。当然真正的缓存实现还包含 TTL,缓存验证,缓存大小等问题,我们这个例子只是一个简单的示意。

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable, of } from 'rxjs';

@Injectable()
export class CacheInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const isCached = true;
if (isCached) {
return of([]);
}
return next.handle();
}
}

上面的代码中我们硬编码了 isCached 变量,以及返回的缓存数据 []。关键点在于我们返回了一个新的流,使用了 RxJS 的 of() 操作符。因此路由处理器永远不会被调用。为了实现一个更完整的解决方案,你可以通过使用 Reflector 创建一个自定义的装饰器来实现缓存功能。

更多的操作符

RxJS 的操作符有很多种能力,我们可以考虑下面这种用例。你需要处理路由请求的超时问题。当你的响应很久都没正常返回时,你会想把它关闭并返回一个错误的响应。

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { timeout } from 'rxjs/operators';

@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(timeout(5000))
}
}

5 秒后,请求处理将会被取消。

自定义装饰器

ES2016装饰器是一个返回函数的表达式,可以将目标、名称和属性描述符作为参数。通过在装饰符前面加上@字符并将其放在要装饰的内容的最顶端来应用它。可以为类、方法或属性定义修饰符。

Nest是围绕一个叫做Decorators的语言特性构建的。在许多常用的编程语言中,装饰器是一个众所周知的概念,但在Javascript中,它是一个比较新的概念。

Nest的装饰器可以分为三类:核心类、Http类和模块类。

核心装饰器

  • 异常过滤器装饰器@Catch:用于声明一个普通的类为异常过滤器;仅用于类装饰;
  • 控制器装饰器@Controller:用于著名某个类为控制器;仅用于类装饰;
  • 绑定异常过滤器装饰器@UseFilters:用于将某个控制器类或者方法与制定的异常过滤器关联;
  • 显式依赖声明装饰器@Inject:在构造函数中需要被注入的依赖项,显式声明类型;
  • 被依赖装饰器@Injectable:用于将类型交由Nest托管,并在必要时注入依赖类;
  • 可选项装饰器@Optional:用于声明某个参数为可选;仅用于参数装饰;
  • 设置源数据装饰器@SetMetadata:用于设置对象的源数据信息,K V值;
  • 绑定守卫装饰器@UseGuards:用于为类或者方法绑定守卫;
  • 绑定拦截器装饰器@UseInterceptors:用于为类或者方法绑定制定拦截器;
  • 绑定管道装饰器@UsePipes:用于为某个方法或者类数绑定管道;

Http相关的装饰器

  • 自定义参数装饰器@createParamDecorator:严格意义上来说,它并不是一种装饰器,而是装饰器模板,本章后续再具体讲解;
  • 限制响应头装饰器@Header:用于限制响应数据中Header的字段值;仅用于方法装饰;
  • Http状态码装饰器@HttpCode:用户方法返回的Http状态码;仅用于方法装饰;
  • 重定向装饰器@Redirect:用于将请求重定向到其他资源上;仅用于方法装饰;
  • 渲染装饰器@Render:用于指明请求方法需要渲染的HTML模板;仅用于方法装饰;
  • 请求映射装饰器@RequestMapping:请求路径映射的基础方法,有这个方法加上不同的请求方式GetPost等,扩展出@Get@Post等装饰器。仅用于方法装饰;
  • 路由参数装饰,以createRouteParamDecorator为基础函数,扩展出@Request@Response@Next@Ip@Session@Headers装饰器,用于提取底层平台等相关参数或对象。此处的@Header装饰器与上文的不同,这里所有的装饰器均限于参数装饰器;
  • 管道路由参数装饰器,以createPipesRouteParamDecorator为基础函数,扩展出@Query@Body@Param@HostParam装饰器,用于提取请求对象中的相关参数(数据)的装饰器;
  • 文件上传装饰器@UploadFile:以Multer为逻辑内核,以createPipesRouteParamDecorator为基础函数的装饰器,用于处理上传文件;
  • SSE装饰器@Sse:用于将某个方法定义为SSE方法的装饰器;

模块装饰器

  • 全局装饰器@Global:定义某个类为全局范围内有效;
  • 模块装饰器@Module:定义某个普通类为模块属性;

自定义装饰器

在Nest框架中,有两类自定义装饰器,一种是开篇将Nest命令行的时候有提到generator命令中有一个decorator的创建自定义装饰器的命令;还有一种是专门用于HTTP下的路由参数装饰器。利用用命令创建的装饰器,主要是用于设置对象的元数据,它可以用于类、方法或者参数,而后者仅可以用于参数装饰。我们先来看看用命令行nest g d ID生成的装饰器源码:

import { SetMetadata } from '@nestjs/common';

export const Id = (...args: string[]) => SetMetadata('id', args);

以下是createParamDecorator函数,仅用于定义HTTP请求中的路由参数。以下是源码:

export type CustomParamFactory<TData = any, TInput = any, TOutput = any> = (
data: TData, input: TInput,
) => TOutput;

export function createParamDecorator<
FactoryData = any,
FactoryInput = any,
FactoryOutput = any
>(
factory: CustomParamFactory<FactoryData, FactoryInput, FactoryOutput>,
enhancers: ParamDecoratorEnhancer[] = [],
): (
...dataOrPipes: (Type<PipeTransform> | PipeTransform | FactoryData)[]
) => ParameterDecorator {
const paramtype = uuid();
return (
data?,
...pipes: (Type<PipeTransform> | PipeTransform | FactoryData)[]
): ParameterDecorator => (target, key, index) => {
const args =
Reflect.getMetadata(ROUTE_ARGS_METADATA, target.constructor, key) || {};

const isPipe = (pipe: any) =>
pipe &&
((isFunction(pipe) &&
pipe.prototype &&
isFunction(pipe.prototype.transform)) ||
isFunction(pipe.transform));

const hasParamData = isNil(data) || !isPipe(data);
const paramData = hasParamData ? (data as any) : undefined;
const paramPipes = hasParamData ? pipes : [data, ...pipes];

Reflect.defineMetadata(
ROUTE_ARGS_METADATA,
assignCustomParameterMetadata(
args,
paramtype,
index,
factory,
paramData,
...(paramPipes as PipeTransform[]),
),
target.constructor,
key,
);
enhancers.forEach(fn => fn(target, key, index));
};
}

TS还不是特别精通的同学看到这段代码可能有点晕了(不管怎样,还是建议同学们细品之)。createParamDecorator方法类型限制为返回一个参数装饰器的闭包。从上述代码的const args =...开始时正式逻辑,过程大致分为:读取装饰对象(即被装饰参数)的元数据,判断参数类型,重新写入元数据。不难看出createParamDecorator是一个类似于工厂模式的装饰器产生函数。两者用途有着明显的差别。

创建一个自定义的装饰器

打开利用先前命令行创建的装饰器程序文件,修改一下:

import { SetMetadata } from '@nestjs/common';
import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const ID = createParamDecorator(
(data: string, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const id = request.query.id;
return id;
},
);

再将其安排到一个路由中:

@Get('a')
idQuery(@ID() id): string {
console.log(id);
return this.appService.getHello();
}

眼尖的同学可能已经发现了,这不就是一个管道吗~也确实如此。不过除了管道常规操作外,我们还有SetMetadata方法可以使用。

使用管道

我们创建一个管道nest g pipe Double,随便填充一些代码,例如将输入的值平方一下:

import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common';

@Injectable()
export class DoublePipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
return value ** 2;
}
}

将管道应用到这个装饰器中:

  @Get('a')
idQuery(@ID(new DoublePipe()) id): string {
console.log(id);
return this.appService.getHello();
}

再次访问这个url curl -X GET "http://localhost:3000/a?id=4",控制台输出了平房后的ID值。甚至可以使用多个管道操作:

  @Get('a')
idQuery(@ID(new DoublePipe(), new DoublePipe()) id): string {
console.log(id);
return this.appService.getHello();
}

自定义装饰器组合多种元素

Nest框架中有个名为applyDecorators方法,可以将多个装饰器组合使用,源码:

export function applyDecorators(
...decorators: Array<ClassDecorator | MethodDecorator | PropertyDecorator>
) {
return <TFunction extends Function, Y>(
target: TFunction | object, propertyKey?: string | symbol, descriptor?: TypedPropertyDescriptor<Y>, ) => {
for (const decorator of decorators) {
if (target instanceof Function && !descriptor) {
(decorator as ClassDecorator)(target);
continue;
}
(decorator as MethodDecorator | PropertyDecorator)(
target,
propertyKey,
descriptor,
);
}
};
}

可见,其参数只要是装饰器类的,都可以被支持。例如将先前的一些案例都集成进项目后,可以写这样一个组合:

import {
SetMetadata,
UseFilters,
UseGuards,
UseInterceptors,
} from '@nestjs/common';
import { applyDecorators } from '@nestjs/common';
import { AllexceptionFilter } from './allexception.filter';
import { AuthGuard } from './auth.guard';
import { ClassInterceptor } from './class.interceptor';

export function Custom(...allow: Array<string>) {
return applyDecorators(
SetMetadata('allowHeader', allow),
UseGuards(AuthGuard),
UseFilters(AllexceptionFilter),
UseInterceptors(ClassInterceptor),
);
}

路由的方法中这样使用:

  @Get()
@Custom('FireFox', 'Chrome')
getHello(): string {
return this.appService.getHello();
}

由于第一条组合中就设置了对象的元数据,所以在后续的任意一个中间过程中,都可以调取这个元数据。例如,我们在守卫中加入以下代码:

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Observable } from 'rxjs';

@Injectable()
export class AuthGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
console.log(
this.reflector.get<string[]>('allowHeader', context.getHandler()),
);
return true;
}
}

执行到此时,将SetMetadata的元数据给读取出来了。

Nest可以分别对类、方法、属性和参数的元数据进行读取和写入。当前,读取的前提条件就是获取上下文。写入元数据的方法是SetMetadata()方法,它已经封装了根据被装饰对象(类、方法、属性还是参数)的判断以及写入逻辑,源代码如下:

export const SetMetadata = <K = string, V = any>(
metadataKey: K,
metadataValue: V,
): CustomDecorator<K> => {
const decoratorFactory = (target: object, key?: any, descriptor?: any) => {
if (descriptor) {
Reflect.defineMetadata(metadataKey, metadataValue, descriptor.value);
return descriptor;
}
Reflect.defineMetadata(metadataKey, metadataValue, target);
return target;
};
decoratorFactory.KEY = metadataKey;
return decoratorFactory;
};

尾声

Nestjs的基本概念了解的查不到了,现在可以开始编写第一个Nestjs程序了。