💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
# 管道 管道是一个用`@Injectable()`装饰器注解的类,它实现了`PipeTransform`接口。 ![](https://docs.nestjs.com/assets/Pipe_1.png) 管道有两个类型: - **转换**:管道将输入数据转换为所需的数据输出(例如,从字符串到整数) - **验证**:对输入数据进行验证,如果验证成功继续传递; 验证失败则抛出异常; 在这两种情况下, 管道 `参数(arguments)` 会由 [控制器(controllers)的路由处理程序](https://docs.nestjs.com/controllers#route-parameters) 进行处理. Nest 会在调用这个方法之前插入一个管道,管道会先拦截方法的调用参数,进行转换或是验证处理,然后用转换好或是验证好的参数调用原方法。 Nest 带有许多内置管道,您可以开箱即用。您还可以构建自己的自定义管道。在本章中,我们将介绍内置管道并展示如何将它们绑定到路由处理程序。然后,我们将检查几个定制的管道,以展示如何从头开始构建一个。 > 管道在异常区域内运行。这意味着当抛出异常时,它们由核心异常处理程序和应用于当前上下文的 [异常过滤器](https://docs.nestjs.com/exception-filters) 处理。当在 Pipe 中发生异常,controller 不会继续执行任何方法。 ## 内置管道 `Nest` 自带八个开箱即用的管道,即 - `ValidationPipe` - `ParseIntPipe` - `ParseBoolPipe` - `ParseArrayPipe` - `ParseUUIDPipe` - `DefaultValuePipe` - `ParseEnumPipe` - `ParseFloatPipe` 他们从 `@nestjs/common` 包中导出。为了更好地理解它们是如何工作的,我们将从头开始构建它们。 让我们快速浏览一下使用`ParseIntPipe`.这是**转换**用例的示例,其中管道确保将方法处理程序参数转换为 JavaScript 整数(或在转换失败时引发异常)。在本章后面,我们将展示一个简单的自定义实现`ParseIntPipe`。下面的示例技术也适用于其他内置转换管道(`ParseBoolPipe`、`ParseFloatPipe`、和`ParseEnumPipe`,我们将在本章中将其称为管道)。`ParseArrayPipe``ParseUUIDPipe``Parse*` #### 绑定管道[#](#binding-pipes) 要使用管道,我们需要将管道类的实例绑定到适当的上下文。在我们的`ParseIntPipe`示例中,我们希望将管道与特定的路由处理程序方法相关联,并确保它在调用该方法之前运行。我们使用以下构造来做到这一点,我们将其称为在方法参数级别绑定管道: ~~~typescript @Get(':id') async findOne(@Param('id', ParseIntPipe) id: number) { return this.catsService.findOne(id); } ~~~ 这确保了以下两个条件之一为真:我们在`findOne()`方法中收到的参数是一个数字(正如我们对 的调用所期望的那样`this.catsService.findOne()`),或者在调用路由处理程序之前抛出异常。 例如,假设路由被称为: ~~~bash GET localhost:3000/abc ~~~ Nest 会抛出这样的异常: ~~~json { "statusCode": 400, "message": "Validation failed (numeric string is expected)", "error": "Bad Request" } ~~~ 该异常将阻止`findOne()`方法的主体执行。 在上面的示例中,我们传递了一个类 (`ParseIntPipe`),而不是实例,将实例化的责任留给了框架并启用了依赖注入。与管道和守卫一样,我们可以传递一个就地实例。如果我们想通过传递选项来自定义内置管道的行为,传递就地实例很有用: ~~~typescript @Get(':id') async findOne( @Param('id', new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE })) id: number, ) { return this.catsService.findOne(id); } ~~~ 绑定其他转换管道(所有**Parse\***管道)的工作方式类似。这些管道都在验证路由参数、查询字符串参数和请求正文值的上下文中工作。 例如使用查询字符串参数: ~~~typescript @Get() async findOne(@Query('id', ParseIntPipe) id: number) { return this.catsService.findOne(id); } ~~~ 这是一个使用`ParseUUIDPipe`解析字符串参数并验证它是否为 UUID 的示例。 ~~~typescript @Get(':uuid') async findOne(@Param('uuid', new ParseUUIDPipe()) uuid: string) { return this.catsService.findOne(uuid); } ~~~ > **提示**:使用`ParseUUIDPipe()`时解析版本 3、4 或 5 中的 UUID,如果您只需要特定版本的 UUID,则可以在管道选项中传递版本。 上面我们已经看到了绑定各种`Parse*`内置管道系列的示例。绑定验证管道有点不同;我们将在下一节讨论这个问题。 > **提示**:此外,请参阅[验证技术](https://docs.nestjs.com/techniques/validation)以获取验证管道的广泛示例。 #### 自定义管道[#](https://docs.nestjs.com/pipes#custom-pipes) 如前所述,您可以构建自己的自定义管道。虽然 Nest 提供了强大的内置`ParseIntPipe`和`ValidationPipe`,但让我们从头开始构建每个简单的自定义版本,以了解如何构建自定义管道。 我们从 `ValidationPipe`. 开始。 首先它只接受一个值并立即返回相同的值,其行为类似于一个标识函数。 > validate.pipe.ts ```typescript 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` `value` 是当前处理的参数,而 `metadata` 是其元数据。元数据对象包含一些属性: ```typescript export interface ArgumentMetadata { type: 'body' | 'query' | 'param' | 'custom'; metatype?: Type<unknown>; data?: string; } ``` 这里有一些属性描述参数: | 参数 | 描述 | | -------- | ------------------------------------------------------------------------------------------------------------------------------ | | type | 告诉我们该属性是一个 body `@Body()`,query `@Query()`,param `@Param()` 还是自定义参数 [在这里阅读更多](https://docs.nestjs.com/custom-decorators)。 | | metatype | 属性的元类型,例如 `String`。 如果在函数签名中省略类型声明,或者使用原生 JavaScript,则为 `undefined`。 | | data | 传递给装饰器的字符串,例如 `@Body('string')`。 如果您将括号留空,则为 `undefined`。 | > `TypeScript`接口在编译期间消失,所以如果你使用接口而不是类,那么 `metatype` 的值将是一个 `Object`。 ## 基于模式的验证 让我们的验证管道更有用一点。仔细看看 的`create()`方法`CatsController`,我们可能希望在尝试运行我们的服务方法之前确保 post body 对象是有效的。 > cats.controller.ts ```typescript @Post() async create(@Body() createCatDto: CreateCatDto) { this.catsService.create(createCatDto); } ``` 下面是 `CreateCatDto` 参数. 类型为 CreateCatDto: > create-cat.dto.ts ```typescript export class CreateCatDto { name: string; age: number; breed: string; } ``` 我们要确保`create`方法能正确执行,所以必须验证 `CreateCatDto` 里的三个属性。我们可以在路由处理程序方法中做到这一点,但是我们会打破单个责任原则(SRP)。另一种方法是创建一个验证器类并在那里委托任务,但是不得不每次在方法开始的时候我们都必须使用这个验证器。 如何创建验证中间件? 这可能是一个好主意,但我们不可能创建一个整个应用程序通用的中间件(因为中间件不知道 `execution context`执行环境,也不知道要调用的函数和它的参数)。 当然,这正是管道设计的用例。因此,让我们继续完善我们的验证管道。 ## 对象结构验证 有几种方法可以实现,一种常见的方式是使用**基于结构**的验证。[Joi](https://github.com/hapijs/joi) 库是允许您使用一个可读的 API 以非常简单的方式创建 schema,让我们构建一个使用基于 Joi 的模式的验证管道。 首先安装所需的软件包: ~~~bash $ npm install --save joi $ npm install --save-dev @types/joi ~~~ 在下面的代码示例中,我们创建了一个以模式作为`constructor`参数的简单类。然后我们应用该`schema.validate()`方法,该方法根据提供的模式验证我们的传入参数。 就像是前面说过的,`验证管道` 要么返回该值,要么抛出一个错误。 在下一节中,你将看到我们如何使用 `@UsePipes()` 修饰器给指定的控制器方法提供需要的 schema。这样做使我们的验证管道可以跨上下文重用,就像我们开始做的那样。 ```typescript import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common'; import { ObjectSchema } from '@hapi/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('Validation failed'); } return value; } } ``` ## 绑定管道 早些时候,我们看到了如何绑定转换管道(比如`ParseIntPipe`和其他`Parse*`管道)。 绑定验证管道也非常简单。 在这种情况下,我们希望在方法调用级别绑定管道。在我们当前的示例中,我们需要执行以下操作才能使用`JoiValidationPipe`: 1. 创建一个实例`JoiValidationPipe` 2. 在管道的类构造函数中传递特定于上下文的 Joi 模式 3. 将管道绑定到方法 我们使用`@UsePipes()`如下所示的装饰器来做到这一点: ```typescript @Post() @UsePipes(new JoiValidationPipe(createCatSchema)) async create(@Body() createCatDto: CreateCatDto) { this.catsService.create(createCatDto); } ``` >**提示:**`@UsePipes()`装饰器是从`@nestjs/common`包 中导入的。 ## 类验证器 > 本节中的技术需要 `TypeScript` ,如果您的应用是使用原始 `JavaScript`编写的,则这些技术不可用。 让我们看一下验证的另外一种实现方式 Nest 与 [class-validator](https://github.com/pleerock/class-validator) 配合得很好。这个优秀的库允许您使用基于装饰器的验证。装饰器的功能非常强大,尤其是与 Nest 的 Pipe 功能相结合使用时,因为我们可以通过访问 `metatype` 信息做很多事情,在开始之前需要安装一些依赖。 ``` $ npm i --save class-validator class-transformer ``` 一旦安装了这些,我们就可以在`CreateCatDto`类中添加一些装饰器。在这里,我们看到了这种技术的一个显着优势:`CreateCatDto`该类仍然是我们的 Post body 对象的唯一真实来源(而不是必须创建一个单独的验证类)。 > create-cat.dto.ts ```typescript import { IsString, IsInt } from 'class-validator'; export class CreateCatDto { @IsString() name: string; @IsInt() age: number; @IsString() breed: string; } ``` > 在[此处](https://github.com/typestack/class-validator#usage)了解有关类验证器修饰符的更多信息。 现在我们来创建一个 `ValidationPipe` 类。 > validate.pipe.ts ```typescript import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common'; import { validate } from 'class-validator'; import { plainToClass } 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 = plainToClass(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); } } ``` >我们已经使用了 [class-transformer](https://github.com/pleerock/class-transformer) 库。它和 [class-validator](https://github.com/pleerock/class-validator) 库由同一个作者开发,所以他们配合的很好。 让我们来看看这个代码。首先你会发现 `transform()` 函数是 `异步` 的, Nest 支持**同步**和**异步**管道。这样做的原因是因为有些 `class-validator` 的验证是[可以异步的](typestack/class-validator#custom-validation-classes)(利用Promises) 接下来请注意,我们正在使用解构赋值(从 `ArgumentMetadata` 中提取参数)到方法中。这是一个先获取全部 `ArgumentMetadata` 然后用附加语句提取某个变量的简写方式。 下一步,请观察 `toValidate()` 方法。当验证类型不是 JavaScript 的数据类型时,跳过验证。 接下来,我们使用 `class-transformer` 的 `plainToClass()` 方法我们的纯 JavaScript 参数对象转换为类型化对象,以便我们可以应用验证。我们必须这样做的原因是传入的 `post body` 对象在从网络请求反序列化时**没有任何类型信息**(这是底层平台(例如 Express)的工作方式)。Class-validator 需要使用我们之前为 DTO 定义的验证装饰器,因此我们需要执行此转换以将传入的主体视为经过适当装饰的对象,而不仅仅是普通的 `vanilla` 对象。 最后,如前所述,这就是一个验证管道,它要么返回值不变,要么抛出异常。 最后一步是设置 `ValidationPipe` 。管道,与[异常过滤器](exceptionfilters.md)相同,它们可以是可以是参数范围、方法范围、控制器范围或全局范围。早些时候,在我们基于 Joi 的验证管道中,我们看到了在方法级别绑定管道的示例。在下面的示例中,我们将管道实例绑定到路由处理程序`@Body()`装饰器,以便调用我们的管道来验证。 > cats.controller.ts ```typescript @Post() async create(@Body(new ValidationPipe()) createCatDto: CreateCatDto) { this.catsService.create(createCatDto); } ``` 当验证逻辑仅涉及一个指定的参数时,参数范围的管道非常有用。要在方法级别设置管道,您需要使用 `UsePipes()` 装饰器。 > cats.controller.ts ```typescript @Post() @UsePipes(new ValidationPipe()) async create(@Body() createCatDto: CreateCatDto) { this.catsService.create(createCatDto); } ``` ## 全局管道[#](https://docs.nestjs.com/pipes#global-scoped-pipes) 由于 `ValidationPipe` 被创建为尽可能通用,所以我们将把它设置为一个**全局作用域**的管道,用于整个应用程序中的每个路由处理器。 > main.ts ```typescript async function bootstrap() { const app = await NestFactory.create(AppModule); app.useGlobalPipes(new ValidationPipe()); await app.listen(3000); } bootstrap(); ``` > 在 [混合应用](https://docs.nestjs.com/faq/hybrid-application)中 `useGlobalPipes()` 方法不会为网关和微服务设置管道, 对于标准(非混合) 微服务应用使用 `useGlobalPipes()` 全局设置管道。 全局管道用于整个应用程序、每个控制器和每个路由处理程序。请注意,就依赖注入而言,从任何模块外部注册的全局管道(如上例所示)无法注入依赖,因为它们不属于任何模块。为了解决这个问题,可以使用以下构造直接为任何模块设置管道: > app.module.ts ```typescript import { Module } from '@nestjs/common'; import { APP_PIPE } from '@nestjs/core'; @Module({ providers: [ { provide: APP_PIPE, useClass: ValidationPipe } ] }) export class AppModule {} ``` > 请注意使用上述方式依赖注入时,请牢记无论你采用那种结构模块管道都是全局的,那么它应该放在哪里呢?使用 `ValidationPipe` 定义管道 另外,useClass 并不是处理自定义提供者注册的唯一方法。在[这里](https://docs.nestjs.com/fundamentals/custom-providers)了解更多。 ## 内置验证管道 幸运的是,由于 `ValidationPipe` 和 `ParseIntPipe` 是内置管道,因此您不必自己构建这些管道(请记住, `ValidationPipe` 需要同时安装 `class-validator` 和 `class-transformer` 包)。与本章中构建ValidationPipe的示例相比,该内置的功能提供了更多的选项,为了说明管道的基本原理,该示例一直保持基本状态。您可以在[此处](https://docs.nestjs.com/techniques/validation)找到完整的详细信息以及许多示例。 ## 转换管道 验证不是管道唯一的用处。在本章的开始部分,我已经提到管道也可以将输入数据**转换**为所需的输出。这是可以的,因为从 `transform` 函数返回的值完全覆盖了参数先前的值。 在什么时候使用?有时从客户端传来的数据需要经过一些修改(例如字符串转化为整数),然后处理函数才能正确的处理。还有种情况,比如有些数据具有默认值,用户不必传递带默认值参数,一旦用户不传就使用默认值。**转换管道**可以通过在客户端请求和请求处理程序之间插入处理功能来执行这些功能。 这是一个`ParseIntPipe`负责将字符串解析为整数值的简单程序。(如上所述,Nest 具有`ParseIntPipe`更复杂的内置功能;我们将其作为自定义转换管道的简单示例包含在内)。 > parse-int.pipe.ts ```typescript 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; } } ``` 如下所示, 我们可以很简单的配置管道来处理所参数 id: ```typescript @Get(':id') async findOne(@Param('id', new ParseIntPipe()) id) { return await this.catsService.findOne(id); } ``` 由于上述结构,`ParseIntpipe` 将在请求触发相应的处理程序之前执行。 另一个有用的例子是按 ID 从数据库中选择一个现有的**用户实体**。 ```typescript @Get(':id') findOne(@Param('id', UserByIdPipe) userEntity: UserEntity) { return userEntity; } ``` 如果愿意你还可以试试 `ParseUUIDPipe` 管道, 它用来分析验证字符串是否是 UUID. ```typescript @Get(':id') async findOne(@Param('id', new ParseUUIDPipe()) id) { return await this.catsService.findOne(id); } ``` > `ParseUUIDPipe` 会使用 UUID 3,4,5 版本 来解析字符串, 你也可以单独设置需要的版本. 你也可以试着做一个管道自己通过 id 找到实体数据: ```typescript @Get(':id') findOne(@Param('id', UserByIdPipe) userEntity: UserEntity) { return userEntity; } ``` 请读者自己实现, 这个管道接收 id 参数并返回 UserEntity 数据, 这样做就可以抽象出一个根据 id 得到 UserEntity 的公共管道, 你的程序变得更符合声明式(Declarative 更好的代码语义和封装方式), 更 [DRY (Don't repeat yourself 减少重复代码) ](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself)编程规范. #### 提供默认值[#](#providing-defaults) `Parse*`管道期望定义一个参数的值。`null`他们在接收或`undefined`值时抛出异常。`Parse*`为了允许端点处理丢失的查询字符串参数值,我们必须在管道对这些值进行操作之前提供要注入的默认值。服务于这个`DefaultValuePipe`目的。`DefaultValuePipe`只需在相关管道之前在`@Query()`装饰器中实例化 a`Parse*`,如下所示: ~~~typescript @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 }); } ~~~