管道
管道是一个用@Injectable()
装饰器注解的类,它实现了PipeTransform
接口。
管道有两个典型用例:
- transformation(转换): 将输入数据转换为所需形式(例如,从字符串到整数)
- validation(验证): 验证数据,如果有效,则只需按将其原样传递;否则,抛出异常
在这两种情况下,管道对控制器路由处理程序正在处理的参数进行操作。 Nest在调用方法之前插入一个管道, 该管道接收指定给该方法的参数并对它们进行操作。 任何转换或验证操作都会在此时发生,之后使用任何(可能)转换的参数调用路由处理程序。
Nest附带了许多内置管道,您可以开箱即用。 您还可以构建自己的自定义管道。 在本章中,我们将介绍内置管道并展示如何将它们绑定到路由处理程序。 然后,我们将检查几个定制的管道,以展示如何从头开始构建一个管道。
管道在例外区域内运行。这意味着当Pipe抛出异常时, 它将由异常层处理(全局异常过滤器和应用于当前上下文的任何异常过滤器)。 鉴于上述情况,应该清楚的是,当Pipe中引发异常时,随后不会执行任何控制器方法。 这为您提供了一种最佳实践技术,用于验证从系统边界的外部源进入应用程序的数据。
内建管道
Nest附带九个开箱即用的管道:
ValidationPipe
ParseIntPipe
ParseFloatPipe
ParseBoolPipe
ParseArrayPipe
ParseUUIDPipe
ParseEnumPipe
DefaultValuePipe
ParseFilePipe
它们是从@nestjs/common
包导出的。
让我们快速了解一下ParseIntPipe
的使用。
这是转换用例的示例,其中管道确保方法处理程序参数转换为JavaScript整数(或者如果转换失败则引发异常)。
在本章后面,我们将展示ParseIntPipe
的简单自定义实现。
下面的示例技术也适用于其他内置转换管道(ParseBoolPipe
,ParseFloatPipe
,
ParseEnumPipe
,ParseArrayPipe
和ParseUUIDPipe
,在本章中我们将其称为Parse*
管道)。
绑定管道
要使用管道,我们需要将管道类的实例绑定到适当的上下文。
在我们的ParseIntPipe
示例中,
我们希望将管道与特定的路由处理程序方法相关联,并确保它在调用该方法之前运行。
我们使用以下构造来实现此目的,我们将其称为在方法参数级别绑定管道。
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
return this.catsService.findOne(id);
}
这确保以下两个条件之一为真:我们在findOne()
方法中收到的参数是一个数字(正如我们对
this.catsService.findOne()
的调用所预期的那样),或者在调用路由处理程序之前抛出异常。
例如,假设该路线被称为:
GET localhost:3000/abc
Nest将抛出一个异常:
{
"statusCode": 400,
"message": "Validation failed (numeric string is expected)",
"error": "Bad Request"
}
该异常将阻止findOne()
方法的主体执行。
在上面的示例中,我们传递一个类(ParseIntPipe
),
而不是实例,将实例化的责任留给框架并启动依赖项注入。
与管道和守卫一样,我们可以传递一个就地实例。
如果我们想通过传递选项来自定义内置管道的行为,
则传递就地实例非常有用:
@Get(':id')
async findOne(
@Param('id', new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE }))
id: number,
) {
return this.catsService.findOne(id);
}
绑定其他转换管道(所有Parse*
管道)的工作原理类似。
这些管道都在验证路由参数、查询字符串参数和请求正文值的上下文中工作。
例如,使用查询字符串参数:
@Get()
async findOne(@Query('id', ParseIntPipe) id: number) {
return this.catsService.findOne(id),
}
下面是使用ParseUUIDPipe
解析字符串参数并验证它是否为UUID的示例。
@Get(':uuid')
async findOne(@Param('uuid', new ParseUUIDPipe()) uuid: string) {
return this.catsService.findOne(uuid);
}
当使用ParseUUIDPipe()
时,您正在解析版本3,4或5中的UUID,如果您只需要特定版本的UUID,
您可以再管道选项中传递版本。
上面我们看到了绑定各种Parse*
系列内置管道的示例。
绑定验证管道有点不同;我们将在下一节中讨论这一点。
自定义管道
如前所述,您可以构建自己的自定义管道。
虽然Nest提供了强大的内置ParseIntPipe
和ValidationPipe
,但让我们从头开始构建
每个版本的简单自定义版本,看看如何构造自定 义管道。
我们从一个简单的ValidationPipe
开始。最初,我们将让它简单地接受一个输入值并立即返回相同的值,
其行为就像一个恒等函数。
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()
方法来履行PipeTransform
接口契约。
该方法有两个参数:
value
metadata
value
参数是当前处理的方法参数(在路由处理方法接收之前),
metadata
是当前处理的方法参数的元数据。
元数据对象具有以下属性:
export interface ArgumentMetadata {
type: 'body' | 'query' | 'param' | 'custom';
metatype?: Type<unknown>;
data?: string;
}
这些属性描述了当前处理的参数。
type
:指示参数是主体Body()
,查询Query()
,参数@Param()
还是自定义参数metatype
:提供参数的元数据,例如String
。注意:如果您在路由处理程序方法签名中省略类型声明,或者使用普通JavaScript,则该值未定义。data
:传递给装饰器的字符串,例如@Body('string')
。如果将装饰器括号留空,则它是未定义(undefined
)的。
TypeScript接口在转译过程中消失。因此,如果方法参数的类型声明为接口而不是类,
则metatype
值将为Object
基于模式的验证
让我们让验证管道变得更有用一点。仔细看看CatsController
的create()
方法,我们可能希望在
尝试运行我们的服务方法之前确保帖子主题对象有效。
@Post()
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
让我们关注createCatDto
body参数。它的类型是CreateCatDto
:
export class CreateCatDto {
name: string;
age: number;
breed: string;
}
我们希望确保对create方法的任何传入请求都包含有效的主体。
因此,我们必须验证createCatDto
对象的三个成员。
我们可以在路由处理方法内部执行此操作,但这样做并不理想,因为它会违反单一职责原则(SRP)。
另一种方法可能是创建一个验证器类并将任务委托给它。 这样做的缺点是我们必须记得在每个方法的开头调用这个验证器。
创建验证中间件怎么样?这可能有效,但不幸的是,无法创建通用中间件,可以在整个应用程序的所有上下文中使用。 这是因为中间件不知道执行上下文,包括将被调用的处理程序及其任何参数。
当然,这正是管道设计的用例。因此,让我们继续完善我们的验证管道。
对象模式验证
有几种可以以清晰、不重复的方式进行对象验证的方法。一种常见的方法是使用基于模式的验证。 让我们尝试这种方法。
Zod库允许您以一种简单易读的API方式创建模式。 让我们构建一个使用基于Zod的模式的验证管道。
首先,安装所需的包。
npm install --save zod
在下面的代码示例中,我们创建一个简单的类,该类以模式作为构造函数参数。
然后,我们应用schema.parse()
方法,该方法根据提供的模式验证我们传入的参数。
正如前面提到的,验证管道要么返回不变的值,要么抛出异常。
在下一节中,您将看到我们如何使用@UsePipes()
装饰器为给定的控制器方法提供适当的模式。
这样做使我们的验证管道在不同上下文中可重用,正如我们所设想的那样。
import { PipeTransform, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { ZodObject } from 'zod';
export class ZodValidationPipe implements PipeTransform {
constructor(private schema: ZodObject<any>) {}
transform(value: unknown, metadata: ArgumentMetadata) {
try {
this.schema.parse(value);
} catch (error) {
throw new BadRequestException('Validation failed');
}
return value;
}
}
绑定验证管道
之前,我们看到了如何绑定转换管道(比如ParseIntPipe
和其他Parse*
管道)。
绑定验证管道也非常简单。
在这种情况下,我们希望在方法调用级别绑定管道。
在我们当前的示例中,要使用ZodValidationPipe
,我们需要执行以下操作:
- 创建
ZodValidationPipe
的实例 - 在管道的类构造函数中传递上下文特定的Zod模式
- 将管道绑定到方法
Zod模式示例:
import { z } from 'zod';
export const createCatSchema = z
.object({
name: z.string(),
age: z.number(),
breed: z.string(),
})
.required();
export type CreateCatDto = z.infer<typeof createCatSchema>;
我们使用@UsePipes()
装饰器来做到这一点,如下所示:
@Post()
@UsePipes(new ZodValidationPipe(createCatSchema))
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
@UsePipes()
装饰器是从@nestjs/common
包导入的
zod
库需要再tsconfig.json
文件中启用strictNullChecks
配置
类验证器
本节中的技术需要TypeScript,如果您的应用程序是使用普通JavaScript编写的,则这些技术不可用
Nest与class-validator
库搭配使用效果很好。
这个强大的库允许您使用基于装饰器的验证。
基于装饰器的验证非常强大,特别是与Nest的管道功能结合使用时,
因为我们可以访问已处理属性的元类型(metatype
)。
在开始之前,我们需要安装所需的包:
npm i --save class-validator class-transformer
安装完成后,我们可以在CreateCatDto
类中添加一些装饰器。
这里我们看到了这种技术的一个重要优势:CreateCatDto
类仍然是我们Post主体对象的唯一真实来源
(而不是必须创建一个单独的验证类)。
import { IsString, IsInt } from 'class-validator';
export class CreateCatDto {
@IsString()
name: string;
@IsInt()
age: number;
@IsString()
breed: string;
}
现在我们可以创建一个使用这些注解的ValidationPipe
类
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToInstance } from 'class-validator';
@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 (error.length > 0) {
throw new BadRequestException('Validation failed');
}
return value;
}
private toValidate(metatype: Function): boolean {
const types: Function[] = [String, Boolean, Number, Array, Object];
return !types.include(metatype);
}
}
作为提醒,您无需自己构建通用验证管道,因为Nest已经提供了ValidationPipe
。
内置的ValidationPipe
提供了比本章中构建的示例更多的选项,为了说明自定义管道的机制,本章将其保持基本。
您可以在这 里找到完整的详细信息,以及许多示例。
我们在上面使用了由与class-validator
库相同作者制作的class-transformer
库,
因此它们之间协同运作得非常好。
让我们浏览一下这段代码。首先,请注意transform()
方法被标记为异步。
这是因为Nest支持同步和异步管道。我们使这个方法异步,
因为class-validator
的一些验证可能是异步的(使用Promises)。
接下来请注意,我们使用解构来提取metatype
字段(从ArgumentMetadata
中提取此成员)到我们的metatype
参数中。
这只是一种简写,用于获取完整的ArgumentMetadata
,然后有一个额外的语句来分配metatype
变量。
接下来,请注意辅助函数toValidate()
。它负责在当前正在处理的参数是原生JavaScript类型时绕过验证步骤(这些类型不能附加验证装饰器,因此没有理由将它们通过验证步骤)。
接下来,我们使用class-transformer
函数plainToInstance()
将我们的普通JavaScript参数对象转换为带类型的对象,
以便我们可以应用验证。我们必须这样做的原因是,从网络请求反序列化的传入主体对象没有任何类型信息(这是底层平台,
如Express,的工作方式)。
Class-validator需要使用我们之前为DTO定义的验证装饰器,因此我们需要进行这种转换,
将传入的主体视为适当装饰的对象,而不仅仅是一个普通的对象。
最后,如前所述,由于这是一个验证管道,它要么返回不变的值,要么抛出异常。
最后一步是绑定ValidationPipe
。
管道可以是参数范围、方法范围、控制器范围或全局范围。
之前,在我们基于Zod的验证管道中,我们看到了一个在方法级别绑定管道的示例。
在下面的示例中,我们将把管道实例绑定到路由处理程序的@Body()
装饰器,以便我们的管道被调用来验证发布主体。
@Post()
async create(
@Body(new ValidationPipe()) creteCatDto: CreateCatDto,
) {
this.catsService.create(createCatDto);
}
当验证逻辑仅涉及一个指定参数时,参数范围管道非常有用。
全局范围的管道
由于ValidationPipe
被创建为尽可能通用,我们可以通过将其设置为全局范围的管道来充分发挥其效用,
使其应用于整个应用程序的每个路由处理程序。
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();
在混合应用程序的情况下,useGlobalPipes()
方法不会为网关和微服务设置管道。
对于“标准”(非混合)微服务应用程序,useGlobalPipes()
确实会在全局范围内挂载管道。
全局管道在整个应用程序中使用,对于每个控制器和每个路由处理程序都起作用。
请注意,在依赖注入方面,从任何模块外部注册的全局管道(如上例中的 useGlobalPipes()
)无法注入依赖项,
因为绑定是在任何模块的上下文之外完成的。为了解决这个问题,您可以使用以下结构直接从任何模块设置全局管道:
import { Module } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';
@Module({
providers: [
{
provider: APP_PIPE,
useClass: ValidationPipe,
},
],
})
export class AppModule {}
在使用此方法执行管道的依赖注入时,请注意,无论在哪个模块中使用此结构,管道实际上都是全局的。
这应该在哪里完成?选择定义管道(上面的例子中是ValidationPipe
)的模块。
另外,useClass
不是处理自定义提供者注册的唯一方法。