文章目录
- nest cli
- 项目基本结构
- IOC & DI
- 基础
- 注册值
- 注册时 key 的管理
- 动态注册类
- 工厂函数方式注册
- 设置别名
- 导出 provider
- 模块
- 功能模块
- 模块的导入导出
- 模块类中使用注入
- 全局模块
- 动态模块
- 中间件
- 定义中间件
- 注册中间件
- MiddlewareConsumer 类
- 全局中间件
- 异常过滤器
- 抛出异常
- 自定义异常类
- 内置的异常类
- 自定义异常过滤器类
- ArgumentsHost
- 应用自定义的过滤器
- 扩展异常过滤器
- 管道 pipe
- 内置管道
- 使用内置转换管道
- 自定义管道
- body 数据验证
- 基于对象的模式验证 zod
- 基于类的验证
- 全局验证管道
- 参数默认值 `DefaultValuePipe`
- 守卫 guard
- 自定义授权守卫
- 对 nest 执行上下文的理解
- ArgumentsHost
- ExecutionContext
- 使用守卫
- 基于角色的 controller 鉴权
- 案例
- 拦截器 interceptor 切面编程
- 自定义拦截器
- 应用拦截器
- 案例
- 响应映射
- 异常映射
- 流覆盖 —— 缓存拦截器
- 响应超时拦截
- 自定义装饰器
- 给装饰器传递数据
- 使用管道
- 组合装饰器
pnpm i -g @nestjs/cli
nest cli
cli 提供 nest 命令,有 6 个子命令:
Usage: nest <command> [options]
Options:
-v, --version Output the current version.
-h, --help Output usage information.
Commands:
new|n [options] [name] Generate Nest application.
build [options] [app] Build Nest application.
start [options] [app] Run Nest application.
info|i Display Nest project details.
add [options] <library> Adds support for an external library to your project.
generate|g [options] <schematic> [name] [path] Generate a Nest element.
前 4 个命令不用说。add 命令用于集成一些 nest 适配过的第三方包,generate 命令用生成代码模板。具体有哪些模板查看帮助信息即可。
最好用的要数 resource 模板了,可以直接生成一个 crud。
项目基本结构
src
├── app.controller.spec.ts
├── app.controller.ts
├── app.module.ts
├── app.service.ts
└── main.ts
以下是这些核心文件的简要概述:
app.controller.ts | 带有单个路由的基本控制器示例。 |
---|---|
app.controller.spec.ts | 对于基本控制器的单元测试样例 |
app.module.ts | 应用程序的根模块。 |
app.service.ts | 带有单个方法的基本服务 |
main.ts | 应用程序入口文件。它使用 NestFactory 用来创建 Nest 应用实例。 |
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();
要使用底层框架的 API 时,可以给 create 方法设置泛型:NestExpressApplication
和 NestFastifyApplication
。
const app = await NestFactory.create<NestExpressApplication>(AppModule);
IOC & DI
基础
ioc 一般有两个步骤:
- 标记这个类是可被容器管理的
- 在容器注册表中注册这个类
- 注册就像是填一张 key-value 的表,key 为唯一标识,value 为要被容器管理的内容,通常也就是要注入的类
应用启动时,会实例化所有注册的类。然后当有其他类依赖这个类的实例时,容器就会拿着 key 去容器中找到该类的实例并注入。
nest 容器管理的实例一般都是单例的。
- https://angular.cn/guide/dependency-injection
nest 把可以被容器管理的类称为 提供者 provider。和 invertify-util-express 一样使用@Injectable()
来装饰这个类 。
// 标记可注入的类
import { Injectable } from '@nestjs/common';
@Injectable()
export class CatsService {}
在根模块中注册类:在模块类的 @Module
装饰器中注册。
// 在容器注册表中注册类
import { Module } from "@nestjs/common";
import { CatsService } from './cats/cats.service';
@Module({
providers: [
{
provide: CatsService, // key,这里 key 就是类本身,当然最好用 Symbol
useClass: CatsService, // value
},
];
})
export class AppModule {}
当注册的 key 使用类本身时,可简写。另外对于 controller 这种特殊的注入类,用 @controller 声明,注册也有专门的属性。
@Module({
controllers: [CatsController], // 注册 controller
providers: [CatsService] // 简写
})
export class AppModule {}
和 inversify-express-utils 一样使用@inject("key")
注入依赖:
import { CatsService } from './cats.service';
// 构造注入
class Ikun {
constructor(@inject(CatsService) private readonly catsService: CatsService) {}
}
// 属性注入
class Ikun {
@inject(CatsService)
private readonly catsService: CatsService;
}
但其实上面的代码可以省略@inject("key")
,因为默认容器会把变量类型当做 key 去查找容器中的对象注入。这里类型就是 CatsService 类,而我们注册时本就是将类作为 key,所以不明确指定 inject key ,也能正确注入。
import { Controller, Get } from '@nestjs/common';
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';
@Controller('cats')
export class CatsController {
// 省略@inject(CatsService)
constructor(private readonly catsService: CatsService) {}
@Get()
async findAll(): Promise<Cat[]> {
return this.catsService.findAll();
}
}
注册值
前面为了注入类的实例,注册时,都是注册的类。其实注册时,可以直接注册一个值,这样注入的就一定是这个值。比如直接注册一个类的实例,而不是注册类本身。
这个功能在 mock 时会用到。测试时可以明确注入的是哪个对象。
useValue
表示注册值。
import { CatsService } from './cats.service';
const mockCatsService = {
/* mock implementation
...
*/
};
@Module({
imports: [CatsModule],
providers: [
{
provide: CatsService,
useValue: mockCatsService,
},
],
})
export class AppModule {}
注册时 key 的管理
前面我们直接用类作为 key。当然我们也可以用字符串代替,为了保证一定唯一,最好用 Symbol 代替。这也是 invertify-express-utils 推荐的。
对于自定义 key 的管理,为了清晰的代码组织,最佳实践是在单独的文件(例如 constants.ts )中定义标记。 对待它们就像对待在其自己的文件中定义并在需要时导入的符号或枚举一样。
动态注册类
有些类也想给容器管理,但是注册时又不能写死,因为想使用同一个 key 注入使用。这时可以动态注册类。比如配置类,开发配置类和生产配置类。
const configServiceProvider = {
provide: ConfigService,
useClass:
process.env.NODE_ENV === 'development'
? DevelopmentConfigService
: ProductionConfigService,
};
@Module({
providers: [configServiceProvider],
})
export class AppModule {}
工厂函数方式注册
工厂注册比动态注册更进一步,灵活性拉满。用函数来确定注册的内容。
- useFactory 属性是一个函数,函数的返回值就是会被注册的内容。
- 函数里面可能会依赖其他类的对象实例。如果依赖了,这些套娃注入的对象会作为函数的参数传入。
- inject 属性就和一般的使用注入对象的类一样,接收 key 指明要注入的对象。
- 这里指明要注入 useFactory 函数的类实例。
- 只是 useFactory 函数可能不止依赖一个类,所以 inject 接收一个 key 数组。
工厂函数可以是异步的。
const connectionFactory = {
provide: 'CONNECTION',
useFactory: (optionsProvider: OptionsProvider) => {
const options = optionsProvider.get();
return new DatabaseConnection(options);
},
inject: [OptionsProvider],
};
@Module({
providers: [connectionFactory],
})
export class AppModule {}
设置别名
useExisting 属性允许您为现有的 provider 创建别名。相当于两个 key 都指向同一个注册内容。
感觉这功能可以用来解构,但是这种解耦方式意义不大。真要解耦还得多创建类来解决,名字解构算啥解构。
导出 provider
@Module
装饰器每装饰一次类就是创建了一个容器。
一个容器中要使用另一个容器中管理的对象,那后者容器就要导出才可以。
具体导出内容可以是注册的 key ,也可以是整个注册信息对象,也就是 provider。
使用的模块一方,也要导入要使用的外部模块对象。
const connectionFactory = {
provide: 'CONNECTION',
useFactory: (optionsProvider: OptionsProvider) => {
const options = optionsProvider.get();
return new DatabaseConnection(options);
},
inject: [OptionsProvider],
};
@Module({
imports: [xxxx], // 导入 provider
providers: [connectionFactory],
exports: [connectionFactory], // 导出 connectionFactory provider 整个注册信息对象
})
export class AppModule {}
模块
nest 建议模块化开发。当然,项目小的话,一个根模块走天下也不是不行。
功能模块
nest 建议按功能划分模块,比如用户模块、系统模块。
一个 cats 功能模块的组织结构模板:
src
├──cats
│ ├──dto
│ │ └──create-cat.dto.ts
│ ├──interfaces
│ │ └──cat.interface.ts
│ ├─cats.service.ts
│ ├─cats.controller.ts
│ └──cats.module.ts
├──app.module.ts
└──main.ts
模块的导入导出
新建了一个子模块,怎么在根模块中注册?
只要在根模块 @Module imports
属性中整体导入整个模块即可。
并且导入模块后,就可以直接使用导入模块中 exports 导出的对象了,不用在 imports 中一个一个写出,毕竟整体都被完全导入了。并且模块中共享的实例都是同一个,所以是单例的。
import { Module } from '@nestjs/common';
import { CatsModule } from './cats/cats.module';
@Module({
imports: [CatsModule],
})
export class ApplicationModule {}
模块可以导出他们的内部提供者,还可以再导出自己导入的模块。
模块的组织结构类似一颗树,中间的节点模块,就可能会导入模块又导出模块。
@Module({
imports: [CommonModule],
exports: [CommonModule],
})
export class CoreModule {}
模块类中使用注入
容器管理的实例也可以注入到模块(类)中(例如,用于配置目的):
- 注意循环依赖性,模块类不能注入到提供者中。
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class CatsModule {
// 注入
constructor(private readonly catsService: CatsService) {}
}
全局模块
如果你不得不在任何地方导入相同的模块,那可能很烦人。在 Angular 中,提供者是在全局范围内注册的。一旦定义,他们到处可用。另一方面,Nest 将提供者封装在模块范围内。您无法在其他地方使用模块的提供者而不导入他们。但是有时候,你可能只想提供一组随时可用的东西 - 例如:helper,数据库连接等等。这就是为什么你能够使模块成为全局模块。
import { Module, Global } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Global()
@Module({
controllers: [CatsController],
providers: [CatsService],
exports: [CatsService],
})
export class CatsModule {}
@Global
装饰器使模块成为全局作用域。 全局模块应该只注册一次,最好由根或核心模块注册。 在上面的例子中,CatsService 组件将无处不在,而想要使用 CatsService 的模块则不需要在 imports 数组中导入 CatsModule。
动态模块
- https://nest.nodejs.cn/fundamentals/dynamic-modules
中间件
nest 的中间件是在路由处理程序之前
调用的函数。相当于可以对请求做预处理。
nest 的中间件就和 express 中间件一样,有三个参数。
如果当前的中间件函数没有结束请求-响应周期, 它必须调用 next() 将控制传递给下一个中间件函数。否则, 请求将被挂起。
定义中间件
nest 定义中间件有两种,函数式中间件和类中间件。
函数式中间件就和 express 中间件一模一样。
export function logger(req, res, next) {
console.log(`Request...`);
next();
};
要在类中定义中间件,这个类就要实现 NestMiddleware
接口。并且中间件类也是可以依赖注入的。
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log('Request...');
next();
}
}
注册中间件
中间件不和 provider 一样写在@Module()
装饰器里。而是在模块类的configure()
方法中使用。并且该模块类需要实现NestModule
接口。
在配置中间件时,还可以指定中间件应用的路由和路由的 http 方法。
import { Module, NestModule, RequestMethod, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { CatsModule } from './cats/cats.module';
@Module({
imports: [CatsModule],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
// 可按顺序应用多个中间件
.apply(LoggerMiddleware)
// 将中间件应用在 GET /cats路由上
.forRoutes({ path: 'cats', method: RequestMethod.GET });
}
}
给该路由的所有 http 方法设置中间件。
// 使用 All
.forRoutes({ path: 'cats', method: RequestMethod.All });
// 也可简写
.forRoutes('cats');
路由同样支持模式匹配。例如,星号被用作通配符,将匹配任何字符组合。
forRoutes({ path: 'ab*cd', method: RequestMethod.ALL });
MiddlewareConsumer 类
MiddlewareConsumer 是一个帮助类。它提供了几种内置方法来管理中间件。他们都可以链式调用。
forRoutes()
可接受一个字符串、多个字符串、对象、还可以接受一个 controller 类甚至多个 controller 类。
直接把中间件应用在 controller 上,省的写一堆的路由匹配。
有时我们想从应用中间件中排除某些路由。我们可以使用该exclude()
方法轻松排除某些路由。
此方法可以采用一个字符串,多个字符串或一个 RouteInfo 对象来标识要排除的路由,
consumer
.apply(LoggerMiddleware)
.exclude(
{ path: 'cats', method: RequestMethod.GET },
{ path: 'cats', method: RequestMethod.POST },
'cats/(.*)',
)
.forRoutes(CatsController);
全局中间件
如果我们想一次性将中间件绑定到每个注册路由,我们可以在 INestApplication 实例的 use()
方法中注册:
const app = await NestFactory.create(AppModule);
app.use(logger);
await app.listen(3000);
异常过滤器
过滤器的执行时机是处于洋葱模型的末尾。
nest 内置了全局异常过滤器,它会捕获处理整个应用程序中所有抛出的异常。
全局异常过滤器能识别异常类型为 HttpException(及其子类)的异常。其他异常,会直接响应 500 错误。
{
"statusCode": 500,
"message": "Internal server error"
}
抛出异常
之前我们都是手动定义一个 http 异常工具类,而 nest 内置了这个工具类。
HttpException
构造函数有两个必要的参数来决定响应:
- response 参数定义 JSON 响应体。它可以是 string 或 object,如下所述。
- status参数定义HTTP状态代码。
@Get()
async findAll() {
throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}
{
"statusCode": 403,
"message": "Forbidden"
}
一般我们设计响应会包含两个属性:
- statusCode:默认为 status 参数中提供的 HTTP 状态代码
- message: 基于状态的 HTTP 错误的简短描述
HttpException 构造函数第一个参数可以是对象,自定义错误响应结构。nest 会序列化对象成 json 传输。
@Get()
async findAll() {
throw new HttpException({
status: HttpStatus.FORBIDDEN,
error: 'This is a custom message',
}, HttpStatus.FORBIDDEN);
}
自定义异常类
在许多情况下,您无需编写自定义异常,而可以使用内置的 Nest HTTP异常,如下一节所述。
如果确实需要创建自定义的异常,则最好创建自己的异常层次结构,其中自定义异常继承自 HttpException
基类。 使用这种方法,Nest可以识别您的异常,并自动处理错误响应。
export class ForbiddenException extends HttpException {
constructor() {
super('Forbidden', HttpStatus.FORBIDDEN);
}
}
@Get()
async findAll() {
throw new ForbiddenException();
}
内置的异常类
为了减少样板代码,Nest 提供了一系列继承自核心异常 HttpException 的可用异常。所有这些都可以在 @nestjs/common包中找到:
- BadRequestException:错误的请求异常
- UnauthorizedException:未授权异常
- NotFoundException:未找到异常
- ForbiddenException:禁止异常
- NotAcceptableException:不可接受异常
- RequestTimeoutException:请求超时异常
- ConflictException:冲突异常
- GoneException:已删除异常
- PayloadTooLargeException:负载过大异常
- UnsupportedMediaTypeException:不支持的媒体类型异常
- UnprocessableException:无法处理的异常
- InternalServerErrorException:内部服务器错误异常
- NotImplementedException:未实现异常
- BadGatewayException:错误网关异常
- ServiceUnavailableException:服务不可用异常
- GatewayTimeoutException:网关超时异常
所有内置异常也可以使用 options
参数提供错误 cause
和错误描述:
throw new BadRequestException('Something bad happened', {
cause: new Error(),
description: 'Some error description'
})
{
"message": "Something bad happened",
"error": "Some error description",
"statusCode": 400,
}
自定义异常过滤器类
上面都是基于 nest 内置的捕获逻辑在处理 http 异常。如果我们想捕获其他异常进行处理或者想要更改内置的 http 异常处理逻辑。比如将异常写入日志,修改异常的响应结构等操作。则可以自定义过滤器来处理异常。
所有异常过滤器类都需要实现通用的 ExceptionFilter<T>
接口。并使用 catch(exception: T, host: ArgumentsHost)
方法处理异常。T 表示异常的类型。
异常过滤器类上可使用 @Catch(异常类型, 异常类型, ...)
装饰器,指定要捕获的类型。如果参数为空,则表示捕获所有异常。
让我们创建一个异常过滤器,它负责捕获作为 HttpException 类实例的异常,并为它们设置自定义响应逻辑。为此,我们需要访问底层平台 Request 和 Response。我们将访问 Request 对象,以便提取原始 url 并将其包含在日志信息中。我们将使用 Response.json()
方法,使用 Response 对象直接控制发送的响应。
自定义的 http 异常处理:
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,
});
}
}
ArgumentsHost
ArgumentsHost 对象是一个上下文对象,就和 koa 中的 ctx 一样。ArgumentsHost 是 nest 整个应用的上下文对象,意味着它可以获取 nest 底层框架的一些 API,比如上文就用它获取了 request 和 response 对象。
https://docs.nestjs.cn/8/fundamentals?id=argumentshost%e7%b1%bb
应用自定义的过滤器
使用 @UseFilters
装饰器,可以在方法上或类上使用,使用位置不同,作用范围自然就不同,并且可以使用多个,逗号隔开。
该装饰器可以接收过滤器实例,也可以接收过滤器类,建议传递类,可以节约内存。
@Post()
@UseFilters(new HttpExceptionFilter())
async create(@Body() createCatDto: CreateCatDto) {
throw new ForbiddenException();
}
@UseFilters(HttpExceptionFilter)
export class CatsController {}
全局范围应用过滤器:
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new HttpExceptionFilter());
await app.listen(3000);
}
bootstrap();
扩展异常过滤器
之前是从零自定义异常过滤器,如果希望在已经实现的核心异常过滤器基础上扩展处理逻辑,则可以继承基础异常过滤器 BaseExceptionFilter
并调用继承的 catch()
方法。
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);
}
}
管道 pipe
管道是实现了 PipeTransform
接口,并被容器管理的类。因此要添加 @Injectable() 装饰器。
整出管道这个概念就是用来做请求参数预处理的,所以管道的执行时机肯定在路由处理函数之前。
Nest 在调用方法之前插入一个管道,管道接收指定给该方法的参数并对它们进行操作。任何转换或验证操作都会在此时发生
对参数的处理一般分两种:转换和验证。
那管道自然也分两种:
- 转换:管道将输入数据转换为所需的数据输出
- 验证:对输入数据进行验证。
管道的执行结果是转换或验证成功继续传递; 失败则抛出异常。
内置管道
Nest 自带八个开箱即用的管道,即
-
ValidationPipe: 验证管道
-
DefaultValuePipe: 默认值管道
-
ParseIntPipe: 解析整数管道
-
ParseBoolPipe: 解析布尔值管道
-
ParseArrayPipe: 解析数组管道
-
ParseUUIDPipe: 解析UUID管道
-
ParseEnumPipe: 解析枚举管道
-
ParseFloatPipe: 解析浮点数管道
Parse 开头的管道都有转换与验证参数的能力。以 ParseIntPipe 为例,它确保传给路由处理程序的参数是一个整数(若转换失败,则抛出异常)。
使用内置转换管道
方法参数级别绑定管道
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
return this.catsService.findOne(id);
}
测试:
GET localhost:3000/abc
{
"statusCode": 400,
"message": "Validation failed (numeric string is expected)",
"error": "Bad Request"
}
在上述例子中,我们使用管道用的是类(ParseIntPipe),而不是一个实例,容器会自动依赖注入的。
如果我们想通过传递一些选项来自定义内置管道的行为,那么传递就地实例很有用
@Get(':id')
async findOne(
@Param('id', new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE }))
id: number,
) {
return this.catsService.findOne(id);
}
自定义管道
PipeTransform<T, R>
是一个通用接口,任何管道都必须实现。
- 泛型接口用 T 表示输入 value 的类型,
- 用 R 表示 transform() 方法的返回类型。
每个管道都必须实现 transform()
方法,它有两个参数:
- value 参数是当前处理的方法参数(在被路由处理方法接收之前)
- metadata 是当前处理的方法参数的元数据。
export interface ArgumentMetadata {
type: 'body' | 'query' | 'param' | 'custom';
metatype?: Type<unknown>;
data?: string;
}
type | 指示参数是主体 @Body()、查询 @Query()、参数 @Param() 还是自定义参数(了解更多 此处)。 |
---|---|
metatype | 提供参数的元类型,例如 String。注意:如果你在路由处理程序方法签名中省略类型声明或使用普通 JavaScript,则该值为 undefined。 |
data | 传递给装饰器的字符串,例如 @Body(‘string’)。如果将装饰器括号留空,则为 undefined。 |
示例:我们让它简单地接受一个输入值并立即返回相同的值,表现得像一个恒等函数。
import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';
@Injectable()
export class ValidationPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
return value;
}
}
body 数据验证
之前的验证都是请求头上的参数验证。现在来探讨请求体中的验证,比如创建对象接口验证传递的请求体数据。
方案:
我们可以在路由处理程序方法内执行此操作,但这样做并不理想,因为它会破坏单一职责原则 (SRP)。
另一种方法可能是创建一个验证器类并在那里委派任务。这样做的缺点是我们必须记住在每个方法的开头调用此验证器。express 模板中我就是这么干的。
这些方法都不够好。有几种方法可以用干净的 DRY 方式进行对象验证。
“不要重复自己”(Don’t repeat yourself)是软件开发的一个原则,旨在减少可能改变的信息的重复,用不太可能改变的抽象代替它,或者使用数据规范化,首先避免冗余。
基于对象的模式验证 zod
一种常见的方法是使用基于对象的模式验证。
Zod 验证库
zod 库需要在 tsconfig.json 文件中启用 strictNullChecks 配置。
定义一个用 Zod 验证数据对象的管道:
import { PipeTransform, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { ZodSchema } from 'zod';
export class ZodValidationPipe implements PipeTransform {
constructor(private schema: ZodSchema) {}
transform(value: unknown, metadata: ArgumentMetadata) {
try {
const parsedValue = this.schema.parse(value);
return parsedValue;
} catch (error) {
throw new BadRequestException('Validation failed');
}
}
}
使用 zod 验证管道的流程:
- 创建特定的 Zod 验证规则 schema
- 创建 ZodValidationPipe 的实例,并在管道的类构造函数中传递上下文特定的 Zod 验证规则 schema
- 将管道实例绑定到方法
使用 Zod 验证的 schema 示例:
import { z } from 'zod';
export const createCatSchema = z
.object({
name: z.string(),
age: z.number(),
breed: z.string(),
})
.required();
export type CreateCatDto = z.infer<typeof createCatSchema>;
@UsePipes()
将管道实例绑定到方法。这是方法级别的验证。
@Post()
@UsePipes(new ZodValidationPipe(createCatSchema)) // schema 传递给 zod 管道实例
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
基于类的验证
nest 可以搭配 class-transformer 和 class-validator 基于类实现验证。
这种方式不用像 zod 一样额外定义一个验证的模式类,更简洁。
import { IsString, IsInt } from 'class-validator';
export class CreateCatDto {
@IsString()
name: string;
@IsInt()
age: number;
@IsString()
breed: string;
}
然后和 zod 一样定义一个通用的验证管道进行验证即可。哈哈,这个通用的验证管道,nest 已经内置了,就是 ValidationPipe
。我们直接使用就好了。
- https://nest.nodejs.cn/techniques/validation
这里以类的方式使用,当然也可以使用实例的方式,那样就可以传递一些配置项了。
@Post()
create(@Body(ValidationPipe) createUserDto: CreateUserDto) {
return this.userService.create(createUserDto);
}
ValidationPipe
的内部实现类似于此:
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToInstance } 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 = plainToInstance(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);
}
}
全局验证管道
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();
参数默认值 DefaultValuePipe
有时候我们希望请求的参数有默认值。比如转换相关的 Parse* 管道需要有值的参数值。他们在收到 null 或 undefined 值就会抛出异常。因此我们希望可以给参数设置默认值。
只需在相关的 Parse* 管道之前的 @Query()
装饰器中实例化一个 DefaultValuePipe
,如下所示:
@Get()
async findAll(
@Query('activeOnly', new DefaultValuePipe(false), ParseBoolPipe) activeOnly: boolean,
@Query('page', new DefaultValuePipe(0), ParseIntPipe) page: number,
) {
return this.catsService.findAll({ activeOnly, page });
}
守卫 guard
这也是 nest 整出来的新概念,守卫的设计与异常过滤器、管道和拦截器非常相似,可让你在请求/响应周期的正确位置插入处理逻辑,并以声明方式进行。这有助于使你的代码保持干爽和声明式。
守卫是一个用 @Injectable() 装饰器注释的类,它实现了 CanActivate
接口。
守卫用来鉴权,是作访问控制的中间件,根据运行时存在的某些条件(如权限、角色、ACL 等)确定给定请求是否将由路由处理程序处理。守卫啥,守卫接口。
具体执行时机:
在所有中间件之后、任何拦截器或管道之前执行。
自定义授权守卫
每个守卫都必须实现一个 canActivate()
函数。此函数应返回一个布尔值,指示是否允许当前请求。它可以同步或异步(通过 Promise 或 Observable)返回响应。Nest 使用返回值来控制下一步的动作:
- 如果它返回 true,请求将被处理。
- 如果它返回 false,Nest 将拒绝该请求。
canActivate()
函数只有一个参数,即 ExecutionContext
执行上下文类的实例。
ExecutionContext
继承自 ArgumentsHost
,并提供了一些获取当前执行过程更多详细信息的扩展方法。
- 更多介绍:https://nest.nodejs.cn/fundamentals/execution-context
这里我们只是用了在 ArgumentsHost 上定义的获取 request 对象的辅助方法。
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);
}
}
validateRequest() 为自定义的鉴权函数。函数内的逻辑可以根据需要简单或复杂。这个例子的要点是展示守卫如何适应请求/响应周期。
对 nest 执行上下文的理解
这个上下文就和 koa 中的 ctx 一样。当前请求的执行上下文,包含了很多信息。koa 中间件中拿到了 ctx,就是拿到了一切,可以随意操作该请求。这使得开发很方便。
Nest 也提供了几个实用程序类,帮我们去拿请求的上下文。因为 nest 拿到上下文并不容易,因为它可以有多个执行环境,比如 http 服务,微服务 和 websocket。
nest 提供工具类帮我们屏蔽了底层的差异。
你可能会问,压根不需要屏蔽啊。我什么环境就自己去拿什么上下文不就结了。
这当然是可以的。但是很多时候,我们可能会写一些在各个环境都要使用的代码。比如守卫、过滤器、拦截器。这三个很可能是多环境使用的,当写它们的时候不用关心底层环境,直接统一使用 nest 的工具类拿到上下文信息岂不是爽歪歪。
这个过程就像是 globalThis,不用区分 node 还是 browser。
主要介绍两个这样的类:ArgumentsHost 和 ExecutionContext。
ArgumentsHost
ArgumentsHost 类提供了用于检索传递给处理程序的参数的方法。它允许选择适当的上下文(例如 HTTP、RPC(微服务)或 WebSockets)以从中检索参数。
该框架在你可能想要访问它的地方提供了一个 ArgumentsHost 类的实例,通常作为 host 参数引用。例如,使用 ArgumentsHost 实例调用 异常过滤器 的 **catch()**
方法。
ArgumentsHost 只是作为处理程序参数的抽象。
例如,对于 HTTP 服务器应用(当使用 @nestjs/platform-express 时),host 对象封装了 Express 的 [request, response, next] 数组,其中 request 是请求对象,response 是响应对象,next 是控制应用请求-响应周期的函数。
另一方面,对于 GraphQL 应用,host 对象包含 [root, args, context, info] 数组。
获取某个环境下的处理函数的参数。
比如 express,获取中间件的参数数组。一种方法是使用宿主对象的 getArgs()
方法。也可以使用 getArgByIndex() 方法按索引提取特定参数:
const [req, res, next] = host.getArgs();
const request = host.getArgByIndex(0);
const response = host.getArgByIndex(1);
但这种索引获取的方式不推荐用,因为它将应用耦合到特定的执行上下文。
你可以通过使用 host 对象的实用方法之一切换到适合你的应用的应用上下文,从而使你的代码更加健壮和可重用。上下文切换实用程序方法如下所示。
/**
* Switch context to RPC.
*/
switchToRpc(): RpcArgumentsHost;
/**
* Switch context to HTTP.
*/
switchToHttp(): HttpArgumentsHost;
/**
* Switch context to WebSockets.
*/
switchToWs(): WsArgumentsHost;
切换到 HTTP 上下文环境后,HttpArgumentsHost 对象有两个有用的方法可以用来提取所需的对象。在这种情况下,我们还使用 Express 类型断言来返回原生 Express 类型的对象:
const ctx = host.switchToHttp(); // 切换到 HTTP 环境上下文
const request = ctx.getRequest<Request>();
const response = ctx.getResponse<Response>();
同样,WsArgumentsHost 和 RpcArgumentsHost 具有在微服务和 WebSockets 上下文中返回适当对象的方法。以下是 WsArgumentsHost 的方法:
// websocket 环境下获取环境程序的参数
export interface WsArgumentsHost {
getData<T>(): T; // Returns the data object.
getClient<T>(): T; // Returns the client object.
}
// RPC 环境下获取环境程序的参数
export interface RpcArgumentsHost {
getData<T>(): T; // Returns the data object.
getContext<T>(): T; // Returns the context object.
}
ExecutionContext
ExecutionContext 扩展 ArgumentsHost,提供有关当前执行过程的更多详细信息。与 ArgumentsHost 一样,Nest 在你可能需要的地方提供了 ExecutionContext 的实例,例如 guard 的 **canActivate()**
方法和 interceptor 的 **intercept()**
方法
主要扩展了两个方法:
export interface ExecutionContext extends ArgumentsHost {
/**
* Returns the type of the controller class which the current handler belongs to.
*/
getClass<T>(): Type<T>;
/**
* Returns a reference to the handler (method) that will be invoked next in the
* request pipeline.
*/
getHandler(): Function;
}
getHandler() 方法返回对即将被调用的处理程序的引用。getClass() 方法返回此特定处理程序所属的 Controller 类的类型。例如,在 HTTP 上下文中,如果当前处理的请求是 POST 请求,绑定到 CatsController 上的 create() 方法,则 getHandler() 返回对 create() 方法的引用,getClass() 返回 CatsController 类(不是实例)。
const methodKey = ctx.getHandler().name; // "create"
const className = ctx.getClass().name; // "CatsController"
这种能访问当前类和处理程序方法的引用的能力,提供了极大的灵活性。最重要的是,它使我们可以访问通过 Reflector#createDecorator 创建的装饰器或来自守卫或拦截器内的内置 @SetMetadata()
装饰器设置的元数据。
使用守卫
与管道和异常过滤器一样,守卫可以是 controller 范围、方法范围或全局作用域。
让我们构建一个功能更强大的守卫示例,只允许具有特定角色的用户访问。我们将从一个基本的守卫模板开始,并在接下来的部分中构建它。目前,它允许所有请求继续:
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 作用域的守卫。这个装饰器可以接受一个参数,或者一个逗号分隔的参数列表。这使你可以通过一个声明轻松应用一组适当的保护。
@Controller('cats')
@UseGuards(RolesGuard)
export class CatsController {}
为了设置全局守卫,使用 Nest 应用实例的 useGlobalGuards() 方法:
const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new RolesGuard());
基于角色的 controller 鉴权
上面的例子中 RolesGuard 可以鉴权守卫了,但还是不够智能。因为它没有利用到执行上下文,实现更细致的鉴权,它是全都放行。
例如,CatsController 可以针对不同的路由使用不同的权限方案。有些可能只对管理员用户可用,而另一些可能对所有人开放。
我们如何以灵活且可重用的方式将角色与路由匹配?
答:在路由上声明角色。为了灵活可重用,我们希望有这样一门技术,在运行时给这个接口附加一些额外数据。这个技术就是反射。反射可以在运行时添加一些数据,这些数据也称为元数据 。因为反射是底层的原始操作,底层添加的数据,自然也是原始数据,元数据。俗称亚当夏娃。
- 自定义元数据:https://nest.nodejs.cn/fundamentals/execution-context#reflection-and-metadata
nest 为支持自定义元数据的操作。nest 提供了Reflector#createDecorator
静态方法,该方法可创建装饰器用于自定义元数据。
除了自己创建装饰器, nest 还内置了@SetMetadata()
装饰器,开箱即用给路由附加元数据。当然,使用这个没有自己建的装饰器名称更语义化。
- 了解更多:https://nest.nodejs.cn/fundamentals/execution-context#low-level-approach
例如,让我们使用 Reflector#createDecorator 方法创建一个 @Roles() 装饰器,该方法将元数据附加到处理程序。
import { Reflector } from '@nestjs/core';
export const Roles = Reflector.createDecorator<string[]>();
使用:
@Post()
@Roles(['admin'])
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
上面我们已经给路由设置了元数据,现在就要获取元数据来实现鉴权。
获取元数据依旧依靠反射 Reflector 。
在 Node.js 世界中,通常的做法是将授权的用户信息附加到 request 对象上。比如 password 就是验证 token 后就是把 token 负载添加到 request.user 上。
因此在我们的示例代码中,我们假设 request.user 就包含了用户实例和允许的角色。
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Roles } from './roles.decorator';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const roles = this.reflector.get(Roles, context.getHandler());
if (!roles) {
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user;
return matchRoles(roles, user.roles);
}
}
鉴权失败,默认返回:
{
"statusCode": 403,
"message": "Forbidden resource",
"error": "Forbidden"
}
这是因为,当守卫返回 false 时,框架就会抛出 ForbiddenException。
守卫抛出的任何异常都将由 异常层(全局异常过滤器和应用于当前上下文的任何异常过滤器)处理。
因此如果你想返回不同的错误响应,你应该抛出你自己的特定异常。例如:
throw new UnauthorizedException();
案例
JWT 身份验证:
- https://nest.nodejs.cn/security/authentication
RBAC、声明授权、CASL 真实案例:
- https://nest.nodejs.cn/security/authorization
拦截器 interceptor 切面编程
拦截器这个执行时机牛皮一点,在请求和响应阶段都能执行。这不就是切面 AOP 的环绕通知(Around Advice)吗。
Nest.js 的拦截器更像是 AOP 中的环绕通知(Around Advice),因为它们可以在方法执行之前和之后执行代码。但是,Nest.js 拦截器没有提供 AOP 的全部功能,比如引入(Introduction)和切点表达式(Pointcut Expression)的概念。
AOP 通常涉及以下概念:
- 切点(Pointcut):定义何时(即在哪些连接点)执行通知。
- 通知(Advice):定义切点上要执行的操作(如前置通知、后置通知、环绕通知等)。
- 切面(Aspect):切点和通知的组合。
拦截器可以:
- 在函数执行之前/之后绑定额外的逻辑
- 转换从函数返回的结果
- 转换从函数抛出的异常
- 扩展基本函数行为
- 根据所选条件完全重写函数 (例如, 缓存目的)
自定义拦截器
拦截器是用 @Injectable()
装饰器注释并实现 NestInterceptor
接口的类。
NestInterceptor<T, R> 是一个通用接口,其中 T 表示 Observable(支持响应流)的类型,R 是 Observable 封装的值的类型。
每个拦截器都实现了intercept()
方法,它有两个参数。第一个是 ExecutionContext 上下文实例,前面介绍过。第二个参数是 CallHandler
。
CallHandler 接口实现了 handle()
方法,你可以使用它在拦截器中的某个点调用路由处理程序方法。如果在 intercept()
方法的实现中不调用 handle()
方法,则根本不会执行路由处理程序方法。
这种方式意味着intercept()
方法有效地封装了请求/响应流。因此,你可以在最终路由处理程序执行之前和之后实现自定义逻辑。
很明显,你可以在 intercept()
方法中编写,在调用 handle()
之前执行的代码,这就是在定义路由处理程序执行之前的逻辑,比如参数验证,权限验证啥的。从这里其实也能看出来,拦截器的用途主要体现在响应之后的处理。因为之前的处理已经有管道、守卫这些了。
但是如何影响路由处理方法调用之后发生的情况呢?一旦调用完 controller 里的方法,响应不就结束了吗?结束了你还怎么影响。
别急,nest 引入了 RxJS,实现了观察者模式。
作为前端,你需要知道 RxJS(响应式编程-流)
「RxJS」是使用
Observables
的响应式编程的库,它基于流,使编写异步或基于回调的代码更容易。
这样一看,我们可能会把「RxJS」来和Promise
作一些列比较,因为Promise
也是为了使得编写异步或基于回调的代码更容易而出现的。也确实。
简单列举几点「RxJS」解决了哪些使用Promise
存在的痛点,如:
- 控制异步任务的执行顺序。
- 控制异步任务的开始和暂停。
- 可以获取异步任务执行的各个阶段的状态。
- 监听异步任务执行过程中发生的错误,在并发执行的情况下,不会因为一个异步任务出错而影响到其他异步代码的执行。
handle()
方法返回的就是一个 RxJS Observable,我们可以使用强大的 RxJS 操作符来进一步操作响应。这样我们就实现了在路由处理程序执行之后添加自定义逻辑。
使用面向方面的编程术语,路由处理程序的调用(即调用 handle())称为 切入点,表示它是我们附加逻辑的插入点。
举个例子:
例如,传入 POST /cats 请求。此请求发往 CatsController 内部定义的 create() 处理程序。如果调用了一个不调用 handle() 方法的拦截器,则不会执行 create() 方法。另一面,一旦 handle() 被调用(并且其 Observable 已被返回),则 create() 处理程序将被触发。一旦通过 Observable 接收到响应流,就可以对该流执行其他操作,并将最终结果返回给调用者。
示例:
使用拦截器记录用户的请求信息(例如,存储用户调用、异步调度事件或计算时间戳)。
我们在下面展示一个简单的 LoggingInterceptor:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@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()
运算符,它在可观察流正常或异常终止时调用我们的匿名日志记录函数,但不会以其他方式干扰响应周期。
应用拦截器
@UseInterceptors()
装饰器应用拦截器。与 pipes 和 guards 一样,拦截器可以是控制器作用域级别、方法作用域级别或全局作用域级别。
@UseInterceptors(LoggingInterceptor)
export class CatsController {}
请注意,我们传递了 LoggingInterceptor 类(而不是实例),将实例化的责任留给框架并启用依赖注入。与管道、守卫和异常过滤器一样,我们也可以传递一个就地实例:
@UseInterceptors(new LoggingInterceptor())
export class CatsController {}
为了设置全局拦截器,我们使用 Nest 应用实例的 useGlobalInterceptors()
方法:
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new LoggingInterceptor());
案例
响应映射
我们已经知道 handle() 返回 Observable。该流包含从路由处理程序返回的值,因此我们可以使用 RxJS 的 map()
运算符轻松地改变它。
注意:响应映射功能不适用于特定于库的响应策略(禁止直接使用 @Res() 对象)。也就是 controller 中函数不能用原生对象 res 响应数据。
说白了就是拦截器可以实现统一响应处理。
简单演示一下:
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 })));
}
}
通过上述拦截器,当有人调用 GET /cats 接口时,响应将如下所示(假设路由处理程序返回一个空数组 []):
{
"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())),
);
}
}
流覆盖 —— 缓存拦截器
完全不执行 controller 中的函数,而是从缓存中读取数据返回。也就是用缓存流覆盖了原本的 controller 请求流。
让我们看一下一个简单的缓存拦截器,它从缓存返回其响应。在一个现实的例子中,我们想要考虑其他因素,如 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();
}
}
我们的 CacheInterceptor 有一个硬编码的 isCached 变量和一个硬编码的响应 []。
需要注意的关键点是,我们在这里返回一个由 RxJS of()
运算符创建的新流,因此根本不会调用路由处理程序。当有人调用使用 CacheInterceptor 拦截的接口时,将立即返回响应(硬编码的空数组)。实现了流覆盖。
为了创建通用解决方案,你可以利用 Reflector 并创建自定义装饰器。Reflector 在 guards 章节中有详细描述。
响应超时拦截
使用 RxJS 运算符操纵流的可能性为我们提供了许多功能。让我们考虑另一个常见的用例。想象一下你想要处理路由请求的超时。当你的接口在一段时间后未返回任何内容时,你希望以错误响应终止请求。
以下结构可以实现这一点:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, RequestTimeoutException } from '@nestjs/common';
import { Observable, throwError, TimeoutError } from 'rxjs';
import { catchError, timeout } from 'rxjs/operators';
@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
timeout(5000),
catchError(err => {
if (err instanceof TimeoutError) {
return throwError(() => new RequestTimeoutException());
}
return throwError(() => err);
}),
);
};
};
5 秒后,请求处理将被取消。你还可以在抛出 RequestTimeoutException 之前添加自定义逻辑(例如释放资源)。
自定义装饰器
nest 内置了很多参数装饰器,如 @Session()、@Ip() 等。
- 参数装饰器列表
在 Node.js 中,很多库经常会将传递的值加到 request 对象的 user 属性上,比如 password 就把 token 解析后的结果放到 req.user 上。
因此在每个路由处理程序中我们就得总是手动提取它们,使用如下代码:
const user = req.user;
这太烦了,因此我们可以创建一个 @User()
参数装饰器来完成这个操作。
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const User = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
},
);
使用:
@Get()
async findOne(@User() user: UserEntity) {
console.log(user);
}
给装饰器传递数据
当装饰器的行为取决于某些条件时,可以使用 data 参数将参数传递给装饰器的工厂函数。
比如上面的 user 装饰器获取 token 数据,如果想直接获取 token 负载的对象中的某一个属性的数据,比如 id。那我们就希望装饰器可以接收一个字符串,表示要获取的哪个属性。
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const User = createParamDecorator(
(data: string, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const user = request.user;
return data ? user?.[data] : user;
},
);
@Get()
async findOne(@User('id') firstName: string) {
console.log(`Hello ${firstName}`);
}
对于 TypeScript 用户,请注意 createParamDecorator() 是泛型。这意味着你可以显式强制执行类型安全,例如 createParamDecorator((data, ctx) => …)。或者,在工厂函数中指定参数类型,例如 createParamDecorator((data: string, ctx) => …)。如果两者都省略,则 data 的类型将为 any。
使用管道
nest 内置的解析参数的装饰器都能应用管道,实现数据验证。我们自定义的装饰器如果也要支持传入管道。
则管道必须使用实例的模式,并且将 validateCustomDecorators
选项设为 true。
@Get()
async findOne(
@User(new ValidationPipe({ validateCustomDecorators: true }))
user: UserEntity,
) {
console.log(user);
}
组合装饰器
nest 支持把多个装饰器组合成一个装饰器,也就是复合装饰器。
比如你想要将所有与身份验证相关的装饰器组合成一个装饰器。这可以通过以下构造来完成:
import { applyDecorators } from '@nestjs/common';
export function Auth(...roles: Role[]) {
return applyDecorators(
SetMetadata('roles', roles),
UseGuards(AuthGuard, RolesGuard),
ApiBearerAuth(),
ApiUnauthorizedResponse({ description: 'Unauthorized' }),
);
}
使用,一个顶四个。
@Get('users')
@Auth('admin')
findAllUsers() {}
注意:@nestjs/swagger
包中的 @ApiHideProperty()
装饰器不可组合,并且无法与 applyDecorators
函数一起正常工作。