Pipe 是在参数传给 handler 之前对参数做一些验证和转换的 class。
对应的源码如下:
对每个参数都会应用 pipe:
内置的 Pipe 有这些:
它们都实现了 PipeTransform 接口:
比如 ParseIntPipe 的源码是这样的:
分别来试下内置的 Pipe 的功能吧。
创建个项目:
nest new pipe-test -p npm参数默认是 string 类型:
可以通过 Pipe 把它转为整数:
效果如下:
当你传入的参数不能 parse 为 int 时,会返回这样的响应:
这个也是可以修改的,但要使用 new XxxPipe 的方式:
比如我指定错误时的状态码为 404。
就会返回这样的响应。
此外,你还可以自己抛一个异常出来,然后让 exception filter 处理:
可以看到,状态码和 message 都改了:
你也可以加个 @UseFilters 来使用自己的 exception filter 处理。
ParseFloatPipe 是把参数转换为 float 类型的。
它也同样可以 new ParseFloatPipe 的形式,传入 errorHttpStatusCode 和 exceptionFactory。
剩下这些与 parse 有关的 pipe 都试一下:
ParseBoolPipe:
ParseArrayPipe:
这时会提示需要 class-validator 这个包:
这是可以用装饰器和非装饰器两种方式对 class 属性做验证的库。
还会提示需要 class-transformer 这个包:
它是把普通对象转换为对应的 class 实例的包:
后面也会用到这俩包。
安装这俩包:
npm install -D class-validator class-transformer然后访问下:
你会发现它确实把每一项都提取出来了,但是没有转为 number。
这时候就需要用 new XxxPipe 的方式传入参数了:
指定 item 的类型。
这样就把数组每一项处理为 number 了。
此外,你还可以指定分隔符:
当没有传参数的时候会报错:
可以把它设置为 optional:
然后是 ParseEnumPipe:
假设有这样一个枚举:
就可以用 ParseEnumPipe 来取:
ParseEnumPipe 还是有用的:
第一个是可以限制参数的取值范围:
如果参数值不是枚举里的,就会报错。
这个错误自然也可以通过 errorHttpStatusCode 和 exceptionFactory 来定制。
第二个是帮你转换类型:
这里拿到的就直接是枚举类型了,如果有个方法的参数是这样的枚举类型,就可以直接传入。
接下来是 ParseUUIDPipe:
UUID 是一种随机生成的几乎不可能重复的字符串,可以用来做 id。
它有 v3、v4、v5 3 个版本,用 uuid 包可以生成这种 id:
在参数里,可以用 ParseUUIDPipe 来校验是否是 UUID:
如果不是 uuid 会抛异常:
接下来是 DefaultValuePipe:
这个是设置参数默认值的:
当你没传参数的时候,会使用默认值:
它的源码也很简单:
还剩下 ValidationPipe 和 ParseFilePipe,这个之后再讲。
接下来自己实现个 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 就都是很简单的事情了。
Pipe 是在参数传给 handler 之前做一些验证和转换的,有 9 个内置的 Pipe 可以直接用。
过了一遍内置的 ParseIntPipe、ParseBoolPipe、ParseArrayPipe、ParseUUIDPipe、ParseEnumPipe、ParseFloatPipe 还有 DefaultValuePipe。
剩下的 ValidationPipe 和 ParseFilePipe 之后的章节讲。
自己写一个 pipe 也很简单,就是实现 PipeTransform 接口的 transform 方法,它的返回值就是传给 handler 的值。
在 pipe 里可以拿到装饰器和 handler 参数的各种信息,基于这些来实现校验和转换就是很简单的事情了。
上节学了 pipe 来对参数做验证和转换,但那些都是 get 请求的参数,如果是 post 请求呢?
post 请求的数据是通过 @Body 装饰器来取,并且要有一个 dto class 来接收:
(dto 是 data transfer object,数据传输对象,用于封装请求体的数据)
用 postman 来发个 post 请求。
(postman 在这里下载: https://www.postman.com/downloads)
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;
}
const object = plainToInstance(metatype, value);
const errors = await validate(object);
if (errors.length > 0) {
throw new BadRequestException('参数验证失败');
}
return value;
}
}pipe 里拿到的 metatype 就是这部分:
如果没有声明这部分,那就没法转换和验证,直接返回 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:
在 postman 里发送 post 请求。
{
"title": "aaaaaaaaaaaaaaa",
"text": "hello aaa",
"rating": 10,
"email": "aaa@qq.com",
"site": "aaa.guang.com",
"createDate": "2023-05-28T01:45:37.803Z"
}参数正确的时候是不会报错的。
当参数不正确,ValidationPipe 就会返回 class-validator 的报错:
这个错误消息也是可以定制的:
添加一个 options 对象,传入 message 函数,打印下它的参数:
可以拿到对象、属性名、属性值、class 名等各种信息,然后你可以返回自定义的 message:
@Length(10, 20, {
message({targetName, property, value, constraints}) {
return `${targetName} 类的 ${property} 属性的值 ${value} 不满足约束: ${constraints}`
}
})
title: string;再次访问,返回的就是自定义的错误消息:
更多的装饰器可以看 class-validator 文档。
接收 post 请求的方式是声明一个 dto class,然后通过 @Body 来取请求体来注入值。
对它做验证要使用 ValidationPipe。
它的实现原理是基于 class-transformer 把参数对象转换为 dto class 的对象,然后通过 class-validator 基于装饰器对这个对象做验证。
可以自己实现这样的 pipe,pipe 里可以注入依赖。
如果是全局 pipe 想注入依赖,需要通过 APP_PIPE 的 token 在 AppModule 里声明 provider。
class-validator 支持很多种验证规则,比如邮箱、域名、长度、值的范围等,而且错误消息也可以自定义。
ValidationPipe 是非常常用的 pipe,后面会大量用到。
Exception Filter 是在 Nest 应用抛异常的时候,捕获它并返回一个对应的响应。
比如路由找不到时返回 404:
服务端报错时返回 500:
参数的错误返回 400:
这些都是 Exception Filter 做的事情。
那么,如果想自定义异常时返回的响应格式呢?
这种就要自定义 Exception Filter 了。
创建个 nest 项目:
nest new exception-filter-test把它跑起来:
npm run start:dev浏览器访问 http://localhost:3000 可以看到 hello world,代表服务跑起来了:
然后在 controller 里抛个异常:
throw new HttpException('xxxx', HttpStatus.BAD_REQUEST)这个 HttpStatus 就是一些状态码的常量:
这时候刷新页面,返回的就是 400 对应的响应:
这个响应的格式是内置的 Exception Filter 生成的。
当然,你也可以直接抛具体的异常:
然后自己定义个 exception filter:
nest g filter hello --flat --no-spec--flat 是不生成 hello 目录,--no-spec 是不生成测试文件。
@Catch 指定要捕获的异常,这里指定 BadRequestException。
import { ArgumentsHost, BadRequestException, Catch, ExceptionFilter } from '@nestjs/common';
@Catch(BadRequestException)
export class HelloFilter implements ExceptionFilter {
catch(exception: BadRequestException, host: ArgumentsHost) {
debugger;
}
}先打个断点。
在 AppModule 里引入:
app.useGlobalFilters(new HelloFilter());如果你想局部启用,可以加在 handler 或者 controller 上:
然后新建个调试配置文件:
输入调试配置:
{
"type": "node",
"request": "launch",
"name": "debug nest",
"runtimeExecutable": "npm",
"args": [
"run",
"start:dev",
],
"skipFiles": [
"<node_internals>/**"
],
"console": "integratedTerminal",
}把之前的服务关掉,点击调试启动:
刷新页面,代码会在断点处断住:
只要根据异常信息返回对应的响应就可以了:
import { ArgumentsHost, BadRequestException, Catch, ExceptionFilter, HttpException } from '@nestjs/common';
import { Response } from 'express';
@Catch(BadRequestException)
export class HelloFilter implements ExceptionFilter {
catch(exception: BadRequestException, host: ArgumentsHost) {
const http = host.switchToHttp();
const response = http.getResponse<Response>();
const statusCode = exception.getStatus();
response.status(statusCode).json({
code: statusCode,
message: exception.message,
error: 'Bad Request',
xxx: 111
})
}
}
这样,抛异常时返回的响应就是自定义的了:
但只是 @Catch 了 BadRequestException。
如果抛的是其他异常,依然是原来的格式:
比如我抛一个 BadGatewayException。
依然是默认格式。
那只要 @Catch 指定 HttpException 不就行了?
因为 BadRequestException、BadGatewayException 等都是它的子类。
试一下:
确实,现在所有的 HttpException 都会被处理了。
但其实这也有个问题。
就是当用了 ValidationPipe 的时候。
比如加一个路由:
@Post('aaa')
aaa(@Body() aaaDto: AaaDto ){
return 'success';
}然后创建 src/aaa.dto.ts
export class AaaDto {
aaa: string;
bbb: number;
}安装用到的包:
npm install --save class-validator class-transformer然后给 AaaDto 添加几个校验规则:
import { IsEmail, IsNotEmpty, IsNumber } from "class-validator";
export class AaaDto {
@IsNotEmpty({message: 'aaa 不能为空'})
@IsEmail({}, {message: 'aaa 不是邮箱格式'})
aaa: string;
@IsNumber({}, {message: 'bbb 不是数字'})
@IsNotEmpty({message: 'bbb 不能为空'})
bbb: number;
}在 main.ts 启用 ValidationPipe:
app.useGlobalPipes(new ValidationPipe());在 postman 里测试下:
可以看到,提示的错误也不对了。
因为自定义的 exception filter 会拦截所有 HttpException,但是没有对这种情况做支持。
先不加这个 filter。
这时候响应是这样的:
对这种情况做下支持:
启用自定义的 filter,然后打个断点:
再次访问会在断点处断住:
可以看到 ValidationPipe 的 response 格式是这样的。
所以可以这样改:
import { ArgumentsHost, BadRequestException, Catch, ExceptionFilter, HttpException } from '@nestjs/common';
import { Response } from 'express';
@Catch(HttpException)
export class HelloFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const http = host.switchToHttp();
const response = http.getResponse<Response>();
const statusCode = exception.getStatus();
const res = exception.getResponse() as { message: string[] };
response.status(statusCode).json({
code: statusCode,
message: res?.message?.join ? res?.message?.join(',') : exception.message,
error: 'Bad Request',
xxx: 111
})
}
}如果 response.message 是个数组,就返回 join 的结果,否则还是返回 exception.message
再试下:
现在,ValidationPipe 的错误和其他的错误就都返回了正确的格式。
那如果我想在 Filter 里注入 AppService 呢?
这就需要改一下注册方式:
不用 useGlobalFilters 注册了,而是在 AppModule 里注册一个 token 为 APP_FILTER 的 provider:
{
provide: APP_FILTER,
useClass: HelloFilter
}Nest 会把所有 token 为 APP_FILTER 的 provider 注册为全局 Exception Filter。
注册多个 Filter 也是这么写。
其余的全局 Guard、Interceptor、Pipe 也是这样注册:
这样注册的好处就是可以注入其他 provider 了:
比如我注入了 AppService,然后调用它的 getHello 方法:
import { ArgumentsHost, BadRequestException, Catch, ExceptionFilter, HttpException, Inject } from '@nestjs/common';
import { Response } from 'express';
import { AppService } from './app.service';
@Catch(HttpException)
export class HelloFilter implements ExceptionFilter {
@Inject(AppService)
private service: AppService;
catch(exception: HttpException, host: ArgumentsHost) {
const http = host.switchToHttp();
const response = http.getResponse<Response>();
const statusCode = exception.getStatus();
const res = exception.getResponse() as { message: string[] };
response.status(statusCode).json({
code: statusCode,
message: res?.message?.join ? res?.message?.join(',') : exception.message,
error: 'Bad Request',
xxx: 111,
yyy: this.service.getHello()
})
}
}可以看到,service 方法调用成功了:
此外,如果你想自定义 Exception 也是可以的。
比如添加一个 src/unlogin.filter.ts
import { ArgumentsHost, Catch, ExceptionFilter, HttpStatus } from '@nestjs/common';
import { Response } from 'express';
export class UnLoginException{
message: string;
constructor(message?){
this.message = message;
}
}
@Catch(UnLoginException)
export class UnloginFilter implements ExceptionFilter {
catch(exception: UnLoginException, host: ArgumentsHost) {
const response = host.switchToHttp().getResponse<Response>();
response.status(HttpStatus.UNAUTHORIZED).json({
code: HttpStatus.UNAUTHORIZED,
message: 'fail',
data: exception.message || '用户未登录'
}).end();
}
}创建了一个 UnloginException 的异常。
然后在 ExceptionFilter 里 @Catch 了它。
在 AppModule 里注册这个全局 Filter:
{
provide: APP_FILTER,
useClass: UnloginFilter
}之后在 AppController 里抛出这个异常:
浏览器里访问下:
可以看到,返回的是自定义的格式。
也就是说,可以用自定义 Exception Filter 捕获内置的或者自定义的 Exception。
这节学习了自定义 Exception Filter。
通过 @Catch 指定要捕获的异常,然后在 catch 方法里拿到异常信息,返回对应的响应。
如果捕获的是 HttpException,要注意兼容下 ValidationPipe 的错误格式的处理。
filter 可以通过 @UseFilters 加在 handler 或者 controller 上,也可以在 main.ts 用 app.useGlobalFilters 全局启用。
如果 filter 要注入其他 provider,就要通过 AppModule 里注册一个 token 为 APP_FILTER 的 provider 的方式。
此外,捕获的 Exception 也是可以自定义的。
这样,就可以自定义异常和异常返回的响应格式了。