Pipe 是在参数传给 handler 之前对参数做一些验证和转换的 class。
nestjs 内置的 Pipe 有这些:
- ValidationPipe
- ParseIntPipe
- ParseBoolPipe
- ParseArrayPipe
- ParseUUIDPipe
- DefaultValuePipe
- ParseEnumPipe
- ParseFloatPipe
- ParseFilePipe
分别来试下内置的 Pipe 的功能。
ParseIntPipe
参数默认是 string 类型。
我们可以通过 Pipe 把它转为整数:
效果如下:
当你传入的参数不能 parse 为 int 时,会返回这样的响应:
这个也是可以修改的,但要使用 new XxxPipe 的方式:
比如我指定错误时的状态码为 404。
就会返回这样的响应。
此外,你还可以自己抛一个异常出来,然后让 exception filter 处理:
可以看到,状态码和 message 都改了:
你也可以加个 @UseFilters 来使用自己的 exception filter 处理。
ParseFloatPipe 是把参数转换为 float 类型的。
ParseArrayPipe
这时会提示需要 class-validator 和 class-transformer 这两个包,前者是可以用装饰器和非装饰器两种方式对 class 属性做验证的库,后者是把普通对象转换为对应的 class 实例的包。
npm install -D class-validator class-transformer
然后访问下:
你会发现它确实把每一项都提取出来了,但是没有转为 number。
这时候就需要用 new XxxPipe 的方式传入参数了,指定 item 的类型。
这样就把数组每一项处理为 number 了。
此外,你还可以指定分隔符:
当没有传参数的时候会报错:
可以把它设置为 optional,这样不带参数就不会报错了。
ParseEnumPipe
假设我们有这样一个枚举:
这不是多此一举么,本来 @Param 也能把它取出来呀。
ParseEnumPipe 还是有用的:
第一个是可以限制参数的取值范围:
如果参数值不是枚举里的,就会报错。
这个错误自然也可以通过 errorHttpStatusCode 和 exceptionFactory 来定制。
第二个是帮你转换类型:
这里拿到的就直接是枚举类型了,如果有个方法的参数是这样的枚举类型,就可以直接传入。
ParseUUIDPipe
UUID 是一种随机生成的几乎不可能重复的字符串,可以用来做 id。
它有 v3、v4、v5 3 个版本,我们用 uuid 包可以生成这种 id:
在参数里,可以用 ParseUUIDPipe 来校验是否是 UUID:
如果不是 uuid 会抛异常:
DefaultValuePipe
这个是设置参数默认值的:
自定义Pipe
nest g pipe aaa --flat --no-spec
生成一个 pipe,打印下参数值,返回 aaa:
在 handler 里用下:
返回的值是 aaaaaa,也就是说 pipe 的返回值就是传给 handler 的参数值。
打印的 value 就是 query、param 的值,而 metadata 里包含 type、metatype、data:
type 就是 @Query、@Param、@Body 装饰器,或者自定义装饰器。
而 metatype 是参数的 ts 类型:
data 是传给 @Query、@Param、@Body 等装饰器的参数。
有了这些东西,做一下验证,抛出异常给 exception filter 处理,或者对 value 做些转换再传给 handler 就都是很简单的事情了。
ValidatetionPipe
上面学了 pipe 来对参数做验证和转换,但那些都是 get 请求的参数,如果是 post 请求呢?
post 请求的数据是通过 @Body 装饰器来取,并且要有一个 dto class 来接收:
(dto 是 data transfer object,数据传输对象,用于封装请求体的数据)
我们用 postman 来发个 post 请求。
content-type 指定为 json。
点击 send,就可以看到服务端接收到了数据,并且把它转为了 dto 类的对象。
但如果我们 age 传一个浮点数,服务端也能正常接收。
因为它也是 number。
而这很可能会导致后续的逻辑出错。
所以我们要对他做参数验证。
怎么做呢?
这就需要用到 ValidationPipe 了。
它需要两个依赖包:
npm install class-validator class-transformer
然后在 @Body 里添加这个 pipe:
在 dto 这里,用 class-validator 包的 @IsInt 装饰器标记一下:
再次请求,你就会发现它检查出了参数里的错误:
那它是怎么实现的呢?
class-validator 包提供了基于装饰器声明的规则对对象做校验的功能:
而 class-transformer 则是把一个普通对象转换为某个 class 的实例对象的:
这两者一结合,那 ValidationPipe 是怎么实现的不就想明白了么:
我们声明了参数的类型为 dto 类,pipe 里拿到这个类,把参数对象通过 class-transformer 转换为 dto 类的对象,之后再用 class-validator 包来对这个对象做验证。
我们自己写写看:
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToInstance } from 'class-transformer';
@Injectable()
export class MyValidationPipe implements PipeTransform<any> {
async transform(value: any, { metatype }: ArgumentMetadata) {
if (!metatype) {
return value;
}
// 把参数值转化为class Ooo 的实例
const object = plainToInstance(metatype, value);
// 然后就可以进行验证了
const errors = await validate(object);
if (errors.length > 0) {
throw new BadRequestException('参数验证失败');
}
return value;
}
}
metatype 为参数的类型,也就是 class Ooo:
如果没有声明这部分,那就没法转换和验证,直接返回 value。
否则,通过 class-transformer 包的 plainToInstance 把普通对象转换为 dto class 的实例对象。
之后调用 class-validator 包的 validate api 对它做验证。如果验证不通过,就抛一个异常。
替换为我们自己实现的 MyValidationPipe。
确实检查出了错误。
当然,我们做的并不够完善,还是直接用内置的 ValidationPipe 好了。
pipe 里也是可以注入依赖的:
比如,我们指定 @Inject 注入 token 为 validation_options 的对象。
因为标记了 @Optional,没找到对应的 provider 也不会报错。
但当我们在 module 里添加了这个 provider:
就可以正常注入了。
当然,这种方式就不能用 new 的方式了:
直接指定 class,让 Nest 去创建对象放到 ioc 容器里。
如果是全局的 pipe,要通过这种方式来创建才能注入依赖:
这就和我们之前创建全局 interceptor 一样。
同理,其余的 filter、guard 也可以通过这种方式声明为全局生效的:
现在我们就可以把 handler 里的 ValidationPipe 去掉了
再次访问,它依然是生效的。
会用 ValidationPipe 之后,我们回过头来再看看 class-validator 都支持哪些验证方式:
我们声明这样一个 dto class:
import { Contains, IsDate, IsEmail, IsFQDN, IsInt, Length, Max, Min } from 'class-validator';
export class Ppp {
@Length(10, 20)
title: string;
@Contains('hello')
text: string;
@IsInt()
@Min(0)
@Max(10)
rating: number;
@IsEmail()
email: string;
@IsFQDN()
site: string;
}
其中 @IsFQDN 是是否是域名的意思。
然后添加一个 post 的 handler:
当参数不正确,ValidationPipe 就会返回 class-validator 的报错:
这个错误消息也是可以定制的:
添加一个 options 对象,传入 message 函数,打印下它的参数:
可以拿到对象、属性名、属性值、class 名等各种信息,然后你可以返回自定义的 message:
@Length(10, 20, {
message({targetName, property, value, constraints}) {
return `${targetName} 类的 ${property} 属性的值 ${value} 不满足约束: ${constraints}`
}
})
title: string;
再次访问,返回的就是自定义的错误消息:
总结
接收 post 请求的方式是声明一个 dto class,然后通过 @Body 来取请求体来注入值。
对它做验证要使用 ValidationPipe。
它的实现原理是基于 class-tranformer 把参数对象转换为 dto class 的对象,然后通过 class-validator 基于装饰器对这个对象做验证。
我们可以自己实现这样的 pipe,pipe 里可以注入依赖。
如果是全局 pipe 想注入依赖,需要通过 APP_PIPE 的 token 在 AppModule 里声明 provider。
class-validator 支持很多种验证规则,比如邮箱、域名、长度、值的范围等,而且错误消息也可以自定义。
ValidationPipe 是非常常用的 pipe,后面会大量用到。