管道
管道是一个使用 @Injectable() 装饰器注解的类,它实现了 PipeTransform 接口。
管道有两个典型用例:
- 转换:将输入数据转换为所需的形式(例如,从字符串转换为整数)
- 验证:评估输入数据,如果有效,则原样传递;否则,抛出异常
在这两种情况下,管道都对 控制器路由处理程序 正在处理的 arguments 进行操作。Nest 在调用方法之前插入管道,管道接收发往该方法的参数并对其进行操作。任何转换或验证操作都在此时进行,之后路由处理程序会使用任何(可能经过转换的)参数被调用。
Nest 提供了许多内置管道,您可以直接使用。您也可以构建自己的自定义管道。在本章中,我们将介绍内置管道,并展示如何将它们绑定到路由处理程序。然后,我们将研究几个自定义构建的管道,以展示如何从头开始构建一个管道。
管道在异常区域内运行。这意味着当管道抛出异常时,它会由异常层(全局异常过滤器和应用于当前上下文的任何 异常过滤器)处理。鉴于上述情况,很明显,当在管道中抛出异常时,控制器方法不会被执行。这为您提供了一种最佳实践技术,用于在系统边界验证从外部源进入应用程序的数据。
内置管道
Nest 提供了几个开箱即用的管道:
ValidationPipeParseIntPipeParseFloatPipeParseBoolPipeParseArrayPipeParseUUIDPipeParseEnumPipeDefaultValuePipeParseFilePipeParseDatePipe
它们从 @nestjs/common 包中导出。
让我们快速了解一下使用 ParseIntPipe。这是 转换 用例的一个示例,其中管道确保方法处理程序参数被转换为 JavaScript 整数(如果转换失败则抛出异常)。在本章后面,我们将展示 ParseIntPipe 的简单自定义实现。下面的示例技术也适用于其他内置转换管道(ParseBoolPipe、ParseFloatPipe、ParseEnumPipe、ParseArrayPipe、ParseDatePipe 和 ParseUUIDPipe,我们在本章中将它们称为 Parse* 管道)。
绑定管道
要使用管道,我们需要将管道类的实例绑定到适当的上下文。在我们的 ParseIntPipe 示例中,我们希望将管道与特定的路由处理程序方法相关联,并确保它在方法调用之前运行。我们通过以下构造来实现,我们将其称为在方法参数级别绑定管道:
这确保以下两个条件之一为真:要么我们在 findOne() 方法中接收的参数是数字(正如我们在调用 this.catsService.findOne() 时所期望的那样),要么在路由处理程序被调用之前抛出异常。
例如,假设路由被调用如下:
Nest 将抛出如下异常:
该异常将阻止 findOne() 方法的主体执行。
在上面的示例中,我们传递了一个类(ParseIntPipe),而不是一个实例,将实例化的责任留给框架并启用依赖注入。与管道和守卫一样,我们也可以传递一个内联实例。如果我们想通过传递选项来自定义内置管道的行为,传递内联实例会很有用:
绑定其他转换管道(所有 Parse* 管道)的工作方式类似。这些管道都在验证路由参数、查询字符串参数和请求体值的上下文中工作。
例如,对于查询字符串参数:
下面是使用 ParseUUIDPipe 解析字符串参数并验证它是否为 UUID 的示例。
当使用 ParseUUIDPipe() 时,您正在解析版本 3、4 或 5 的 UUID,如果您只需要特定版本的 UUID,您可以在管道选项中传递版本。
上面我们已经看到了绑定各种内置 Parse* 家族管道的示例。绑定验证管道略有不同;我们将在以下部分讨论这一点。
另请参阅 验证技术 以获取验证管道的大量示例。
自定义管道
如前所述,您可以构建自己的自定义管道。虽然 Nest 提供了强大的内置 ParseIntPipe 和 ValidationPipe,但让我们从头开始构建它们的简单自定义版本,以了解如何构建自定义管道。
我们从一个简单的 ValidationPipe 开始。最初,我们让它简单地接受输入值并立即返回相同的值,表现得像一个恒等函数。
PipeTransform<T, R> 是任何管道必须实现的通用接口。通用接口使用 T 表示输入 value 的类型,R 表示 transform() 方法的返回类型。
每个管道都必须实现 transform() 方法以履行 PipeTransform 接口契约。此方法有两个参数:
valuemetadata
value 参数是当前处理的方法参数(在被路由处理方法接收之前),metadata 是当前处理的方法参数的元数据。元数据对象具有以下属性:
这些属性描述了当前处理的参数。
TypeScript 接口在转译过程中会消失。因此,如果方法参数的类型被声明为接口而不是类,metatype 值将为 Object。
基于模式的验证
让我们使我们的验证管道更有用一些。仔细看看 CatsController 的 create() 方法,我们可能希望在尝试运行服务方法之前确保 post body 对象有效。
让我们关注 createCatDto 主体参数。它的类型是 CreateCatDto:
我们希望确保对 create 方法的任何传入请求都包含有效的主体。因此,我们必须验证 createCatDto 对象的三个成员。我们可以在路由处理程序方法内部执行此操作,但这样做并不理想,因为它会破坏 单一职责原则 (SRP)。
另一种方法是创建一个 验证器类 并将任务委托给它。这样做的缺点是我们必须记住在每个方法的开始调用这个验证器。
创建验证中间件怎么样?这可能有效,但不幸的是,无法创建可在整个应用程序的所有上下文中使用的 通用中间件。这是因为中间件不知道 执行上下文,包括将被调用的处理程序及其任何参数。
当然,这正是管道设计的用例。所以让我们继续完善我们的验证管道。
对象模式验证
有几种方法可以以干净、DRY 的方式进行对象验证。一种常见的方法是使用 基于模式 的验证。让我们尝试这种方法。
Zod 库允许您以简单的方式创建模式,具有可读的 API。让我们构建一个使用基于 Zod 的模式的验证管道。
首先安装所需的包:
在下面的代码示例中,我们创建了一个简单的类,该类将模式作为 constructor 参数。然后我们应用 schema.parse() 方法,该方法根据提供的模式验证我们的传入参数。
如前所述,验证管道 要么返回未更改的值,要么抛出异常。
在下一节中,您将看到我们如何使用 @UsePipes() 装饰器为给定的控制器方法提供适当的模式。这样做可以使我们的验证管道在不同上下文中可重用,正如我们所设定的那样。
绑定验证管道
earlier,我们看到了如何绑定转换管道(如 ParseIntPipe 和其他 Parse* 管道)。
绑定验证管道也非常简单。
在这种情况下,我们希望在方法调用级别绑定管道。在我们当前的示例中,我们需要执行以下操作来使用 ZodValidationPipe:
- 创建
ZodValidationPipe的实例 - 在管道的类构造函数中传递特定于上下文的 Zod 模式
- 将管道绑定到方法
Zod 模式示例:
我们使用 @UsePipes() 装饰器来实现,如下所示:
@UsePipes() 装饰器从 @nestjs/common 包中导入。
zod 库要求在您的 tsconfig.json 文件中启用 strictNullChecks 配置。
类验证器
本节中的技术需要 TypeScript,如果您的应用程序使用纯 JavaScript 编写,则不可用。
让我们看看我们的验证技术的另一种实现。
Nest 与 class-validator 库配合得很好。这个强大的库允许您使用基于装饰器的验证。基于装饰器的验证非常强大,尤其是与 Nest 的 Pipe 功能结合使用时,因为我们可以访问处理属性的 metatype。在开始之前,我们需要安装所需的包:
安装这些包后,我们可以向 CreateCatDto 类添加一些装饰器。在这里,我们看到了这种技术的一个显著优势:CreateCatDto 类仍然是我们的 Post body 对象的单一事实来源(而不是必须创建一个单独的验证类)。
有关 class-validator 装饰器的更多信息,请阅读 这里。
现在我们可以创建一个使用这些注解的 ValidationPipe 类。
提醒一下,您不必自己构建通用验证管道,因为 ValidationPipe 是 Nest 开箱即用的。内置的 ValidationPipe 提供了比我们在本章中构建的示例更多的选项,该示例为了说明自定义构建管道的机制而保持简单。您可以在 这里 找到完整的详细信息以及许多示例。
我们上面使用了 class-transformer 库,它由与 class-validator 库相同的作者制作,因此它们配合得很好。
让我们分析一下这段代码。首先,请注意 transform() 方法被标记为 async。这是可能的,因为 Nest 支持同步和 异步 管道。我们将此方法设为 async,因为一些 class-validator 验证 可以是异步的(利用 Promise)。
接下来,请注意我们使用解构来将 metatype 字段(从 ArgumentMetadata 中提取仅此成员)提取到我们的 metatype 参数中。这只是获取完整 ArgumentMetadata 然后有一个额外语句来分配 metatype 变量的简写。
接下来,请注意辅助函数 toValidate()。它负责在当前处理的参数是原生 JavaScript 类型时跳过验证步骤(这些类型不能附加验证装饰器,因此没有理由通过验证步骤运行它们)。
接下来,我们使用类转换器函数 plainToInstance() 将我们的纯 JavaScript 参数对象转换为类型化对象,以便我们可以应用验证。我们必须这样做的原因是,当从网络请求反序列化时,传入的 post body 对象 没有任何类型信息(这是底层平台的工作方式,例如 Express)。Class-validator 需要使用我们之前为 DTO 定义的验证装饰器,因此我们需要执行此转换,将传入的主体视为适当装饰的对象,而不仅仅是普通对象。
最后,如前所述,由于这是一个 验证管道,它要么返回未更改的值,要么抛出异常。
最后一步是绑定 ValidationPipe。管道可以是参数范围、方法范围、控制器范围或全局范围的。 earlier,通过我们基于 Zod 的验证管道,我们看到了在方法级别绑定管道的示例。
在下面的示例中,我们将管道实例绑定到路由处理程序 @Body() 装饰器,以便调用我们的管道来验证 post body。
参数范围的管道在验证逻辑仅涉及一个指定参数时很有用。
全局范围管道
由于 ValidationPipe 被创建为尽可能通用,我们可以通过将其设置为 全局范围 管道来实现其全部效用,以便它应用于整个应用程序的每个路由处理程序。
在 混合应用程序 的情况下,useGlobalPipes() 方法不会为网关和微服务设置管道。对于 "标准"(非混合)微服务应用程序,useGlobalPipes() 会全局挂载管道。
全局管道用于整个应用程序,适用于每个控制器和每个路由处理程序。
请注意,在依赖注入方面,从任何模块外部注册的全局管道(如上面的示例中的 useGlobalPipes())不能注入依赖项,因为绑定是在任何模块的上下文之外完成的。为了解决这个问题,您可以使用以下构造 直接从任何模块 设置全局管道:
当使用这种方法为管道执行依赖注入时,请注意,无论在此构造中使用哪个模块,管道实际上都是全局的。应该在哪里做?选择定义管道(上面示例中的 ValidationPipe)的模块。此外,useClass 不是处理自定义提供程序注册的唯一方法。了解更多 这里。
内置的 ValidationPipe
提醒一下,您不必自己构建通用验证管道,因为 ValidationPipe 是 Nest 开箱即用的。内置的 ValidationPipe 提供了比我们在本章中构建的示例更多的选项,该示例为了说明自定义构建管道的机制而保持简单。您可以在 这里 找到完整的详细信息以及许多示例。
转换用例
验证不是自定义管道的唯一用例。在本章开始时,我们提到管道还可以 转换 输入数据为所需的格式。这是可能的,因为从 transform 函数返回的值完全覆盖了参数的先前值。
什么时候这有用?考虑到有时从客户端传递的数据需要进行一些更改 - 例如将字符串转换为整数 - 然后才能被路由处理程序方法正确处理。此外,一些必需的数据字段可能缺失,我们希望应用默认值。转换管道 可以通过在客户端请求和请求处理程序之间插入处理函数来执行这些功能。
下面是一个简单的 ParseIntPipe,它负责将字符串解析为整数值。(如上所述,Nest 有一个更复杂的内置 ParseIntPipe;我们将此作为自定义转换管道的简单示例)。
然后我们可以将此管道绑定到所选参数,如下所示:
另一个有用的转换案例是使用请求中提供的 id 从数据库中选择 现有用户 实体:
我们将此管道的实现留给读者,但请注意,与所有其他转换管道一样,它接收输入值(一个 id)并返回输出值(一个 UserEntity 对象)。这可以通过将样板代码从处理程序抽象到公共管道中来使您的代码更加声明性和 DRY。
提供默认值
Parse* 管道期望参数的值被定义。它们在收到 null 或 undefined 值时会抛出异常。为了允许端点处理缺失的查询字符串参数值,我们必须在 Parse* 管道对这些值进行操作之前提供要注入的默认值。DefaultValuePipe 用于此目的。只需在 @Query() 装饰器中在相关 Parse* 管道之前实例化一个 DefaultValuePipe,如下所示:

