NestJS 核心技术
Modules 模块
NestJS 使用模块组织应用程序结构,每个模块封装一组密切相关的功能,里面包含依赖的其他模块、路由控制器、事务处理方法等。
每个应用程序至少有一个模块,即根模块。根模块是 Nest 用于构建应用程序图的起点。
模块是由装饰器 @Module()
装饰的类。
在项目中添加一个模块, 如 users:
1 | nest g mo users |
该命令会在 src 文件夹下创建 users 文件夹,其中包含 users.module.ts 文件。
module 文件应该像下面这样:
1 | import { Module } from "@nestjs/common"; |
@Moudle
的元数据有四个字段,它们起着非常重要的作用:
- imports:用来向当前模块导入其他模块。导入其它模块后,其他模块的控制器上下文就被包含在当前模块中,但 Provider 不能被当前模块直接使用,除非在被导入模块中,Provider 被包含在 exports 字段中,否则需要单独导入 Provider 并将其包含在当前模块的 providers 字段中。
- controllers:引用控制器,使控制器作用于当前模块。
- providers:将包含在其中的 Provider 注入到控制器中,以便控制器能直接使用它们。
- exports:导出 Provider,使导入该模块的模块可直接使用包含在其中的 Provider。
Controllers 控制器
控制器负责注册路由,处理传入的请求并将响应返回给客户端。
控制器是由装饰器@Controller()
装饰的类。
在项目中添加一个控制器,如 users:该命令会在 users 文件夹中创建 users.controller.ts 文件。1
nest g co users
controller 文件应该像下面这样:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15import { Body, Controller, Get, Post, Query } from "@nestjs/common";
import { UserDto } from "./user.dto";
"users") (
export class UsersController {
"list") // path = /users/list (
getList("page") page: number, ("limit") limit: number) ( {
return "分页";
}
"add") // path = /users/add (
addUser() () user: UserDto {
return "添加用户"
}
}@Controller
注册模块的根路由,@Get
,@Post
等方法注册子路由。获取请求参数
从 url 获取参数
Get 方法的参数拼在路径后面,用&
分隔,如/users/list?page=1&limit=10
,获取这种参数可使用@Query
装饰器:由于网络传输的都是字符串。因此这里获取到的 page 和 limit 都是 string 类型,想要将其转换成 number 类型,可直接使用管道1
2
3
4
5
6
7"users") (
export class UsersController {
"list") (
getList("page") page: number, ("limit") limit: number) ( {
console.log(page, limit); // "1" "10"
}
}ParseIntPip
。
修改代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16import { ParseIntPipe, UsePipes, Get, Query } from "@nestjs/common";
"users") (
export class UsersController {
"list") (
getList("page", ParseIntPipe) page: number, ("limit", ParseIntPipe) limit: number) ( {
console.log(page, limit); // 1 10
}
// 或者
"list") (
(ParseIntPipe)
getList("page") page: number, ("limit") limit: number) ( {
console.log(page, limit); // 1 10
}
}从 body 获取参数
通过@Body
装饰器来获取 body 中的参数。application/json
application/json 是传输文本最常用的方式,代码入下:1
2
3
4
5
6
7
8
9
10import { Body, Controller, Post } from "@nestjs/common";
import { UserDto } from "./user.dto";
"users") (
export class UsersController {
"add") (
addUser() () user: UserDto {
console.log(user); // { name: "xxx", age: "20", sex: "male" }
}
}UserDto
是一个数据传输对象(DTO,data transfer object),它是一个类,代码如下:其实,用接口声明类型也是可以的,但如果要用管道(Pipes)的话,就必须使用类,因为管道会在运行时访问变量的元类型,而接口在编译过程中就被删除了。1
2
3
4
5
6
7export class UserDto {
name: string;
age: number;
sex: "male" | "female";
}
开启 ValidationPipe 的 tranform 功能后,user 会自动被转换为 UserDto 对象:1
2
3
4
5
6
7
8
9
10
11import { Body, Controller, Post, UsePipes, ValidationPipe } from "@nestjs/common";
import { UserDto } from "./user.dto";
"users") (
export class UsersController {
"add") (
new ValidationPipe({ transform: true })) (
addUser() () user: UserDto {
console.log(user); // UserDto { name: "xxx", age: "20", sex: "male" }
}
}multipart/form-data
multipart/form-data 类型常用来上传文件。假如现在前端要上传这样一个 FormData:后端该如何接收呢?1
2
3const formData = new FormData();
formData.append("file", new Blob(["123456"]), "xxx.txt");
formData.append("name", "Silence");
为了处理文件上传,Nest 提供了一个基于 Express 的 multer 中间件包的内置模块。Multer 处理以 multipart/form-data 格式上传的数据。
为了更好的类型提示,安装 Multer typings 包:此外,还需要用到1
npm i -D @types/multer
FileInterceptor
拦截器,将在下面介绍。通过1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18import { Body, Controller, Post, UploadedFile, UseInterceptors } from "@nestjs/common";
import { Express } from "express";
import { UploadDto } from "./user.dto";
import { FileInterceptor } from "@nestjs/platform-express";
"users") (
export class UsersController {
"upload") (
"file")) (FileInterceptor(
upload(
() upload: UploadDto,
()
file: Express.Multer.File
) {
console.log(upload, file.buffer.toString());
// { name: "Silence" } 123456
}
}@UseInterceptors()
装饰器使用拦截器,FileInterceptor()
拦截器第一个参数是 FormData 中文件对应的 key,通过@UploadedFile()
装饰器将文件信息注入到 file 参数中。
FormData 中除文件之外的字段以及值会被@Body()
注入到 upload 参数中。Express.Multer.File
对象有以下几个属性:
- buffer:Buffer 类型,通过 buffer.toString() 可获取到文件的文本内容
- originalname:文件名
- mimetype:文件的类型
- size:文件大小
我们还可以通过管道对文件进行验证,比如验证文件大小,文件类型等等,未通过验证的话直接向客户端返回响应。
Nest 提供了一个内置的管道来处理常见的异常:ParseFilePipe
,使用方式如下:
1 | import { Body, Controller, Post, UploadedFile, UseInterceptors, ParseFilePipe, |
ParseFilePipe 构造函数的参数包含四个字段:
fileIsRequired
:FormData 中是否必须包含文件,默认为 true,若不包含文件自动响应 400 错误validators
:验证器数组,Nest 内置了 FileTypeValidator 和 MaxFileSizeValidator 两个验证器- FileTypeValidator:验证文件类型,其原理是根据文件后缀名进行验证
- MaxFileSizeValidator:验证文件的大小是否小于给定值(单位字节)
errorHttpStatusCode
:验证失败返回响应的状态码,默认 400exceptionFactory
:异常工厂函数,用来自定义返回异常的内容并记录日志,有了这个就不用再定义 errorHttpStatusCode
如果想进行更多的验证,可以自定义验证器,并将其加入到 validators 数组中。
更多关于文件上穿的内容请见 https://docs.nestjs.com/techniques/file-upload。
获取请求头
通过 @Headers()
装饰器可以将请求头中的内容注入到参数中:
1 | import { Get, Query, Headers } from "@nestjs/common"; |
返回响应
返回文字
返回文字内容只需在路由函数中 return
你想返回的内筒即可。由于 Nest 框架会自动将返回结果序列化,因此我们不需要考虑格式问题。
返回文件
从服务端向客户端发送文件,可以使用 StreamableFile
类,示例如下:
1 | import { Controller, Get, StreamableFile } from '@nestjs/common'; |
默认的内容类型(Content-Type)是 application/octet-stream,且没有文件名,如果你需要自定义这个值,你可以使用 StreamableFile 中的 type 选项:
1 | import { Controller, Get, StreamableFile } from '@nestjs/common'; |
返回状态码
响应状态代码默认为 200,而 POST 请求是 201。我们可以通过 @HttpCode()
装饰器改变响应状态码。
1 | () |
返回响应头
要自定义响应头,可以使用 @Header()
装饰器。
1 | () |
动态返回响应
使用装饰器返回状态码和响应头是静态的,但是大多数情况都是需要根据业务逻辑动态决定返回什么,因此,我们需要有其他操作响应的方式。
Nest 框架允许我们通过 @Res()
装饰器将其底层 http 框架(express)的 response 注入到参数中供我们使用。
1 | import { Controller, Post, Res, HttpStatus } from "@nestjs/common"; |
但是,这种方法会脱离框架提供的响应处理,如 @HttpCode()
,@Header()
将不再起作用,不能直接通过 return 返回响应等等。此外,代码变得依赖于底层平台(express,fastify)。
要解决这个问题,可以将 passthrough
选项设置为 true,如下所示:
1 | import { Controller, Post, Res, HttpStatus } from "@nestjs/common"; |
Providers 提供者
providers 主要用来提供一些辅助性的功能。许多基本的 Nest 类都可以被视为 providers,如 services、repositories、factories、helpers 等等。
实际上,providers 不一定是类,还可以是对象、字符串、数字等类型。
providers 被设计由 controllers 使用。
注意:
@Injectable()
装饰器与 Providers 之间没有必然联系。@Injectable()
意思是可以注入的,指的是可以在被装饰类中注入其他依赖,若不需要注入依赖则不用装饰,而不是只有被装饰的类才可以作为 Provider。
Services 服务
一般来说,controller 只负责接收请求、处理请求参数、返回响应内容,那么具体的业务逻辑就交给 services 去处理。
在项目中添加一个服务,如 users:
1 | nest g s users |
该命令会在 users 文件夹中创建 users.service.ts 文件。
它看起来像下面这样:
1 | import { Injectable } from "@nestjs/common"; |
如需注入其他依赖,应写成这样:
1 | import { Injectable, Inject } from "@nestjs/common"; |
使用方式
services 使用方式有以下两种。
构造函数注入
在 controller 中这样使用:
1 | import { Controller, Get, Query } from "@nestjs/common" |
这里只是将 service 对象作为 controller 的构造函数的参数,就可以直接在路由方法中使用,这是因为 service 的实例化由框架自动完成。
这种将 service 实例注入到构造函数参数中的方式称为构造函数注入。
属性注入
上面的代码也可以替换成这样:
1 | import { Controller, Get, Query, Inject } from "@nestjs/common" |
@Inject()
装饰器会将 service 的实例注入到声明为 service 对象的 controller 的属性中,这种方式称为属性注入。
为什么需要属性注入呢?它与构造函数注入有什么不同?
想象一下,你定义了一个 service,它依赖了其他的 provider,像下面这样:
1 | () |
现在,你想要实现一个子类继承 MyService,那么,你需要在子类的 super()
方法中传递所有参数:
1 | () |
这样的话,MyService 的子类,子类的子类…都需要传递大量参数,非常不便。为避免这种情况,可以使用属性注入的方法,子类不需要 super()
父类中的 Provider 实例。
注意:如果一个 Service 没有被继承,那么建议使用构造函数注入。构造函数显式地概述了需要哪些依赖项,并提供比属性注入更好的可见性。
自定义 Provier
在 Modules 中,我们这样注册 providers:
1 | import { UsersService } from "./users.service"; |
它其实是下面这种写法的缩写:
1 | import { UsersService } from "./users.service"; |
其中,provide
字段是 provider 的 token,用于请求同名类的实例,useClass
指定 provider 提供的类。
Class providers:useClass
对于 Class 类型的 providers,一般使用简写即可,但有些场景需要我们动态选择 providers 的类,就需要用到 useClass
。useClass
语法允许我们动态确定令牌应解析到的类。例如,我们有一个抽象类 ConfigService
,我们希望 Nest 可以根据当前环境提供不同的配置服务。下面的代码实现了这样的策略。
1 | ({ |
由于 DevelopmentConfigService
类和 ProductionConfigService
类都继承自 ConfigService
,因此,在 provider 注入时类型写 ConfigService
即可。
1 | constructor(private readonly configService: ConfigService); |
若想要自定义令牌,如:
1 | import { UsersService } from "./users.service"; |
那么在 controller 文件中就要这样注入依赖:
1 | "users") ( |
Non-class-based providers:useValue
虽然 providers 经常提供服务,但他们并不限于这种用途,providers 可以提供任何值。
1 | ({ |
在 controller 文件中注入:
1 | "users") ( |
Factory providers:useFactory
useFactory
允许动态创建 providers,它的返回值就是 provider 要提供的值。支持异步。inject
字段是一个 providers 数组,数组中的 providers 会按顺序被注入到 userFactory
的参数中供其使用。
1 | ({ |
导出自定义 provider
可以直接导出 token,也可以导出整个 provider。
1 | const logProvider = { |
Middleware 中间件
中间件是在路由处理器之前调用的函数。
中间件函数可以访问 request 和 response 对象,以及 next()
方法(下一个中间件函数)。
中间件有以下几个功能:
- 对 request 和 response 对象进行更改
- 结束请求-响应周期(直接返回响应)
- 调用下一个中间件函数
- 如果当前的中间件函数没有结束请求-响应周期, 它必须调用 next() 将控制传递给下一个中间件函数。否则,请求将被挂起。
我们可以在函数或带有 @Injectable()
装饰器的类中自定义中间件。
类中间件
使用类的话,需要实现 NestMiddleware
接口。
生成一个中间件文件:
1 | nest g mi auth middlewares --flat |
1 | import { Injectable, NestMiddleware } from "@nestjs/common"; |
函数中间件
如果我们想定义一个中间件,它没有成员,没有额外的方法,也没有依赖项,那么完全可以使用一个简单的函数而不是类。
1 | import { Request, Response, NextFunction } from "express"; |
应用中间件
我们需要在 Module 层面应用中间件,Module 类须实现 NestModule
接口的 configure()
方法。
1 | import { MiddlewareConsumer, Module, NestModule } from "@nestjs/common"; |
apply()
方法用于注册中间件,其参数的顺序决定了中间件的调用顺序。forRoutes()
方法用于限制中间件只能作用于符合规定路由的请求。其参数可以是字符出串(路由通配符),RouteInfo 对象,和 Controller。
注意:
configure()
方法支持 async/await。- 中间件的作用范围和它是在哪个模块中被注册的无关,而是取决于路由规则。
MiddlewareConsumer 支持链式调用:
1 | import { RequestMethod } from "@nestjs/common"; |
排除路由
有时候我们想排除某些路由,不应用中间件。可以用 exclusive()
方法。此方法可以采用单个字符串、多个字符串或 RouteInfo 对象来标识要排除的路由,如下所示:
1 | consumer |
设置全局中间件
可以使用 app.use()
方法来注册全局中间件:
1 | const app = await NestFactory.create(AppModule); |
注意:此方法只能注册函数中间件和没有依赖注入的类中间件,对于有依赖注入的类中间件,需要在根模块(或任何其他模块)中用
.forRoutes('*')
。
Pipes 管道
管道有两个典型的应用场景:
- 转换:将输入数据转换为期望的格式,如将字符串转换为数字
- 验证:验证输入的数据,如果合格则通过,否则抛出异常
管道对控制器路由的参数进行操作,Nest 在调用路由方法之前插入一个管道,管道接收指定给方法的参数并对它们进行操作。转换或验证操作都在此时发生,之后路由处理程序使用的是转换后的参数。
Nest 配备了许多内置管道,可以使用开箱即用。
内置管道
- ValidationPipe:验证参数类型,并将其转换为声明的类型
- ParseIntPipe:验证参数类型,并将其转换为 Int 类型
- ParseFloatPipe:验证参数类型,并将其转换为 Float 类型
- ParseBoolPipe:验证参数类型,并将其转换为 Boolean 类型
- ParseArrayPipe:按照规则验证参数并将其转换为数组
- ParseUUIDPipe:验证参数否为 uuid
- DefaultValuePipe:为参数添加默认值
- ParseFilePipe:解析文件
ValidationPipe
ValidationPipe 提供了一种方便的方法来为所有传入的有效载荷实施验证规则,其中特定的规则是通过装饰器装饰 DTO 类的属性来声明的。
在使用之前,需安装依赖:下面介绍一些该管道常用的配置项:1
npm i class-validator class-transformer
选项 | 类型 | 描述 |
---|---|---|
transform | boolean | 为 true 则将参数转换为声明的类型 |
disableErrorMessages | boolean | 为 true 则不会将验证的错误返回给客户端 |
whitelist | boolean | 为 true 则将从返回结果中剥离 DTO 中未用类验证装饰器装饰的属性 |
errorHttpStatusCode | number | 指定返回错误的状态码,默认 400 |
exceptionFactory | Fuction | 获取错误数组并自定义抛出异常 |
自动验证
假如现在定义了这样一个路由处理器:
1 | () |
我们希望能够验证 UserDto 属性的类型,此时就可以将 ValidationPipe
与 class-validator
结合起来。
先为 UserDto 的属性添加验证规则:
1 | import { IsString, IsNumber, IsEnum } from "class-validator"; |
然后在路由处理器中使用 ValidationPipe
:
1 | () |
此时,请求中的载荷会被自动验证,验证失败则会向客户端返回 400 错误,错误信息是一个数组。
我们可以通过 disableErrorMessages: true
禁用详细错误信息,或使用 exceptionFactory
自定义错误信息。
剥离属性
ValidationPipe
还可以过滤掉不应该被方法处理器接收的属性。通过 whitelist: true
来启用这一特性,其原理是被验证装饰器装饰的属性会被加入白名单并保留,而那些没有被装饰的属性就会被过滤掉。
例如 DTO 文件是下面这样:
1 | import { IsString, IsNumber, IsEnum } from "class-validator"; |
那么路由处理器接收到的参数不包含 age
属性。
转换载荷
通过网络进入的有效负载是普通的 JavaScript 对象。ValidationPipe
可以自动将有效负载转换为其 DTO 类的对象。
要启用自动转换,请设置 transform: true
。
启用后,它不仅可以将载荷转换为 DTO 对象,还可以将参数转换为其声明的类型,相当于 ParseIntPipe
、ParseBoolPipe
。
ValidationPipe
一般在全局使用。
ParseArrayPipe
TypeScript 不存储有关泛型或接口的元数据,因此,在 DTO 中使用它们时,ValidationPipe
可能无法正确验证传入的数据。
例如,在下面的代码中,将无法正确验证 users:
1 | () |
而且,users 也不会被转换为 UserDto 对象数组。
这时,就需要 ParseArrayPipe
去验证数组:
1 | () |
这时,数组的验证和转换都正常了。
此外,它还可以根据分隔符将参数转换为数组,例如参数是一个 id 字符串:”1,2,3”:
1 | () |
自定义管道
创建一个 pipe 文件:
1 | nest g pi custom pipes --flat |
我们需要实现 PipeTransform
的 transform
方法。
1 | import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common'; |
transform
方法有两个参数:
value
:当前路由处理方法的参数(在路由处理方法接收到它之前)metadata
:当前路由处理方法参数的元数据,有以下属性:接下来我们实现一个自定义的 ParseIntPipe:1
2
3
4
5
6
7
8export interface ArgumentMetadata {
// 表示提取参数的装饰器类型
type: 'body' | 'query' | 'param' | 'custom';
// 参数的元类型,例如 String,UserDto,或 undefined
metatype?: Type<unknown>;
// 传递给参数装饰器的字符串,例如 @Body('[data]')。如果未传参,它是 undefined。
data?: string;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14import { ArgumentMetadata, BadRequestException, Injectable, PipeTransform } from "@nestjs/common";
()
export class CustomPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
const { metatype, type } = metadata;
if ((type === "query" || type === "param") && metatype.name === "Number") {
const res = new metatype(value);
if (res.toString() === "NaN") throw new BadRequestException();
return res;
}
return value;
}
}管道的使用位置
管道的使用位置有四种。1. 参数管道
管道可以作为@Query()
、@Body()
、@Param()
等装饰器的参数,只作用于当前路由的特定参数。1
2
3
4()
getList("page", ParseIntPipe) page: number, ("limit") limit: number) ( {
console.log(page, limit)
}2. 路由处理器管道
通过@UsePipes()
装饰路由处理器,并将管道传入@UsePipes()
中,此时管道作用于当前路由处理器的所有参数。1
2
3
4
5()
(ParseIntPipe)
getList("page") page: number, ("limit") limit: number) ( {
console.log(page, limit)
}3. 控制器管道
与路由处理器管道类似,只不过@UsePipes()
装饰的是控制器,此时管道作用于当前控制器的所有路由处理器的参数。1
2
3"users") (
(ParseIntPipe)
export class UsersController {}4. 全局管道
这种方式无法进行依赖注入。解决方法:可以作为 provider 在任意模块中设置以供全局使用:1
2
3
4
5const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({ transform: true, disableErrorMessages: true }));
await app.listen(3000);全局管道作用于所有请求的参数。1
2
3
4
5
6
7
8
9({
providers: [
{
provide: "validation-pipe",
useClass: ValidationPipe
}
]
})
export class AppModule {}ExecutionContext 执行上下文
ArgumentsHost
ArgumentsHost
类提供用于检索传递给处理程序的参数的方法。
它允许选择适当的上下文(HTTP、RPC、微服务或 WebSockets)来检索参数。1
2
3
4
5
6
7
8
9
10
11
12/**
* Switch context to RPC.
*/
switchToRpc(): RpcArgumentsHost;
/**
* Switch context to HTTP.
*/
switchToHttp(): HttpArgumentsHost;
/**
* Switch context to WebSockets.
*/
switchToWs(): WsArgumentsHost;Http 上下文
switchToHttp()
方法返回一个适用于 Http 上下文的HttpArgumentsHost
对象,他有两个有用的方法:分别可以获取到 request 对象和 response 对象。1
2
3const ctx = host.switchToHttp();
const request = ctx.getRequest<Request>();
const response = ctx.getResponse<Response>();Websocket 上下文
switchToWs()
方法返回一个适用于 Websocket 上下文的WsArgumentsHost
对象,他有两个有用的方法:分别可以获取到传输数据和客户端连接。1
2
3
4
5
6
7
8
9
10export interface WsArgumentsHost {
/**
* Returns the data object.
*/
getData<T>(): T;
/**
* Returns the client object.
*/
getClient<T>(): T;
}ExecutionContext
ExecutionContext 继承了 ArgumentsHost,提供了关于当前执行进程的其他信息。1
2
3
4
5
6
7
8
9
10
11export 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()
方法返回该路由处理程序所属的控制器的类。
执行上下文有两个重要作用:
- 提供不同上下文的参数检索对象(HttpArgumentsHost,WsArgumentsHost)
- 让我们可以在上下文对象中获取在路由处理程序中添加的元数据
作用一已在上文中说明,下面介绍作用二。
添加元数据
Nest 提供了将自定义元数据附加到路由处理程序的能力,可以通过 Reflector
创建的装饰器,以及内置的 @SetMetadata()
装饰器来实现。
创建一个装饰器文件:
1 | nest g d roles decorators --flat |
Reflector
Reflector 创建强类型装饰器,我们需要指定类型参数。例如,让我们创建一个 Roles 装饰器,它接受一个字符串数组作为参数。
1 | import { Reflector } from '@nestjs/core'; |
使用:
1 | () |
此时,admin
就被附加到了 create
这个路由处理程序上。
SetMetadata
创建一个装饰器:
1 | import { SetMetadata } from '@nestjs/common'; |
与 Reflector 的不同之处在于,我们可以对元数据键和值进行更多控制,并且还可以创建接受多个参数的装饰器。
使用:
1 | () |
获取元数据
Reflector
1 | const roles = this.reflector.get(Roles, context.getHandler()); |
context
为执行上下文对象。
如果 @Roles()
被添加到了控制器上,那么这样获取元数据:
1 | const roles = this.reflector.get(Roles, context.getClass()); |
this.reflector
是 Reflector 对象。
如果我们在两个级别上都提供了 Roles 元数据:
1 | 'user']) ([ |
如果将 "user"
视为默认角色,只想知道该路由处理器是否需要 "admin""
角色,使用以下方法:
1 | const roles = this.reflector.getAllAndOverride(Roles, [context.getHandler(), context.getClass()]); |
如果想要获取所有的角色,使用以下方法:
1 | const roles = this.reflector.getAllAndMerge(Roles, [context.getHandler(), context.getClass()]); |
SetMetadata
通过 SetMetadata 添加的元数据也使用 reflector 获取:
1 | const roles = this.reflector.get<string[]>('roles', context.getHandler()); |
不同的是它传入 get
方法的第一个参数是创建时使用的 key。
Guards 守卫
guards 只有一个责任,它们根据特定的条件(如权限、角色、权限等)来决定路由处理程序是否处理给定的请求,这通常被称为授权。
中间件也是一个很好的身份验证选择,但它不知道在调用 next()
函数后将执行哪个处理程序。而 guards 可以访问 ExecutionContext
执行上下文,因此它知道接下来要执行什么。
Guards 在中间件之后,拦截器和管道之前执行。
定义
创建一个 guards 文件:
1 | nest g gu auth guards --flat |
guards 需要实现 CanActivate
接口的 canActivate
方法:
1 | import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common"; |
canActivate
方法的参数是执行上下文,它应该返回一个布尔值,表示当前请求是否被允许。它也可以通过 通过 Promise
或 Observable
异步地返回一个布尔值。
- 返回
true
,请求将被处理 - 返回
false
,请求将被拒绝,Nest 会自动返回 403。如果想返回一个不同的错误响应,抛出自定义异常应用
假如现在我们使用了 JWT 去做身份验证,其颁发的 token 中包含了用户的角色:admin 和 user,我们需要在 guards 中验证 token 的正确性和时效性,以及角色是否有权访问当前路由。
要知道当前路由绑定了什么角色,需要用到上节学到的知识:执行上下文。
比如现在有这样两个路由:1
2
3
4
5
6
7
8
9
10
11'create') (
'admin']) ([
create() () createCatDto: CreateCatDto {
this.catsService.create(createCatDto);
}
'list') (
'user', 'admin']) ([
get() {
this.catsService.get();
}/create
路由只允许 admin 访问,/list
路由允许 admin 和 user 访问。
接下来定义 guards: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
32import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
import { Observable } from "rxjs";
import { Request } from "express";
import { Reflector } from "@nestjs/core";
import { UnauthorizedException } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { Roles } from "../decorator/roles.decorator";
()
export class AuthGuard implements CanActivate {
constructor(
private reflector: Reflector,
private jwtService: JwtService
) {}
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest<Request>();
const token = request.headers.authorization;
if (!token) {
throw new UnauthorizedException();
}
let role: string;
try {
role = this.jwtService.verify(token).role;
} catch {
throw new UnauthorizedException();
}
const roles = this.reflector.get(Roles, context.getHandler());
if (!roles || roles.includes(role)) return true;
throw new UnauthorizedException();
}
}使用
guards 可以在方法级别(路由处理器)、类级别(控制器)和全局级别使用。方法级别
1
2
3
4
5()
(AuthGuard)
get() {
return [];
}类级别
1
2
3'users') (
(AuthGuard)
export class UsersController {}全局
但这种方式无法进行依赖注入,因此,可以在任意模块中将其作为 provider 供全局使用。1
2const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new AuthGuard());1
2
3
4
5
6
7
8
9({
providers: [
{
provide: "auth-guard",
useClass: AuthGuard
},
],
})
export class AppModule {}Interceptors 拦截器
拦截器有以下几个功能:
- 在函数执行之前/之后添加额外逻辑
- 转换从函数返回的结果
- 转换函数引发的异常
- 扩展基本功能
- 根据特定条件完全覆盖函数(例如,用于缓存)
这里的”函数“指的是路由处理程序。
定义
创建一个拦截器文件:
1 | nest g itc custom interceptors --flat |
拦截器需要实现 NestInterceptor
的 intercept
方法:
1 | import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common"; |
intercept
方法的第一个参数是执行上下文,第二个参数是一个回调函数,CallHandler.handle()
是当前的路由处理程序,如果它没被调用,路由程序方法就不会执行。
在调用 CallHandler.handle()
之前的代码逻辑实现了前向拦截,那如何实现后向拦截呢?由于 CallHandler.handle()
返回一个 Observable
,因此我们可以使用 RxJS 操作符来操作响应。
tap 操作符:打印日志
1 | import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common"; |
map 操作符:修改响应数据
1 | import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common"; |
catchError 操作符:覆盖异常
1 | import { CallHandler, ExecutionContext, ForbiddenException, Injectable, NestInterceptor } from "@nestjs/common"; |
创建操作符:覆盖流
有时候我们想在不调用事件处理程序的情况下返回响应,那么就不能调用 CallHandler.handle()
方法,但是需要返回一个 Observable
对象,这时就可以使用创建操作符去创建一个新的 Observable
对象并返回。
常见的创建操作符有 of
、from
、empty
等等。
假如我们设置了缓存,如果缓存时间没到,则不触发路由处理程序,直接返回缓存中的内容,否则通过路由处理程序返回。
1 | import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common"; |
使用
与 Guards 一样,拦截器也有三个作用域:路由处理程序、控制器、全局。
路由处理程序
1 | () |
控制器
1 | (CustomInterceptor) |
全局
在任意模块中作为 provider 供全局使用。
1 | ({ |