nestjs学习14:参数验证与转换 pipe

26 阅读6分钟

Pipe 是在参数传给 handler 之前对参数做一些验证和转换的 class。

nestjs 内置的 Pipe 有这些:

  • ValidationPipe
  • ParseIntPipe
  • ParseBoolPipe
  • ParseArrayPipe
  • ParseUUIDPipe
  • DefaultValuePipe
  • ParseEnumPipe
  • ParseFloatPipe
  • ParseFilePipe

分别来试下内置的 Pipe 的功能。

ParseIntPipe

image.png

参数默认是 string 类型。

我们可以通过 Pipe 把它转为整数:

image.png

效果如下:

image.png

当你传入的参数不能 parse 为 int 时,会返回这样的响应:

image.png

这个也是可以修改的,但要使用 new XxxPipe 的方式:

image.png

比如我指定错误时的状态码为 404。

image.png

就会返回这样的响应。

此外,你还可以自己抛一个异常出来,然后让 exception filter 处理:

image.png

可以看到,状态码和 message 都改了:

image.png

你也可以加个 @UseFilters 来使用自己的 exception filter 处理。

ParseFloatPipe 是把参数转换为 float 类型的。

image.png

ParseArrayPipe

image.png

这时会提示需要 class-validatorclass-transformer 这两个包,前者是可以用装饰器和非装饰器两种方式对 class 属性做验证的库,后者是把普通对象转换为对应的 class 实例的包。

npm install -D class-validator class-transformer

然后访问下:

image.png

你会发现它确实把每一项都提取出来了,但是没有转为 number。

这时候就需要用 new XxxPipe 的方式传入参数了,指定 item 的类型。

image.png

这样就把数组每一项处理为 number 了。

image.png

此外,你还可以指定分隔符:

image.png

当没有传参数的时候会报错:

image.png

可以把它设置为 optional,这样不带参数就不会报错了。

image.png

ParseEnumPipe

假设我们有这样一个枚举:

image.png

这不是多此一举么,本来 @Param 也能把它取出来呀。

ParseEnumPipe 还是有用的:

第一个是可以限制参数的取值范围:

image.png

如果参数值不是枚举里的,就会报错。

这个错误自然也可以通过 errorHttpStatusCode 和 exceptionFactory 来定制。

第二个是帮你转换类型:

image.png

这里拿到的就直接是枚举类型了,如果有个方法的参数是这样的枚举类型,就可以直接传入。

ParseUUIDPipe

UUID 是一种随机生成的几乎不可能重复的字符串,可以用来做 id。

它有 v3、v4、v5 3 个版本,我们用 uuid 包可以生成这种 id:

image.png

在参数里,可以用 ParseUUIDPipe 来校验是否是 UUID:

image.png

如果不是 uuid 会抛异常:

image.png

DefaultValuePipe

这个是设置参数默认值的:

image.png

自定义Pipe

nest g pipe aaa --flat --no-spec

生成一个 pipe,打印下参数值,返回 aaa:

image.png

在 handler 里用下:

image.png

image.png

返回的值是 aaaaaa,也就是说 pipe 的返回值就是传给 handler 的参数值。

打印的 value 就是 query、param 的值,而 metadata 里包含 type、metatype、data:

image.png

type 就是 @Query、@Param、@Body 装饰器,或者自定义装饰器。

而 metatype 是参数的 ts 类型:

image.png

data 是传给 @Query、@Param、@Body 等装饰器的参数。

有了这些东西,做一下验证,抛出异常给 exception filter 处理,或者对 value 做些转换再传给 handler 就都是很简单的事情了。

ValidatetionPipe

上面学了 pipe 来对参数做验证和转换,但那些都是 get 请求的参数,如果是 post 请求呢?

post 请求的数据是通过 @Body 装饰器来取,并且要有一个 dto class 来接收:

(dto 是 data transfer object,数据传输对象,用于封装请求体的数据)

image.png

我们用 postman 来发个 post 请求。

image.png

content-type 指定为 json。

点击 send,就可以看到服务端接收到了数据,并且把它转为了 dto 类的对象

但如果我们 age 传一个浮点数,服务端也能正常接收。

image.png

因为它也是 number。

而这很可能会导致后续的逻辑出错。

所以我们要对他做参数验证。

怎么做呢?

这就需要用到 ValidationPipe 了。

它需要两个依赖包:

npm install class-validator class-transformer

然后在 @Body 里添加这个 pipe:

image.png

在 dto 这里,用 class-validator 包的 @IsInt 装饰器标记一下:

image.png

再次请求,你就会发现它检查出了参数里的错误:

image.png

那它是怎么实现的呢?

class-validator 包提供了基于装饰器声明的规则对对象做校验的功能:

image.png

而 class-transformer 则是把一个普通对象转换为某个 class 的实例对象的:

image.png

这两者一结合,那 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:

image.png

如果没有声明这部分,那就没法转换和验证,直接返回 value。

否则,通过 class-transformer 包的 plainToInstance 把普通对象转换为 dto class 的实例对象。

之后调用 class-validator 包的 validate api 对它做验证。如果验证不通过,就抛一个异常。

替换为我们自己实现的 MyValidationPipe。

image.png

image.png

确实检查出了错误。

当然,我们做的并不够完善,还是直接用内置的 ValidationPipe 好了。

pipe 里也是可以注入依赖的:

image.png

比如,我们指定 @Inject 注入 token 为 validation_options 的对象。

因为标记了 @Optional,没找到对应的 provider 也不会报错。

但当我们在 module 里添加了这个 provider:

image.png

就可以正常注入了。

当然,这种方式就不能用 new 的方式了:

image.png

直接指定 class,让 Nest 去创建对象放到 ioc 容器里。

如果是全局的 pipe,要通过这种方式来创建才能注入依赖:

image.png

这就和我们之前创建全局 interceptor 一样。

同理,其余的 filter、guard 也可以通过这种方式声明为全局生效的:

image.png

现在我们就可以把 handler 里的 ValidationPipe 去掉了

image.png

再次访问,它依然是生效的。

会用 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:

image.png

当参数不正确,ValidationPipe 就会返回 class-validator 的报错:

image.png

这个错误消息也是可以定制的:

image.png

添加一个 options 对象,传入 message 函数,打印下它的参数:

image.png

可以拿到对象、属性名、属性值、class 名等各种信息,然后你可以返回自定义的 message:

@Length(10, 20, {
    message({targetName, property, value, constraints}) {
        return `${targetName} 类的 ${property} 属性的值 ${value} 不满足约束: ${constraints}`
    }
})
title: string;

再次访问,返回的就是自定义的错误消息:

image.png

总结

接收 post 请求的方式是声明一个 dto class,然后通过 @Body 来取请求体来注入值。

对它做验证要使用 ValidationPipe。

它的实现原理是基于 class-tranformer 把参数对象转换为 dto class 的对象,然后通过 class-validator 基于装饰器对这个对象做验证。

我们可以自己实现这样的 pipe,pipe 里可以注入依赖。

如果是全局 pipe 想注入依赖,需要通过 APP_PIPE 的 token 在 AppModule 里声明 provider。

class-validator 支持很多种验证规则,比如邮箱、域名、长度、值的范围等,而且错误消息也可以自定义。

ValidationPipe 是非常常用的 pipe,后面会大量用到。