前三篇系列文章:
本文主要介绍 Pipe 的使用。
概念
Pipe 是管道的意思,在 Nestjs 中它可以在处理请求之前对数据进行预处理,从而简化控制器和业务逻辑层的代码。所以如果有需要对数据处理的任务,你应该优先用管道,它主要有两种数据处理场景:
- 转换数据:将输入数据转换为需要的数据
- 校验数据:校验成功则继续传递,否则就抛出异常
借助官方图片理解:
管道会先解析请求中的参数,再将数据传递给相应的路由控制器方法。
创建
在 Nestjs 项目中创建一个 Pipe:
nest g pipe parse --no-spec --flat
会在 src 目录下生成一个 parse.pipe.ts 文件,文件内容:
// parse.pipe.ts
import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common';
@Injectable()
export class ParsePipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
return value;
}
}
自定义管道需要实现 PipeTransform 接口,接口中有一个方法 transform,方法中有两个参数,参数 value 就是传入的要处理的数据,而 metadata 是来源数据的元信息。
现在给这个管道加上处理逻辑,我们希望它可以将传入数据转换成整数,如果不能转换就抛出异常,完整代码:
// parse.pipe.ts
import {
ArgumentMetadata,
BadRequestException,
Injectable,
PipeTransform,
} from '@nestjs/common';
@Injectable()
export class ParsePipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
// 标记执行到达
console.log('执行 ParsePipe');
// 尝试转换成整数
const val = parseInt(value, 10);
// 不能转换为整数则抛出异常
if (isNaN(val)) {
throw new BadRequestException('ParseInt failed');
}
// 将转换好的数据传出去
return val;
}
}
代码逻辑很好理解,关键是要将转换好的数据传递传递出去。接下来介绍怎么使用 Pipe。
使用
用在路由处理器方法上
管道通常是使用在某个路由处理器方法上的,以处理这个路由传入的参数,比如需要转换用户 id,就可以这样用:
当请求 /user/1 这个路径时,就会拿到值为 1 的 id,转换结果:
可以看到 ParsePipe 成功转换了数据。也可以直接将管道类直接传入到 @Param 的第二个参数来使用:
这样写也能生效,同理也支持 @Query、@Body、@Header等内置的装饰器。
需要注意你接收到的参数类型,如果用 @Body 接收参数,得到的会是一个对象,直接用 ParsePipe 转换肯定永远无法成功的,此时可以只对某个字段进行转换。例如有一个 post 请求,会传入这样的一个 json 数据:
如果直接验证整个传入的数据:
因为对象无法转换为数字,所以报错:
此时我们可以只转换某一个字段,例如要将 code 字段转换为整数,可以这样写:
当然也可以这样:
得到结果:
这个主要涉及 Nestjs 中装饰器的基础用法,注意下结合管道怎么使用就行。
用在 Controller 上
管道可以应用到 Controller 上,这样它下面所有的路由方法的参数都会被管道接收到,去掉所有单个路由方法的管道,写到 Controller 上:
此时这个 Controller 下的两个路由方法都应用到了 ParsePipe,结果:
全局使用
跟之前介绍的 Guard 和 Interceptor 一样,管道也有两种全局使用方法,第一种是在 main.ts 中注册实例:
第二种就是在 AppModule 中声明:
关于这两种方式的区别,请参考 # Nestjs AOP 编程之 Guard 的使用 中的全局使用方式。
多个管道的使用
多个管道会按照传入的顺序依次执行,且数据可以被传递。在添加一个管道 AddPipe:
nest g pipe add --no-spec --flat
这个管道的作用就是对传入的数据进行 +1 操作,可以这样写:
// add.pipe.ts
import {
ArgumentMetadata,
BadRequestException,
Injectable,
PipeTransform,
} from '@nestjs/common';
@Injectable()
export class AddPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
let newVal = value;
// 如果是数字且不是 NaN,则加 1;否则抛出异常
if (typeof value === 'number' && !isNaN(value)) {
newVal++;
} else {
throw new BadRequestException('Add failed');
}
return newVal;
}
}
结合上面的管道 ParsePipe,我们可以实现先将传入的数据转为数字,然后对数字加一,使用时只要依次传入即可:
请求/user/1,效果:
可见两个管道依次生效了。如果将两个管道传入顺序反过来,那么 AddPipe 就会先报错而无法继续执行:
全局使用多管道,也是依次传入:
参数 metadata
管道方法 transform 的第二个参数 metadata 是一个对象,包含了关于正在处理的参数的一些元信息,有三个属性:
- type:表示参数的类型,可能是
'param'、'query'、'body'、'header'等,具体取决于参数来源。 - metatype:表示参数的类型注解,如
String、Number或者自定义类型。 - data:表示参数名称,例如路径参数中的
:id,其data属性就是'id'。
在管道 ParsePipe 中打印一下 metadata,然后请求 /user/1,可以看到结果:
内置管道
Nestjs 内置了一些管道,提供了开箱即用的功能,主要有:
- ValidationPipe:用于验证数据。
- ParseIntPipe:用于将请求参数转换为整数(文中我们自己也简单的实现了下这个功能)。
- ParseBoolPipe:用于将请求参数转换为布尔值。
- ParseArrayPipe:用于将请求参数或查询参数转换为数组。
- ParseUUIDPipe:用于将请求参数转换为 UUID。
- DefaultValuePipe:用于为请求参数提供默认值,如果该参数不存在,它会返回提供的默认值。
- ParseFloatPipe:用于将请求参数转换为浮点数。
- ParseEnumPipe:用于将请求参数转换为枚举值。
- ParseFilePipe:用于验证和处理上传的文件。
篇幅有限,这里就不展开介绍每个管道的具体使用了,相信看到上面的简介都知道是干嘛的。
自定义管道传入参数
上面在介绍管道使用时,除了在 main.ts 中使用 app.useGlobalPipes() 方式传入的是类的实例,其余地方一直使用的是类,但其实是可以传入实例的:
这样写效果是一样的,并且可以传入自定义参数来实现更多的功能。
比如上面的自定义管道 AddPipe,我们希望可以控制每次累加的数值,改造下管道:
然后在使用时,实例化并传入参数即可:
此时管道 AddPipe 就会对传入的值进行 +2。
其实在使用内置的管道时,通常也会传入各种参数来控制效果。
总结
本文主要介绍了 Nestjs 中管道的定义与使用。我们实现了两个自定义管道,并介绍了它们的几种使用方式:
- 在
Controller上使用,对其下所有路由处理器方法生效; - 在单个路由处理器方法上使用,只对当前方法生效;
- 全局使用,对所有路由处理器方法生效,并介绍了两种全局使用的方式。
也介绍了多个管道一起使用的场景,它们会按照传入的顺序执行,并且能传递数据。
然后介绍了管道方法中的 metadata 参数,它包含了正在处理的参数的一些元信息,有 type、metatype 和 data 三个属性。
最后介绍了 Nestjs 中的内置管道以及自定义管道传参,在管道使用时,可以传入管道实例,并且能够传入自定义参数来实现不能效果。