NestJS-系列
NestJS[一]-基础
RxJS
NestJS[二]-核心
NestJS[三]-进阶
NestJS[四]-数据库

上传文件

Nest内置了Express的 Multer 中间件,它只处理 multipart/form-data 类型的表单数据,主要用于上传文件文档

类型提示:npm i -D @types/multer,接着就可以使用 Express.Multer.File 类型,表示上传的文件信息

1
2
3
4
5
6
7
8
9
10
11
12
interface File {
fieldname: string; // 字段名
originalname: string; // 原始文件名
encoding: string; // 编码
mimetype: string; // 文件类型
size: number; // 文件大小
stream: Readable; // 文件流
destination: string; // 文件存储目录
filename: string; // 文件名
path: string; // 文件路径
buffer: Buffer; // 文件缓存
}

单文件上传

@nestjs/platform-express 提供了 FileInterceptor() 拦截器处理单文件上传

1
2
3
4
function FileInterceptor(
fieldName: string, // 请求体的字段名
localOptions?: MulterOptions // Multer的配置项
): Type<NestInterceptor>;

Multer的配置项:

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
interface MulterOptions {
dest?: string | Function; // 文件存储目录
storage?: any; // diskStorage() 配置存储项
limits?: { // 限制
fieldNameSize?: number;
fieldSize?: number;
fields?: number;
fileSize?: number;
files?: number;
parts?: number;
headerPairs?: number;
};
preservePath?: boolean; // 保留路径
fileFilter?( // 文件过滤
req: any,
file: {
fieldname: string;
originalname: string;
encoding: string;
mimetype: string;
size: number;
destination: string;
filename: string;
path: string;
buffer: Buffer;
}, callback: (
error: Error | null,
acceptFile: boolean
) => void
): void;
}

在路由方法上应用该拦截器,再使用 @UploadedFile() 参数装饰器获取 Express.Multer.File 类型文件对象。

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
@Post('img')
@UseInterceptors(
FileInterceptor('file', {
storage: diskStorage({
destination: './public/img', // 文件保存的目录
// 自定义文件名
filename: (req, file, cb) => {
const fileParse = path.parse(file.originalname);
// 原始文件名+时间戳+文件后缀
const fileName = `${fileParse.name}_${new Date().getTime()}${fileParse.ext}`;
// 第一个参数为error,如果没有错误,则应该设置为null
return cb(null, fileName);
},
}),
}),
)
imgUpload(@UploadedFile() file: Express.Multer.File) {
return file;
// {
// "fieldname": "file",
// "originalname": "1.png",
// "encoding": "7bit",
// "mimetype": "image/png",
// "destination": "./public/img",
// "filename": "1_1706689279088.png",
// "path": "public\\img\\1_1706689279088.png",
// "size": 787441
// }
}

多文件上传-数组

使用 FilesInterceptor() 拦截器处理多文件上传,一个字段上传多个文件。

1
2
3
4
5
function FilesInterceptor(
fieldName: string, // 请求体的字段名
maxCount?: number, // 最大文件数量
localOptions?: MulterOptions // Multer的配置项
): Type<NestInterceptor>;

使用 @UploadedFiles() 参数装饰器获取 Express.Multer.File[] 类型文件对象。

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
@Post('images')
@UseInterceptors(
FilesInterceptor('files', 10, {
storage: diskStorage({
destination: './public/img', // 文件保存的目录
// 自定义文件名
filename: (req, file, cb) => {
const fileParse = path.parse(file.originalname);
// 原始文件名+时间戳+文件后缀
const fileName = `${fileParse.name}_${new Date().getTime()}${fileParse.ext}`;
// 第一个参数为error,如果没有错误,则应该设置为null
return cb(null, fileName);
},
}),
}),
)
imagesUpload(@UploadedFiles() files: Express.Multer.File[]) {
console.log(files);
// [
// {
// fieldname: 'files',
// originalname: '3.png',
// encoding: '7bit',
// mimetype: 'image/png',
// destination: './public/img',
// filename: '3_1706687883220.png',
// path: 'public\\img\\3_1706687883220.png',
// size: 19457170
// },
// {
// fieldname: 'files',
// originalname: '2.jpg',
// encoding: '7bit',
// mimetype: 'image/jpeg',
// destination: './public/img',
// filename: '2_1706687883283.jpg',
// path: 'public\\img\\2_1706687883283.jpg',
// size: 1127874
// }
// ]
return 'success';
}

MulterModule统一配置

多个路由都需要上传文件,可以将相同配置提取出来,在模块的 imports 中使用 MulterModule.register() 导入配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Module({
imports: [
MulterModule.register({
storage: diskStorage({
destination: './public/img', // 文件保存的目录
// 自定义文件名
filename: (req, file, cb) => {
const fileParse = path.parse(file.originalname);
// 原始文件名+时间戳+文件后缀
const fileName = `${fileParse.name}_${new Date().getTime()}${fileParse.ext}`;
// 第一个参数为error,如果没有错误,则应该设置为null
return cb(null, fileName);
},
}),
}),
],
controllers: [UploadController],
providers: [UploadService],
})
export class UploadModule {}
1
2
3
4
5
6
7
8
9
10
11
@Post('img')
@UseInterceptors(FileInterceptor('file'))
imgUpload(@UploadedFile() file: Express.Multer.File) {
return file;
}

@Post('images')
@UseInterceptors(FilesInterceptor('files'))
imagesUpload(@UploadedFiles() files: Express.Multer.File[]) {
return files;
}

任意字段上传

使用 AnyFilesInterceptor() 任意字段上传,请求体中任意字段上传的文件都会被处理

1
2
3
4
5
@Post('any')
@UseInterceptors(AnyFilesInterceptor())
anyUpload(@UploadedFiles() files: Express.Multer.File[]) {
return files;
}
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
[
{
"fieldname": "aa",
"originalname": "2.jpg",
"encoding": "7bit",
"mimetype": "image/jpeg",
"destination": "./public/img",
"filename": "2_1706690551808.jpg",
"path": "public\\img\\2_1706690551808.jpg",
"size": 1127874
},
{
"fieldname": "bb",
"originalname": "5.jpg",
"encoding": "7bit",
"mimetype": "image/jpeg",
"destination": "./public/img",
"filename": "5_1706690551818.jpg",
"path": "public\\img\\5_1706690551818.jpg",
"size": 1473642
},
{
"fieldname": "cc",
"originalname": "8.jpg",
"encoding": "7bit",
"mimetype": "image/jpeg",
"destination": "./public/img",
"filename": "8_1706690551825.jpg",
"path": "public\\img\\8_1706690551825.jpg",
"size": 433466
}
]

自定义字段上传

在多个字段上传单个或多个文件,使用 FileFieldsInterceptor() 拦截器,指定字段名和最大文件数量,允许携带其他参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Post('files')
@UseInterceptors(
FileFieldsInterceptor([
{ name: 'avatar', maxCount: 1 },
{ name: 'background', maxCount: 2 },
]),
)
uploadFile(
@UploadedFiles()
files: {
avatar?: Express.Multer.File[];
background?: Express.Multer.File[];
},
) {
return files;
}
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
{
"avatar": [
{
"fieldname": "avatar",
"originalname": "5.jpg",
"encoding": "7bit",
"mimetype": "image/jpeg",
"destination": "./public/img",
"filename": "5_1706691247632.jpg",
"path": "public\\img\\5_1706691247632.jpg",
"size": 1473642
}
],
"background": [
{
"fieldname": "background",
"originalname": "6596.png",
"encoding": "7bit",
"mimetype": "image/png",
"destination": "./public/img",
"filename": "6596_1706691247641.png",
"path": "public\\img\\6596_1706691247641.png",
"size": 1328729
},
{
"fieldname": "background",
"originalname": "14.jpg",
"encoding": "7bit",
"mimetype": "image/jpeg",
"destination": "./public/img",
"filename": "14_1706691247648.jpg",
"path": "public\\img\\14_1706691247648.jpg",
"size": 888767
}
]
}

存在意外字段会返回错误信息

1
2
3
4
5
{
"message": "Unexpected field",
"error": "Bad Request",
"statusCode": 400
}

没有文件

要接受multipart/form-data但不允许上传任何文件,需使用NoFilesInterceptor(),会将多部分数据设置为请求主体的属性。如果请求中包含文件,将返回BadRequestException异常

1
2
3
4
5
@Post('noFile')
@UseInterceptors(NoFilesInterceptor())
noFile(@Body() body: any) {
console.log(body);
}

静态资源目录

设置静态目录以访问上传的图片等文件

  1. NestFactory.create<T> 泛型设置为 NestExpressApplication,表示创建一个基于Express的应用
  2. app.useStaticAssets() 设置静态资源目录
1
2
3
4
5
6
7
8
9
10
11
12
13
14
useStaticAssets(path: string, options?: ServeStaticOptions): this;
interface ServeStaticOptions {
dotfiles?: string;
etag?: boolean;
extensions?: string[];
fallthrough?: boolean;
immutable?: boolean;
index?: boolean | string | string[];
lastModified?: boolean;
maxAge?: number | string;
redirect?: boolean;
setHeaders?: (res: any, path: string, stat: any) => any;
prefix?: string;
}
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
50
dotfiles(点文件):
说明: 设置在遇到点文件时如何处理。点文件是以点(".")开头的文件或目录。注意,此检查仅在路径本身进行,而不检查路径在磁盘上是否实际存在。
取值:
'allow': 对点文件没有特殊处理。
'deny': 对任何对点文件的请求返回403。
'ignore': 假装点文件不存在,然后调用next()。

etag(实体标签):
说明: 启用或禁用ETag生成,默认为true。
取值:
true: 启用ETag生成。
false: 禁用ETag生成。

extensions(文件扩展名):
说明: 设置文件扩展名的回退。当文件不存在时,将给定的扩展名添加到文件名中并搜索。将返回找到的第一个文件。例如:['html', 'htm']。
默认值: false。

fallthrough(错误是否穿透):
说明: 让客户端错误穿透为未处理的请求,否则转发客户端错误。
默认值: false。

immutable(是否启用不可变指令):
说明: 启用或禁用Cache-Control响应头中的不可变指令。如果启用,应该同时指定maxAge选项以启用缓存。
取值:
true: 启用不可变指令。

index(默认索引文件):
说明: 默认情况下,该模块将在目录请求时发送"index.html"文件。可以通过设置为false来禁用,或者提供一个字符串或字符串数组以指定新的索引文件。
默认值: true。

lastModified(最后修改时间):
说明: 启用或禁用Last-Modified头,默认为true。使用文件系统的最后修改值。
默认值: true。

maxAge(最大缓存时间):
说明: 提供http缓存的最大时间(毫秒),默认为0。也可以是ms模块接受的字符串。
默认值: 0。

redirect(重定向斜杠):
说明: 当路径名是目录时,是否重定向到带有斜杠的路径。默认为true。
默认值: true。

setHeaders(设置自定义头):
说明: 在响应上设置自定义头的函数。更改头需要同步进行。该函数调用格式为fn(res, path, stat),其中参数为:
res - 响应对象。
path - 正在发送的文件路径。
stat - 正在发送的文件的stat对象。

prefix(虚拟路径前缀):
说明: 创建虚拟路径前缀。
1
2
app.useStaticAssets(join(__dirname, '..', 'public')); // 静态资源目录
// 访问./public/img/1.png -> http://localhost:3000/img/1.png

复制assets

nest-cli.json 中配置 compilerOptions.assets,编译时将静态资源复制到输出目录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
"compilerOptions": {
"deleteOutDir": true,
"watchAssets": true, // 监听静态资源变化
"assets": [
{
"include": "../public", // 静态资源目录,相对于src目录
"outDir": "dist/public" // 输出目录,相对于根目录
},
{
"include": "../views",
"outDir": "dist/views"
}
],
}

返回/下载文件

既然上传了文件,那返回文件也是常用的

sendFile

express的sendFile封装了下面的过程,属于流式传输。

1
2
3
const writeStream = fs.createReadStream(filePath)
stream.pipe(response)
// ....略去一系列setHeader操作

会根据文件的扩展名设置Content-Type,浏览器会直接打开文件,而不是下载。

1
2
3
4
@Get('img')
fileDownload(@Query('name') name: string, @Res() res: Response) {
res.sendFile(name, { root: 'public/img' });
}

StreamableFile

手动操作res可能会让Nest丢失对响应的控制权,让拦截器等不生效

StreamableFile 是Nest流式文件的包装器,允许在响应中返回流式传输的文件。

但它并不会自动根据文件类型设置Content-Type,默认是application/octet-stream

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Get('stream')
async streamDownload(
@Query('name') name: string,
@Res({ passthrough: true }) res: Response,
) {
const fd = await fs.promises.open(`public/img/${name}`, 'r');
const readStream = fd.createReadStream();
const dynamicImport = new Function('specifier', 'return import(specifier)');
const mime = await dynamicImport('mime');
res.set({
'Content-Type': mime.default.getType(fs.stat.name),
// 让浏览器下载文件,需加上Content-Disposition
// 'Content-Disposition': `attachment; filename=${name}`,
});
return new StreamableFile(readStream);
}

使用了mime获取文件类型,但从mime@4开始,只提供了esm导出,nest默认会输出为commonjs,所以需要import()动态引入。

但转为commonjs时,import()会被tsc转为reuqire(),解决问题:Dynamic import() with “module”: “commonjs”

download

Express提供了res.download()方法,可以让浏览器下载文件。

sendFile告知浏览器它是一个流,但没让浏览器下载它。
而download在响应头里加上了 Content-Disposition: attachment; filename=”xxxx” 告知浏览器下载文件。

1
2
3
4
@Get('download')
download(@Query('name') name: string, @Res() res: Response) {
res.download(`public/img/${name}`);
}

压缩文件下载

compressing 压缩文件,StreamableFile 将压缩流返回。并设置 Content-Disposition 让浏览器下载文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
import compressing from 'compressing';
@Get('zip')
downloadZip(
@Query('name') name: string,
@Res({ passthrough: true }) res: Response,
) {
const tarStream = new compressing.zip.Stream();
tarStream.addEntry(`public/img/${name}`);
res.set({
'Content-Disposition': `attachment; filename=${path.parse(name).name}.zip`,
});
return new StreamableFile(tarStream);
}

RxJS

RxJS(Reactive Extensions for JavaScript) 是一个使用可观察(Observable)序列来编写异步和基于事件(Event-based)程序的库。即观察者模式,编写异步队列和事件处理。cn文档v5

详见:学习笔记-RxJS

Nest 内置了 RxJs,如拦截器(Interceptor)等功能都是基于 RxJs 实现的。

Interceptor拦截器

Interceptor是具有@Injectable()装饰器的类,实现了NestInterceptor接口。可以拦截请求和响应,对它们进行转换处理

拦截器是面向切面编程AOP的一种实现,具有一系列有用的功能:

  1. 函数执行之前/之后绑定额外的逻辑
  2. 转换从函数返回的结果
  3. 转换从函数抛出的异常
  4. 扩展基本函数行为
  5. 根据所选条件完全重写函数 (例如, 缓存目的)

nest g itc <name> 创建拦截器

一个最简单的拦截器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable } from 'rxjs';

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

参考:
NestJS10-Interceptors
NestJs学习之旅(9)——拦截器
深入了解Nest拦截器

NestInterceptor

NestInterceptor接口定义了一个intercept方法,接收两个参数:

  1. context:执行上下文,具有一些获取执行信息的方法。
  2. nextCallHandler类型,它的handle方法返回一个Observable
1
2
3
interface NestInterceptor<T = any, R = any> {
intercept(context: ExecutionContext, next: CallHandler<T>): Observable<R> | Promise<Observable<R>>;
}

context参数是ExecutionContext类型,继承自ArgumentsHost,具有一些获取执行信息的方法。

1
2
3
4
5
6
7
8
9
10
11
12
interface ExecutionContext extends ArgumentsHost {
getClass<T = any>(): Type<T>; // 获取控制器类
getHandler(): Function; // 获取路由处理函数
}
interface ArgumentsHost {
getArgs<T extends Array<any> = any[]>(): T; // 获取上下文参数,返回一个数组,0 - 请求对象,1 - 响应对象 2 - next函数
getArgByIndex<T = any>(index: number): T; // 通过索引获取参数
switchToRpc(): RpcArgumentsHost; // 获取RPC上下文对象
switchToHttp(): HttpArgumentsHost; // 获取Http上下文对象
switchToWs(): WsArgumentsHost; // 获取WebSocket上下文对象
getType<TContext extends string = ContextType>(): TContext; // 获取执行上下文的类型
}

next参数是CallHandler类型,是对路由处理函数的抽象,它的handle()方法就是调用路由处理函数,返回一个Observable,包装了响应流,可以用Rxjs操作符对其进行处理。

1
2
3
interface CallHandler<T = any> {
handle(): Observable<T>;
}

intercept方法返回Observable,即拦截器的主要逻辑是对next.handle()返回的Observable进行处理,返回一个新的Observable,而无需订阅它。

绑定拦截器

拦截器可以装饰控制器、路由方法,或是全局的。

@UseInterceptors() 在控制器或路由方法上使用拦截器,传入一个或多个拦截器。

在控制器上使用拦截器
1
2
3
4
import { ConverterInterceptor } from './converter/converter.interceptor';
@Controller()
@UseInterceptors(ConverterInterceptor)
export class AppController {}
在路由方法上使用拦截器
1
2
3
4
import { ConverterInterceptor } from './converter/converter.interceptor';
@Get('value')
@UseInterceptors(ConverterInterceptor)
getValue(){ return 'value'; }

app.useGlobalInterceptors() 配置全局拦截器,传入一个或多个拦截器,需要手动实例化。

全局拦截器
1
2
import { ConverterInterceptor } from './converter/converter.interceptor';
app.useGlobalInterceptors(new ConverterInterceptor());

格式化响应

使用Rxjs的操作符可以很方便地拦截加工响应流,实现格式化响应。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Injectable()
export class ConverterInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map((data) => ({
data,
code: 200,
success: true,
message: '请求成功',
})),
);
}
}

记录处理时间

记录请求的处理时间,对于性能监控很有用。

1
2
3
4
5
6
7
8
9
@Injectable()
export class ConverterInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const now = Date.now();
return next.handle().pipe(
tap(() => console.log(`After... ${Date.now() - now}ms`)), // After... 7ms
);
}
}

超时与错误处理

可以设置请求的超时,并对超时等错误进行处理。

1
2
3
4
5
6
7
8
9
@Injectable()
export class ConverterInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
timeout(5000), // 设置超时
catchError((err) => of('error')), // 捕获错误
);
}
}

获取请求、响应对象

可以通过ExecutionContext获取请求、响应对象,对它们进行处理。这都是在路由处理函数之前的操作。

尽管能拿到请求、响应对象,但不推荐在拦截器中进行鉴权,那是守卫的职责。拦截器的职责是对请求和响应进行处理。比如实现复杂的缓存拦截器,但实际上,nest已经提供了缓存API。

1
2
3
4
5
6
7
8
9
10
11
12
@Injectable()
export class ConverterInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const req = context.switchToHttp().getRequest<Request>();
const res = context.switchToHttp().getResponse<Response>();
console.log(req.url); // /value
res.set({
name: 'qx',
});
return next.handle();
}
}

请求链路日志记录

记录请求链路日志,对于排查问题很有用。

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
import { Logger } from '@nestjs/common';
import { format } from 'util';

@Injectable()
export class ConverterInterceptor implements NestInterceptor {
private readonly logger = new Logger(); // 实例化日志记录器
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const start = Date.now(); // 请求开始时间
return next.handle().pipe(
tap((response) => {
const request = context.switchToHttp().getRequest<Request>();
// 打印请求方法,请求链接,处理时间和响应数据
this.logger.log(
format(
'%s %s %dms %s',
request.method,
request.url,
Date.now() - start,
JSON.stringify(response),
),
);
}),
);
}
}
结果
1
[Nest] 19404  - 2024/02/06 23:53:01     LOG GET /value 7ms [1,2,3]

执行上下文

在上面的拦截器中出现了执行上下文,这里详细介绍下。

Nest实质是一个IoC容器,本身不具备什么功能。是存粹的以一种程序架构理念实现的框架。

服务低层,可以是Express或者Fastify的HTTP服务,也可以是微服务或者是WebSocket应用。无论Nest高层逻辑是什么样的,都与低层松耦合。当需要调取低层的某些功能时,就需要通过执行上下文(文档)来获取相关对象和信息。

例如守卫过滤器拦截器,都需要对整个请求-响应过程做出一定的干预和操作,最基本的就需要通过执行上下文来获取请求和响应对象。

ArgumentsHost 类提供了一些方法,用于获取执行上下文的信息。

1
2
3
4
5
6
7
8
interface ArgumentsHost {
getArgs<T extends Array<any> = any[]>(): T; // 获取上下文参数,返回一个数组,0 - 请求对象,1 - 响应对象 2 - next函数
getArgByIndex<T = any>(index: number): T; // 通过索引获取参数
switchToRpc(): RpcArgumentsHost; // 获取RPC上下文对象
switchToHttp(): HttpArgumentsHost; // 获取Http上下文对象
switchToWs(): WsArgumentsHost; // 获取WebSocket上下文对象
getType<TContext extends string = ContextType>(): TContext; // 获取执行上下文的类型
}

ExecutionContext 继承自 ArgumentsHost,并提供了一些额外的方法,主要用于守卫和拦截器。

1
2
3
4
interface ExecutionContext extends ArgumentsHost {
getClass<T = any>(): Type<T>; // 获取控制器类
getHandler(): Function; // 获取路由处理函数
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
export type ContextType = 'http' | 'ws' | 'rpc'; // 执行上下文类型
export interface HttpArgumentsHost {
getRequest<T = any>(): T; // 获取请求对象
getResponse<T = any>(): T; // 获取响应对象
getNext<T = any>(): T; // nest函数
}
export interface WsArgumentsHost {
getData<T = any>(): T; // 获取数据
getClient<T = any>(): T; // 获取客户端
}
export interface RpcArgumentsHost {
getData<T = any>(): T; // 获取数据
getContext<T = any>(): T; // 获取上下文
}

执行上下文分为三种类型:httpwsrpc,分别对应HTTP、WebSocket和RPC(微服务)。

通过 getType() 获取当前执行上下文的类型,再使用 switchTo*() 获取对应的执行上下文对象。

1
2
3
4
5
6
7
if (host.getType() === 'http') {
// HTTP服务
} else if (host.getType() === 'rpc') {
// rpc(微服务)
} else if (host.getType() === 'ws') {
// WebSocket
}

异常处理层

Nest内置异常处理层,应用程序没有处理的异常的会被这个层捕获,并且自动的返回一个对用户友好的响应。

异常处理层由 Exception filters 异常过滤器实现,该过滤器处理HttpException类型的异常(及其子类),当有异常被忽略(既不是HttpException也不是继承自HttpException的类),这个内置的异常过滤器会返回默认的JSON

1
2
3
4
{
"statusCode": 500,
"message": "Internal server error"
}

抛出标准异常

内置 HttpException 类,用于抛出标准的HTTP异常,并发送标准HTTP响应对象。

1
2
3
4
5
6
7
8
class HttpException extends Error {
constructor(response: string | Record<string, any>, status: number, options?: HttpExceptionOptions);
cause: unknown;
}
interface HttpExceptionOptions {
cause?: unknown;
description?: string;
}

构造函数接收三个参数

  1. response:定义响应体JSON内容,可以是字符串或对象。
  2. status:Http状态码,可以传入数字,或HttpStatus枚举
  3. options:可选参数,包括causedescription属性,用于记录异常的原因和描述,不会添加到响应体中。

默认的情况下,JSON内容里面有2个属性

  1. statusCode:默认为status参数中提供的Http状态码
  2. message:Http错误的简短描述,即response参数传入的字符串

要覆盖JSON响应体,需给response参数传入对象。Nest将序列化该对象,并将其作为JSON响应体返回。

抛出HttpException标准错误
1
2
3
4
5
6
7
8
@Get('value')
getValue() {
throw new HttpException('Forbidden', HttpStatus.FORBIDDEN, {
cause: new Error('Error'),
description: 'Forbidden',
});
return "123";
}
响应
1
2
3
4
{
"statusCode": 403,
"message": "Forbidden"
}

注意:路由处理函数中抛出的异常,可以被拦截器catchError操作符捕获,并处理,不会被异常处理层捕获。

内置 HTTP 异常

Nest提供了许多继承自HttpException的异常类

如果这些异常不满足需求,可以自定义异常,创建一个继承自HttpException的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
BadRequestException // 400 请求错误
UnauthorizedException // 401 未授权
NotFoundException // 404 未找到
ForbiddenException // 403 禁止访问
NotAcceptableException // 406 不可接受
RequestTimeoutException // 408 请求超时
ConflictException // 409 冲突
GoneException // 410 消失
HttpVersionNotSupportedException // 505 HTTP版本不支持
PayloadTooLargeException // 413 负载过大
UnsupportedMediaTypeException // 415 不支持的媒体类型
UnprocessableEntityException // 422 无法处理的实体
InternalServerErrorException // 500 服务器内部错误
NotImplementedException // 501 未实现
ImATeapotException // 418 不希望处理的请求,超文本咖啡壶控制协议HTCPCP,一个愚人节笑话
MethodNotAllowedException // 405 方法不允许
BadGatewayException // 502 错误的网关
ServiceUnavailableException // 503 服务不可用
GatewayTimeoutException // 504 网关超时
PreconditionFailedException // 412 前提条件失败
ForbiddenException
1
2
3
4
5
6
7
class ForbiddenException extends HttpException {
constructor(objectOrError?: string | object | any, descriptionOrOptions?: string | HttpExceptionOptions);
}
interface HttpExceptionOptions {
cause?: unknown;
description?: string;
}

这些内置异常类的构造函数接收两个参数

  1. objectOrError:定义响应体JSON内容,可以是字符串或对象。和HttpExceptionresponse参数差不多。
  2. descriptionOrOptions:可选参数,包括causedescription属性,用于记录异常的原因和描述,其描述添加到响应体的error字段上。
  3. 第二个参数也可以是一个字符串,直接表示description
1
2
3
4
5
6
7
8
@Get('value')
getValue() {
throw new ForbiddenException('Forbidden', {
cause: new Error('Error'),
description: '禁止访问',
});
return "123";
}
1
2
3
4
5
{
"message": "Forbidden",
"error": "禁止访问",
"statusCode": 403
}

异常过滤器

内置的标准异常类已经能够应付大多数异常情况,但也有完全控制异常层的需求,例如在出现异常时记录日志、响应不同的JSON内容等。Exception filters异常过滤器可以满足需求。

Exception filters是具有@Catch()装饰器的类,实现了ExceptionFilter接口。它可以捕获指定类型任何类型异常,不仅限于HttpException

@Catch()不传入参数,表示捕获任何类型的异常。还可以传入若干异常类,表示捕获这些指定类型的异常。

创建异常过滤器:nest g f <name>

一个最简单的异常过滤器
1
2
3
4
5
import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common';
@Catch()
export class ErrorFilter<T> implements ExceptionFilter {
catch(exception: T, host: ArgumentsHost) {}
}

ExceptionFilter接口实现了catch方法,接收两个参数:

  1. exception:捕获到的异常
  2. hostArgumentsHost类型,具有一些获取执行信息的方法。
1
2
3
4
5
6
7
8
interface ArgumentsHost {
getArgs<T extends Array<any> = any[]>(): T; // 获取路由处理函数的参数
getArgByIndex<T = any>(index: number): T; // 通过索引获取参数
switchToRpc(): RpcArgumentsHost; // 切换至RPC
switchToHttp(): HttpArgumentsHost; // 切换至Http
switchToWs(): WsArgumentsHost; // 切换至WebSocket
getType<TContext extends string = ContextType>(): TContext; // 'http' | 'ws' | 'rpc'
}

注意:一但绑定了异常过滤器,就会覆盖内置的异常过滤器,需要手动处理异常、返回响应。

绑定过滤器

过滤器可以装饰控制器、路由方法,或是全局的。

@UseFilters() 在控制器或路由方法上使用过滤器,传入一个或多个过滤器。

在控制器上使用过滤器
1
2
3
4
import { ErrorFilter } from './error/error.filter';
@Controller()
@UseFilters(ErrorFilter)
export class AppController {}
在路由方法上使用过滤器
1
2
3
4
import { ErrorFilter } from './error/error.filter';
@Get('value')
@UseFilters(ErrorFilter)
getValue() {}

app.useGlobalFilters() 配置全局过滤器,传入一个或多个过滤器,需要手动实例化。

全局过滤器
1
2
import { ErrorFilter } from './error/error.filter';
app.useGlobalFilters(new ErrorFilter());

简单的异常处理

绑定了异常过滤器后,就意味着需要手动处理异常,并返回响应。

一个最简单的,包含日志记录的异常过滤器。

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
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
Logger,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { format } from 'util';

@Catch(HttpException)
export class ErrorFilter<T extends HttpException> implements ExceptionFilter {
private readonly logger = new Logger(); // 实例化日志记录器
catch(exception: T, host: ArgumentsHost) {
// 获取请求和响应对象
const ctx = host.switchToHttp();
const req = ctx.getRequest<Request>();
const res = ctx.getResponse<Response>();
// 输出日志
this.logger.error(
format('%s %s %s', req.method, req.url, exception.getResponse()),
);
// 响应
res.status(exception.getStatus()).json(exception.getResponse());
// res.status(exception.getStatus()).json({
// statusCode: exception.getStatus(),
// message: exception.message,
// });
}
}
1
[Nest] 30556  - 2024/02/07 17:03:49   ERROR GET /value { message: 'Forbidden', error: '禁止访问', statusCode: 403 }

pipe管道

pipe管道是用 @Injectable() 注释的类,实现了 PipeTransform 接口,具有 transform() 方法。

nest g pi <name> 创建管道

颗粒度:参数、方法、控制器和全局

作用:

  1. 转换:将输入数据转换为所需的形式,无法转换则抛出异常
  2. 验证:检查输入数据是否符合要求,否则抛出异常

过程:在控制器方法上插入管道,管道接收该方法的参数,并进行转换或验证,最后用转换后参数调用该方法(路由处理程序)。

注意:当无法转换或验证不通过时,管道会抛出异常,会被异常处理层捕获,之后将不会执行任何控制器方法。默认抛出 BadRequestException(400) 异常。

内置管道

Nest提供了一些内置的管道,用于转换和验证。

  1. ValidationPipe 验证管道,基于 class-validatorclass-transformer
  2. ParseIntPipe 将输入转换为整数
  3. ParseFloatPipe 将输入转换为浮点数
  4. ParseBoolPipe 将输入转换为布尔值
  5. ParseArrayPipe 将输入转换为数组
  6. ParseUUIDPipe 将输入转换为UUID
  7. ParseEnumPipe 将输入转换为枚举
  8. DefaultValuePipe 设置默认值,当参数不存在时使用
  9. ParseFilePipe 将上传的文件转换为文件对象

绑定转换管道

转换管道如内置的 Parse*Pipe,作用于控制器方法的参数,将管道类传入参数装饰器。

1
2
3
4
function Param(
property: string,
...pipes: (Type<PipeTransform> | PipeTransform)[]
): ParameterDecorator;

使用 ParseIntPipe 将参数转换为整数。

1
2
3
4
5
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
console.log(typeof id, id); // number 123456
return this.loginService.findOne(id);
}

无法转换时,抛出 BadRequestException(400) 异常。

1
2
3
4
5
{
"message": "Validation failed (numeric string is expected)",
"error": "Bad Request",
"statusCode": 400
}

可以手动实例化管道类,传入选项,自定义管道行为。

1
2
3
4
5
6
7
8
interface ParseIntPipeOptions {
errorHttpStatusCode?: ErrorHttpStatusCode; // 抛出异常的HTTP状态码
exceptionFactory?: (error: string) => any;
optional?: boolean;
}
class ParseIntPipe implements PipeTransform<string> {
constructor(options?: ParseIntPipeOptions);
}
1
2
3
4
5
6
@Param(
'id',
new ParseIntPipe({
errorHttpStatusCode: HttpStatus.NOT_FOUND,
}),
)

自定义管道

管道实现了 PipeTransform 接口,具有 transform() 方法。

一个最简单的管道
1
2
3
4
5
6
7
8
9
10
11
import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common';

@Injectable()
export class LoginPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
console.log(value);// 123456
console.log(metadata);
// { metatype: [Function: Number], type: 'param', data: 'id' }
return value;
}
}

transform() 方法接收两个参数:

  1. value:参数的值
  2. metadata:参数的元数据,包括typemetatypedata

其返回值将作为实际的值传入控制器方法的对应参数。

1
2
3
4
5
6
interface ArgumentMetadata {
readonly type: Paramtype; // 指示参数是body, query, param还是自定义参数
readonly metatype?: Type<any> | undefined; // 参数的元类型
readonly data?: string | undefined; // 传递给装饰器的字符串
}
type Paramtype = 'body' | 'query' | 'param' | 'custom';

实现 ParseIntPipe 的简单版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Injectable()
export class LoginPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
const isNumeric =
['string', 'number'].includes(typeof value) &&
!isNaN(parseFloat(value)) &&
isFinite(value as any);
if (!isNumeric) {
throw new BadRequestException(
'Validation failed (numeric string is expected)',
);
}
return parseInt(value, 10);
}
}

验证

DTO(Data Transfer Object) 数据传输对象,是一种以类的形式实际为数据的对象,用于前后端或程序间传输数据。

src\login\dto\create-login.dto.ts
1
2
3
4
export class CreateLoginDto {
username: string;
password: string;
}
src\login\login.controller.ts
1
2
3
4
5
import { CreateLoginDto } from './dto/create-login.dto';
@Post()
create(@Body() createLoginDto: CreateLoginDto) {
return this.loginService.create(createLoginDto);
}

这样对于数据结构的限制显然不够,且还需要在控制器中写验证逻辑。

类验证器

类验证器 class-validatorclass-transformer两个库,用于验证和转换类的实例。

class-validator 提供了一些验证装饰器,用于验证数据。

1
2
3
4
5
6
7
8
9
10
11
12
import { IsString, IsNotEmpty, Length } from 'class-validator';
export class CreateLoginDto {
@IsNotEmpty() // 验证是否为空
@IsString() // 验证是否为字符串
@Length(3, 10, {
message: '用户名长度必须为3到10位', // 自定义错误信息
}) // 验证字符串长度
username: string;
@IsNotEmpty()
@IsString()
password: string;
}

Nest内置的 ValidationPipe 验证管道,也是基于上面两个库实现的。

1
2
3
4
5
@Post()
create(@Body(ValidationPipe) createLoginDto: CreateLoginDto) {
console.log(createLoginDto);
return this.loginService.create(createLoginDto);
}

当传入的数据不符合规则时,会抛出 BadRequestException(400) 异常。

1
2
3
4
5
6
7
{
"message": [
"用户名长度必须为3到10位"
],
"error": "Bad Request",
"statusCode": 400
}

自定义类验证管道

Nest提供了 ValidationPipe 验证管道,但也可以自定义验证管道,实现更复杂的验证逻辑。

ValidationPipe 也是通过 class-validatorclass-transformer 实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { validate } from 'class-validator';
import { plainToInstance } from 'class-transformer';
@Injectable()
export class LoginPipe implements PipeTransform {
async transform(value: any, metadata: ArgumentMetadata) {
// 通过plainToInstance实例化DTO类,并将value反射到DTO类上
const DTO = plainToInstance(metadata.metatype, value);
// 使用validate验证DTO类,返回错误对象数组
const errors = await validate(DTO);
// 如果errors数组不为空,抛出异常
if (errors.length) {
throw new BadRequestException(errors, 'Validation failed');
}
return value;
}
}

通过 plainToInstance() 实例化DTO类(转换参数对象为有类型的对象),并将 value 反射到DTO类上,再使用 validate() 验证DTO对象,返回错误类型数组,如果数组不为空,抛出异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"message": [
{
"target": {
"username": "12",
"password": "123"
},
"value": "12",
"property": "username",
"children": [],
"constraints": {
"isLength": "用户名长度必须为3到10位"
}
}
],
"error": "Validation failed",
"statusCode": 400
}

自定义验证函数

registerDecorator() 用于注册自定义的验证装饰器,实现更复杂的验证逻辑。

实现 RequireOtherFields() 验证指定属性与另一属性的关联性。

例如 page 依赖于 size 实现分页,所以传入了 page 也必须同时传入 size

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
import {
ValidationOptions,
registerDecorator,
ValidationArguments,
} from 'class-validator';

function RequireOtherFields(
field: string, // 依赖的属性
validationOptions?: ValidationOptions, // 验证选项
) {
return function (object: object, propertyName: string) {
registerDecorator({
name: 'requireOtherFields', // 自定义装饰器的名称
target: object.constructor, // 装饰的目标
propertyName: propertyName, // 装饰的属性
constraints: [field], // 传递给验证函数的附加约束
options: validationOptions, // 验证选项
validator: {
// 自定义验证函数
validate(value: any, args: ValidationArguments) {
const relatedValue = args.object[field]; // 获取依赖的属性的值
return (
// 验证逻辑,当目标属性的值存在时,依赖的属性的值也必须存在
(value !== undefined && relatedValue !== undefined) ||
value === undefined
);
},
// 自定义验证失败时的默认消息
defaultMessage(args: ValidationArguments) {
return `传入 ${args.property} 后也必须传入 ${args.constraints[0]}`;
},
},
});
};
}
使用
1
2
3
4
5
export class FindAllDto {
@RequireOtherFields('size') // 当传入了page时,必须同时传入size
page?: number;
size?: number;
}

模式验证

有许多库实现了以模式来描述数据,如JoiZod等,并提供了验证数据的方法。下面以zod为例。

创建模式与DTO,与类验证器类似,装饰器与函数式的区别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { z } from 'zod';
export const createLoginSchema = z
.object({
username: z
.string()
.min(3, {
message: '用户名长度至少为3位',
})
.max(10, {
message: '用户名长度最多为10位',
}),
password: z.string(),
})
.required();

export type CreateLoginDto = z.infer<typeof createLoginSchema>;

创建模式验证管道

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { ZodSchema } from 'zod';

@Injectable()
export class LoginPipe implements PipeTransform {
constructor(private schema: ZodSchema) {}
async transform(value: any, metadata: ArgumentMetadata) {
try {
const parsedValue = this.schema.parse(value);
// 若验证失败则抛出ZodError错误
return parsedValue;
} catch (error) {
throw new BadRequestException(error, 'Validation failed');
}
}
}

绑定验证管道

1
2
3
4
5
6
7
8
9
import { createLoginSchema, CreateLoginDto } from './dto/create-login.dto';
import { LoginPipe } from './login.pipe';

@Post()
@UsePipes(new LoginPipe(createLoginSchema))
create(@Body() createLoginDto: CreateLoginDto) {
console.log(createLoginDto);
return this.loginService.create(createLoginDto);
}

验证失败返回的json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"issues": [
{
"code": "too_small",
"minimum": 3,
"type": "string",
"inclusive": true,
"exact": false,
"message": "用户名长度至少为3位",
"path": [
"username"
]
}
],
"name": "ZodError"
}

全局管道

通过 ValidationPipe 验证数据是非常常见的,可以将其设置为全局验证管道,不必在每个控制器上单独使用。

1
2
3
4
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
app.useGlobalPipes(new ValidationPipe());
}

如果DTO的某些属性是可选的,应该启用 skipUndefinedProperties,以跳过对未定义属性的验证。

1
2
3
new ValidationPipe({
skipUndefinedProperties: true,
}),

多管道

同一层级的管道可以有多个,按顺序从左向右执行。

前一个管道的输出作为后一个管道的输入,最后的输出作为控制器方法对应参数的值。

使用 DefaultValuePipe 设置默认值,以防止接收到非预期的null或undefined。

1
2
3
4
5
@Get()
findAll(@Query('page', new DefaultValuePipe(0), ParseIntPipe) page: number) {
console.log(page); // 1
return this.loginService.findAll();
}

Guards守卫

Guards守卫是用 @Injectable() 注释的类,实现了 CanActivate 接口,具有 canActivate() 方法。

nest g gu <name> 创建守卫

颗粒度:方法、控制器和全局

传统的web应用,检测用户登陆、鉴权等,通常在控制层或中间件中处理,但中间件的职责是不明确的,且获取不到执行上下文,不知道调用next后哪个路由将被执行。

作用:守卫专门用于鉴权、角色、访问控制等操作,可以获取执行上下文,决定是否放行请求(执行路由)。

顺序:守卫的调用在中间件之后,在任何管道和拦截器之前。

CanActivate

CanActivate 接口定义了 canActivate() 方法,该方法接收 ExecutionContext 执行上下文参数,返回boolean,表示是否放行请求(执行路由)。

当返回false时,请求将被阻止,并抛出ForbiddenException,返回403。

1
2
3
export interface CanActivate {
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean>;
}

一个最简单的守卫

1
2
3
4
5
6
7
8
9
10
11
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';

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

由于单一职责的关系,路由守卫只能返回boolean来决定是否放行当前请求。虽然可以获取请求、响应对象,但不建议在守卫中修改它们,出现问题较难排查。对于一些必要的附加数据,可以在中间件中完成。

使用守卫

通过 @UseGuards() 注释路由方法或控制器,绑定若干个守卫。

1
2
3
4
5
6
import { RoleGuard } from './role/role.guard';
@Get('testRole')
@UseGuards(RoleGuard)
getTestRole() {
return 'testRole';
}

app.useGlobalGuards() 配置全局守卫,传入若干个守卫,需要手动实例化。

main.ts
1
2
import { RoleGuard } from './role/role.guard';
app.useGlobalGuards(new RoleGuard());

当存在多个守卫时,按顺序执行,任何一个守卫返回false,都会阻止请求。

元数据与角色控制RBAC

守卫通过路由方法上的元数据来实现基于角色的访问控制(RBAC)。

流程:前端回传token等信息,后端解析出token中的角色身份,对比路由方法上的元数据(允许的角色),决定是否放行请求。

@SetMetadata() 设置元数据

1
const SetMetadata: <K = string, V = any>(metadataKey: K, metadataValue: V) => CustomDecorator<K>;
1
2
3
4
5
6
7
@Get('testRole')
@UseGuards(RoleGuard)
// 设置元数据,该路由方法只允许admin角色访问
@SetMetadata<string, string[]>('roles', ['admin'])
getTestRole() {
return 'testRole';
}

Reflector 反射器,用于获取元数据
Reflector.get() 传入装饰器引用或元数据key元数据目标上下文(装饰器目标)获取元数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { Reflector } from '@nestjs/core';
@Injectable()
export class RoleGuard implements CanActivate {
constructor(private reflector: Reflector) {} // 注入反射器
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
// 获取当前路由的角色
const roles = this.reflector.get<string[]>('roles', context.getHandler());
if (!roles || !roles.length) {
return true;
}
// 通过query模拟获取用户角色,实际应该从token等方式获取
const request = context.switchToHttp().getRequest<Request>();
return roles.includes(request.query.user as string);
}
}

自定义元数据装饰器

为了更加方便,可以自定义一个装饰器,用于设置元数据。

1
2
3
4
5
6
7
8
9
10
function Roles<T = string>(...roles: T[]) {
return SetMetadata<string, T[]>('roles', roles);
}
@Get('testRole')
@UseGuards(RoleGuard)
// @SetMetadata<string, string[]>('roles', ['admin'])
@Roles('admin')
getTestRole() {
return 'testRole';
}

nest g d <name> 创建装饰器,快速生成一个设置元数据的装饰器。本质是对原有的装饰器进行封装。

role.decorator.ts
1
2
import { SetMetadata } from '@nestjs/common';
export const Role = (...args: string[]) => SetMetadata('role', args);

最佳实践:将key值定义为常量并导出,以便在其他地方使用。

1
2
3
4
5
6
import { SetMetadata } from '@nestjs/common';
export const RolesKey = 'roles';
export const Roles = (...args: string[]) => SetMetadata(RolesKey, args);
// 获取元数据
import { RolesKey } from './role.decorator';
const roles = this.reflector.get(RolesKey, context.getHandler());

SetMetadata 是低级的元数据设置方法,还可以使用 Reflector.createDecorator() 创建一个元数据装饰器。通过它创建的装饰器,可以作为获取元数据的key(装饰器的引用)。

1
2
3
4
5
6
7
import { Reflector } from '@nestjs/core';
export const Roles = Reflector.createDecorator<string[]>();
// 使用装饰器,这样创建的装饰器仅接收一个参数,一般传入数组
@Roles(['admin'])
// 获取元数据
import { Roles } from './role.decorator';
const roles = this.reflector.get(Roles, context.getHandler());

多层次元数据

多层次设置元数据
1
2
3
4
5
6
7
8
9
10
@Roles('user')
@Controller()
export class AppController {
@Get('testRole')
@UseGuards(RoleGuard)
@Roles('admin')
getTestRole() {
return 'testRole';
}
}

由于可以在多个层次上设置元数据(控制器、方法),可能需要从多个上下文中提取元数据。

  1. Reflector.getAll() 获取所有上下文中的元数据,返回二维数组,如 [ [ 'admin' ], [ 'user' ] ]
  2. Reflector.getAllAndMerge() 获取所有上下文中的元数据,并合并,如 [ 'admin', 'user' ]
  3. Reflector.getAllAndOverride() 获取上下文数组中首个的元数据,如 [ 'admin' ]
1
2
3
4
5
6
const contexts = [context.getHandler(), context.getClass()];
console.log(
this.reflector.getAll(RolesKey, contexts), // [ [ 'admin' ], [ 'user' ] ]
this.reflector.getAllAndMerge(RolesKey, contexts), // [ 'admin', 'user' ]
this.reflector.getAllAndOverride(RolesKey, contexts), // [ 'admin' ]
);

自定义装饰器

文档。在守卫中,就自定义了一个装饰器,用于设置元数据。

nest g d <name> 创建装饰器。

组合装饰器

applyDecorators() 用于组合装饰器,传入若干个装饰器,返回组合后的新装饰器。以减少控制器代码中重复的装饰器组合。

1
2
3
4
5
6
7
import { applyDecorators } from '@nestjs/common';
export function Auth(...roles: string[]) {
return applyDecorators(
SetMetadata('roles', roles),
UseGuards(RoleGuard),
);
}

自定义参数装饰器

在Node中,将自定义属性附加到请求对象是常见的做法,在路由方法中获取请求对象,是麻烦且非必要的,可以通过 createParamDecorator() 自定义参数装饰器,快速获取自定义属性。

1
2
3
4
5
6
7
8
import { ExecutionContext, createParamDecorator } from '@nestjs/common';
export const ReqUser = createParamDecorator(
(data: string, ctx: ExecutionContext) => {
// data是通过装饰器传入的参数,这里用于获取req.user上的属性,ctx是执行上下文。
const request = ctx.switchToHttp().getRequest();
return data ? request.user?.[data] : request.user;
},
);

自定义的装饰器也可以与管道一起使用,但必须将 validateCustomDecorators 设为 true,ValidationPipe默认不验证用自定义装饰器注释的参数。

1
@User(new ValidationPipe({ validateCustomDecorators: true }))