管道是一个用@Injectable()装饰器注释的类,它实现了 PipeTransform 接口。
管道的作用如下:
- 转换:将输入数据转换为所需的形式(例如,从字符串到整数)
- 验证:评估输入数据,如果有效,则简单地通过不变;否则,抛出异常
NestJs 在调用方法之前插入一个管道,该管道接收指定给该方法的参数并对它们进行操作。任何转换或验证操作都会在此时发生,之后使用任何(可能)转换的参数调用路由处理程序。转换和验证都是由控制器中进行处理的。
Nest 附带了许多内置管道,您可以开箱即用。您还可以构建自己的自定义管道。
注意:当管道中有异常发生时,随后不会执行任何控制的方法。
内置管道
管道 | 说明 |
---|---|
ValidationPipe | 验证管道,可以验证请求的数据是否符合要求 |
ParseIntPipe | 参数转换 init 类型的管道 |
ParseFloatPipe | 参数转换 float 类型的管道 |
ParseBoolPipe | 参数转换 布尔 类型的管道 |
ParseArrayPipe | 参数转换 数组 类型管道 |
ParseUUIDPipe | 参数转换 UUID 的管道 |
ParseEnumPipe | 参数转换 枚举值 的管道 |
DefaultValuePipe | 设置默认值的管道 |
ParseFilePipe | 解析文件流的管道 |
绑定管道
要使用管道的时候,我们主要将管道的实例绑定在适当的上下文上就可以。具体实例如下:
@Get('/:id')
getCommodityById(@Param('id', ParseIntPipe) id: number) {
console.log(id)
return this.commodityService.findById(id)
}
当请求的参数为非数字的字符串时,接口会直接抛出异常,具体情况如下:
{
"statusCode": 400,
"message": "Validation failed (numeric string is expected)",
"error": "Bad Request"
}
异常阻止了后续的业务处理(findById()方法的执行)。
在上面的示例中,我们传递一个类 ( ParseIntPipe),而不是一个实例,将实例化的责任留给框架并启用依赖项注入。与管道和守卫一样,我们可以传递一个就地实例。如果我们想通过传递选项来自定义内置管道的行为,则传递就地实例非常有用:
@Get('/:id')
getCommodityById(
@Param('id', new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE }))
id: number
) {
console.log(id)
return this.commodityService.findById(id)
}
绑定其他转换管道(所有 Parse*管道)的工作原理类似。这些管道都在验证路由参数、查询字符串参数和请求正文值的上下文中工作。
定制管道
虽然 NestJS 提供了内置的管道,但是我们也可以根据自己的需求来定制管道。我们来创建一个简单的验证管道。具体代码如下:
import { PipeTransform, Injectable, ArgumentMetadata } from "@nestjs/common";
@Injectable()
export class ValidationPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
return value;
}
}
说明:PipeTransform<T, R>是任何管道都必须实现的通用接口。通用接口用于 T 指示输入的类型 value,并 R 指示 transform()方法的返回类型。
每个管道必须实现 transform()
方法。该方法有两个参数:
- value 当前处理的方法参数(在路由处理方法接收之前)
- metadata 当前处理的方法参数的元数据
元数据对象具有以下属性:
export interface ArgumentMetadata {
type: "body" | "query" | "param" | "custom";
metatype?: Type<unknown>;
data?: string;
}
属性说明:
属性 | 说明 |
---|---|
type | 指示参数是否是 body @Body()、 query @Query()、 param@Param()或自定义参数 |
metatype | 提供参数的元类型,例如 String。注意:该值是 undefined 您在路由处理程序方法签名中省略类型声明或使用普通 JavaScript 时的值。 |
data | 传递给装饰器的字符串,例如@Body(‘string’)。如果 undefined 您将装饰器括号留空。 |
基于模式的验证
业务基础代码:
@Post('/save')
saveItem(@Body() accountItem: CreateAccountDto) {
return this.accountService.save(accountItem)
}
export class CreateAccountDto {
name: string;
age: number;
breed: string;
}
看到上述的代码后,我们会考虑希望确保对 saveItem
方法的请求参数都是有效的数据类型。所以我们必须验证对象的三个成员 CreateAccountDto
。我们可以在路由处理程序方法内执行此操作,但这样做并不理想,因为它会破坏单一责任规则(SRP)。
另一种方法可能是创建一个验证器类并在那里委派任务。这样做的缺点是我们必须记住在每个方法的开头调用这个验证器。
创建验证中间件怎么样?这可能有效,但不幸的是,不可能创建可在整个应用程序的所有上下文中使用的通用中间件。这是因为中间件不知道执行上下文,包括将被调用的处理程序及其任何参数。
接下来我们借助joi
库来简化数据校验的工作,首先我们先安装所需要的包:
npm install --save joi
编写验证参数的管道,具体代码如下:
import {
PipeTransform,
Injectable,
ArgumentMetadata,
BadRequestException,
} from "@nestjs/common";
import { ObjectSchema } from "joi";
@Injectable()
export class JoiValidationPipe implements PipeTransform {
constructor(private schema: ObjectSchema) {}
transform(value: any, metadata: ArgumentMetadata) {
const { error } = this.schema.validate(value);
if (error) {
throw new BadRequestException("请求参数类型不正确");
}
return value;
}
}
绑定管道以及声明 Joi 的校验规则,具体代码如下:
import { Body, Controller, Get, Param, Post, UsePipes } from "@nestjs/common";
import * as Joi from "joi"; // 这里的导入方式写成import Joi from "joi" 会报错需要注意
import { CreateAccountDto } from "src/dto/createAccount.dto";
import { JoiValidationPipe } from "src/pipe/JoiValidation.pipe";
import { AccountService } from "src/service/account.service";
import { CommodityService } from "src/service/commodity.service";
// Joi 的校验规则可以放在控制器中也可以放在单独的文件夹下
const createCatSchema = Joi.object({
name: Joi.string().required(),
age: Joi.number().required(),
breed: Joi.string().required(),
});
@Controller("account/v1")
export class AccountController {
constructor(
private commodityService: CommodityService,
private accountService: AccountService
) {}
@Get("/info")
getInfo(@Param() params: { id: number }) {
const _commodity = this.commodityService.findById(params.id);
return this.accountService.findById(_commodity?.id);
}
@Post("/save")
@UsePipes(new JoiValidationPipe(createCatSchema))
saveItem(@Body() accountItem: CreateAccountDto) {
console.log(accountItem);
return this.accountService.save(accountItem);
}
}
当发起如下的请求参数时,接口会返回具体的错误信息。
// 请求参数
{
"name": "123",
"age": "12asd",
"breed": "123"
}
// 返回结果
{
"statusCode": 400,
"message": "请求参数类型不正确",
"error": "Bad Request"
}
类验证器
除了可以使用基于模式的验证器外,我们还可以使用类验证器来实现验证管道。要实现类验证器的管道只需几步就可以实现:
- 首先安装相关插件
npm i --save class-validator class-transformer
- 编写相应的
dto
文件
import { IsInt, IsString } from "class-validator";
export class CreateAccountDto {
@IsString()
name: string;
@IsInt()
age: number;
@IsString()
breed: string;
}
- 编写类验证器管道
import {
PipeTransform,
Injectable,
ArgumentMetadata,
BadRequestException,
} from "@nestjs/common";
import { validate } from "class-validator";
import { plainToInstance } from "class-transformer";
@Injectable()
export class ValidationPipe implements PipeTransform<any> {
async transform(value: any, { metatype }: ArgumentMetadata) {
if (!metatype || !this.toValidate(metatype)) {
return value;
}
const object = plainToInstance(metatype, value);
const errors = await validate(object);
if (errors.length > 0) {
throw new BadRequestException("Validation failed");
}
return value;
}
private toValidate(metatype: Function): boolean {
const types: Function[] = [String, Boolean, Number, Array, Object];
return !types.includes(metatype);
}
}
- 绑定类验证器管道
@Post('/save')
saveItem(@Body(new ValidationPipe()) accountItem: CreateAccountDto) {
console.log(accountItem)
return this.accountService.save(accountItem)
}
上述的类验证器管道代码需要做出以下几点说明:
-
transform()
方法被标记为async
。因为 NestJS 支持同步和异步管道。我们使用此方法 async 是因为某些类验证器验证可以是异步的(利用 Promises) -
toValidate()
方法是一个辅助方法,它的方法处理机制是,当传入的参数是 JavaScript 默认数据类型时,它就不会验证数据是否合规(这些参数不能附加验证装饰器,因此没有理由通过验证步骤运行它们,类验证器是首先验证数据类型) -
plainToInstance()
方法是将普通 JavaScript 参数对象转换为类型化对象,以便我们可以应用验证。我们必须这样做的原因是,当从网络请求反序列化时,传入的 post body 对象没有任何类型信息(这是底层平台(例如 Express)的工作方式)。类验证器需要使用我们之前为 DTO 定义的验证装饰器,因此我们需要执行此转换以将传入的主体视为适当装饰的对象,而不仅仅是普通对象
全局范围管道
全局范围的管道可以使用useGlobalPipes
来装载,具体实例如下:
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();
全局范围管道会应用于整个应用程序、每个控制器和每个路由。当我们需要在某个模块注入全局范围管道的依赖项是不行的,因为全局范围管道是在没有上下文环境下完成,所以没有上下文的支持,模块是不能找到对应实例。为了解决这个问题,我们可以用下面的方式来进行管道的注入:
import { Module } from "@nestjs/common";
import { APP_PIPE } from "@nestjs/core";
@Module({
providers: [
{
provide: APP_PIPE,
useClass: ValidationPipe,
},
],
})
export class AppModule {}
内置的校验管道
提醒一下,您不必自己构建通用验证管道,因为它 ValidationPipe 是由 Nest 开箱即用提供的。内置 ValidationPipe 提供了很多的功能,在后续的笔记中会做展示。
转换用例
验证并不是自定义管道的唯一用例。在笔记开头我们说过管道的另一个作用就是将输入数据转换为所需的格式。这是可能的,因为从函数返回的值 transform 完全覆盖了参数的先前值。
这什么时候有用?请考虑,有时从客户端传递的数据需要进行一些更改(例如将字符串转换为整数),然后才能由路由处理程序方法正确处理。此外,一些必需的数据字段可能会丢失,我们希望应用默认值。转换管道可以通过在客户端请求和请求处理程序之间插入处理函数来执行这些功能。
这是一个简单的函数 ParseIntPipe,负责将字符串解析为整数值。(如上所述,Nest 有一个 ParseIntPipe 更复杂的内置功能;我们将其作为自定义转换管道的简单示例)。具体实例如下:
import {
PipeTransform,
Injectable,
ArgumentMetadata,
BadRequestException,
} from "@nestjs/common";
@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
transform(value: string, metadata: ArgumentMetadata): number {
const val = parseInt(value, 10);
if (isNaN(val)) {
throw new BadRequestException("Validation failed");
}
return val;
}
}
然后我们可以将此管道绑定到所选参数,如下所示:
@Get(':id')
async findOne(@Param('id', new ParseIntPipe()) id) {
return this.catsService.findOne(id);
}
提供默认值
Parse*
管道期望定义参数的值。null
他们在接收或值时抛出异常 undefined
。为了允许端点处理缺失的查询字符串参数值,我们必须提供一个要在管道对 Parse\*
这些值进行操作之前注入的默认值。就是 DefaultValuePipe
为了这个目的。DefaultValuePipe
只需在@Query()装饰器中相关管道之前实例化 a 即可 Parse\*
,如下所示:
@Get()
async findAll(
@Query('activeOnly', new DefaultValuePipe(false), ParseBoolPipe) activeOnly: boolean,
@Query('page', new DefaultValuePipe(0), ParseIntPipe) page: number,
) {
return this.catsService.findAll({ activeOnly, page });
}