前言
之前匆匆写了一篇关于Nest官文的翻译,然而写的时候比较着急,所以很多地方的翻译比较马虎,甚至直接丢进翻译器在丢出来....(有的时候我会被丢进翻译器之后被丢出来的译文吓到,原因是...译文比自己翻译的好/(ㄒoㄒ)/~~)但还是有很多部分是翻译器解决不了的,语句通顺固然优雅锦上添花,但一点小的错误却是致命的。
现在手头比较悠闲,也打算重新修改一份比较优雅的中文文档。有人会问花这么多时间写这个东西,是不是真的有用。百度上面也有一些关于Nest的文档,完全也不会有人来看你写的翻译。我的感受是,可能这就是我的学习方式吧。其实平时阅读文档,大多数情况下都是脑子说会了,手说不会。一边翻译英文文档,一遍理解框架的含义,还有助于提高阅读英文文档的能力。手敲过一遍和眼睛看过一遍真的不太一样,而且这种方式会增加使用时的自信。之后打算将每一章节分开书写,最后再通过链接汇总到一篇Nest妲己大记中去。就算没有人看,自己想要翻阅文档的时候,也可以拿出来看看,还能改改。这是一种乐趣,就好像养成游戏一样。
正文 Pipes
Pipe(管道)是一个使用装饰器 @Injectable() 的类。管道应当实现 PipeTransform 接口。
- 转换:将输入数据转换成想要的输出
- 验证:校验输入的数据以及是否可用,不改变地传递这个数据。当数据不正确会爆出一个异常。
在以上两种情况中,管道对参数的处理由控制器的路由处理函数完成。Nest在调用函数之前会插入一个管道,管道会接受传给函数的参数。任何转换或验证都在此时此刻完成执行,然后路由处理函数接受转换后的参数并执行。
Hints
管道运行在异常的层内,这意味着当管道抛出异常时会交给异常层处理(全局的异常过滤器以及任何异常过滤器会应用在当前上下文中)。综上你应该清楚,当一个异常在管道内被抛出时,控制器的方法将不会执行。
Built-in pipes 内置的管道
Nest自带了三个开箱即用的管道:
ValidationPipe,
ParseIntPipe和
ParseUUIDPipe。
他们从@nestjs/common包导出。
从
ValidationPipe开始,我们让他简单地接收一个值并立刻返回相同的值,就像一个恒等函数。
// validation.pipe.ts
import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common'
@Injectable()
export class ValidationPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
return value
}
}
Hints
PipeTransform<T, R>是一个通用接口。T表示输入值的类型,R表示由 transform()方法返回的值的类型。
所有管道都需要提供 transform() 方法。这个方法有两个参数:
- value
- metadata
value当前要处理的参数(在路由处理函数接受他们之前),metadata是他的元数据。metadata有这些属性:、
export interface ArgumentMetadata {
type: 'body' | 'query' | 'param' | 'custom';
metatype?: Type<any>;
data?: string;
}
这些属性描述了当前处理的参数。
| type | 表示参数在@Body(),@Query(),@Param()中或者是自定义参数 |
| metatype | 提供了参数的元类型,例如String。注意如果你在路由处理函数的签名中省略了类型声明,或者使用js,value的值会是undefined |
| data | 传入装饰器的字符,例如Body('string')。当然装饰器的参数为空,值就是undefined |
Warning
TypeScript的接口在编译的时候会消失。如果一个方法的参数类型使用接口声明而不是类,那么metatype元类型的值将会是对象。
Validation use case 使用管道验证
让我们仔细看看 CatsController 控制器中的 create() 方法。
@Post()
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createDto)
}
让我们关注参数 createCatDto 的类型 CreateCatDto。
// create-cat.dto.ts
export class CreateCatDto {
readonly name: string;
readonly age: number;
readonly bread: string;
}
我们想要确保任何create方法的请求包含一个可用的请求体body。所以需要验证
createCatDto
对象的三个成员。这可以在路由处理函数中完成,但是会打破SRP(single responsibility rule)(单一职能)原则。另一个方法就是创建验证器的类,将任务委托给他,但是这就必须在每个路由处理函数之前使用。如果打造一个验证的中间件呢?但我们不可能创建出一个整个应用都通用的中间件(因为中间件并不知道执行上下文,包括将要调用的处理函数和所接受的参数)。
综上管道便是理想的解决方案。
Object schema validation 对象结构的验证
有几种可用的方法来验证对象。一个普通的方法就是使用基于结构的验证。Joi库可以让你通过可读的api简单地创建结构。
从安装依赖包开始:
$ npm install --save @hapi/joi
$ npm install --save-dev @types/hapi__joi
在下面的例子中,我们创建了一个简单地类,构造器中接受一个结构对象。然后应用
schema.validate()
方法来验证参数是否符合提供的结构对象。
如前所述,验证管道要么返回不变的值,要么跑出一个异常。
下一节中你会看到我们如何通过使用装饰器
@UsePipes()
给控制器方法应用适当的结构。
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common'
@Injectable()
export class JoiValidationPipe implements PipeTransform {
constructor(private readonly schema: Object) {}
transform(value: any, metadata: ArgumentMetadata) {
const { error } = this.schema.validate(value)
if (error) {
throw new BadRequestException('Validation failed')
}
return value
}
}
Binding pipe 绑定管道
使用 @UsePipes 装饰器,创建一个管道实例并传入Joi验证结构。
@Post()
@UsePipes(new JoiValidationPipe(createCatSchema))
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createDto)
}
Class validation 类验证器
Warning
此节技术需要ts,如果app使用纯js写的就没有用。
让我们看看实现验证的另一种技术。
Nest也可以与class-validator库一起工作。这个库可以让你使用基于装饰器的验证。基于装饰器的验证是非常强大,尤其是当他和Nest管道的能力相结合的时候,因为我们能访问到要待处理属性的metatype元类型。
开始之前需要安装依赖:
$ npm i --save class-validator class-transformer
安装完成后可以在 CreateCatDto 类中添加一谢装饰器。
// create-cat.dto.ts
import { IsString, IsInt } from 'class-validator'
export class CreateCatDto {
@IsString()
readonly name: string;
@IsInt()
readonly age: number;
@IsString()
readonly breed: string;
}
Warning
关于class-validator,请阅读这里
现在我们可以创建一个 ValidationPipe 类。
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 (error.length > 0) {
throw new BadRequestException('Validation faild')
}
return value
}
private toValidate(metatype: Function): boolean {
const types: Function[] = [String, Boolean, Number, Array, Object]
return !types.includes(metatype)
}
}
}
Hints
上面我们使用了class-transformer库,由同一个制作了class-validator库的作者制作。
首先,注意
transform()
这个方法是异步的。Nest可以支持异步或同步的管道,故而示例用async表现了验证器可以使用异步操作。
使用解构精确地将元类型字段提取到metatype中(仅从
ArgumentMetadata
类型中提取metatype成员)。
注意辅助函数
toValidate()
。他负责当当前处理地参数是一个原生的js类型时绕过验证步骤(因为他们不能携带结构,所以没有理由让他们经过验证的步骤)。
然后我们使用class-transformer的方法
plainToClass()
来将我们纯js的参数对象转换为具有类型的对象。如此,才可以使用管道验证。从网络请求反序列化来的数据没有任何的类型信息。class-validator库需要借助我们在DTO中定义的验证装饰器,所以我们必须执行这个转换方法。
最后就像之前所说的,验证管道返回一个不变的值,或者抛出一个异常。
最后一步是绑定这个 ValidationPipe 管道。就像异常过滤器一样,管道可以是函数范围的,控制器范围的或者全局范围的。此外,管道还可以是参数范围的。下例中,我们将管道实例绑定在路由装饰器 @Body() 上。
// cats.controller.ts
@Post()
async create(
@Body(new ValidationPipe()) createCatDto: CreateCatDto,
) {
this.catsService.create(createCatDto)
}
当验证的逻辑只关注指定的参数时,参数范围的管道也非常有用。使用装饰器 @UsePipes()
// cats.controller.ts
import { UsePipes, Post, Body } from '@nestjs/common'
@Post()
@UsePipes(new ValidationPipe())
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto)
}
下面的例子中, ValidationPipe 的实例立刻就地创建。将类传入装饰器,将实例化的工作交给框架来完成,并实现注入依赖。
// cats.controller.ts
@Post()
@UsePipes(ValidationPipe)
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(CreateCatDto)
}
如果希望创建的管道 ValidationPipe 尽可能通用,就将他设置为全局范围地管道。
// main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule)
app.useGlobalPipes(new ValidationPipe())
await app.listen()
}
bootstrap()
NOTICE
在混合应用中, useGlobalPipes() 方法不会为网关和微服务设置管道。标准(非混合)的微服务应用 useGlobalPipes() 方法不会挂载全局的管道。
全局管道作用在整个应用,所有的控制器和路由处理函数。按照依赖注入,在任意模块意外注册的全局管道不能注入依赖,因为这一步是在模块的上下文之外完成的。俄日了解决这个问题,你可以用以下的方式设置来自任意模块的全局管道。
// app.module.ts
import { Module } from '@nestjs/common'
import { APP_PIPE } from '@nestjs/core'
@Module({
providers:[
{
provide: APP_PIPE,
useClass: ValidationPipe,
}
]
})
export class AppModule {}
Hints
当使用这个方法来为管道注入依赖时,注意无论在哪里实例化,实际上管道都是全局的。所以在哪里注入呢?选择管道定义所在的模块就可以了。 useClass 也不是注册provider的唯一方式。
Transformation use case 使用管道转换
验证不是管道的唯一用处。在章节开始的时候我们提到,管道也可以将输入的数据转换为预期输出。因为由
transform()
方法返回的值完全重写了先前传进来的参数。考虑到有时候从客户端传入的数据需要一些改变——比如说将字符转为数字——在他被路由处理函数处理之前。此外,一些必需的字段可能会遗漏,而我们希望应用默认的值。转换管道能通过在客户端请求和请求函数之间插入一个处理函数完成这些功能。
这里有一个
ParsePipe
管道,负责将字符传为整数。
// parse-int.pipe.ts
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common'
@Injectable()
export class ParsePipe implements PipeTransform<string, number> {
transform(value: string; metadata: ArguemntMetadata): 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)
}
如果你愿意可以使用 ParseUUIDPipe 管道,负责将参数转为字符并验证是否是uuid。
@Get('id')
async findOne(@Param('id', new ParseUUIDPipe()) id) {
return this.catsService.findOne(id)
}
Hints
当使用 ParseUUIDPipe() 方法,你是在版本3、4、5中解析。如果你只希望指定版本的uuid,那么管道配置中传入一个版本号。
如此一来,管道
ParseIntPipe
和
ParseUUIDPipe
会在请求到达正确的处理函数之前执行。并确保接受的id参数失踪是一个整数或者uuid。
另一个有用的场景是通过id来选择用户实体:
@Get(':id')
findOne(@Param('id', UseByIdPipe) userEntity: UserEntity) {
return useEntity
}
请注意,与所有其他转换管道一样,它接收一个输入值(id)并返回一个输出值(UserEntity对象)。通过将代码从处理程序抽象到公共管道中,这可以使你的代码干净可读。
The built-in ValidationPipe 内置的验证管道
幸运的是你不需要构建这些管道,Nest提供了开箱即用的
ValidationPipe
和
ParseUUIDPipe
管道。(记住
ValidationPipe
这个管道需要clas-validator和class-transform两个依赖,确保他们已经安装)。
内置的
ValidationPipe
管道提供了比此章节描述更多的选项,你可以在这里找到很多例子。
其中一个选项是
transform
。回忆一下关于关于反序列化纯js对象的数据的讨论(没有DTO类型)。迄今为止,我们已经使用管道来验证负载数据,你可能还记得,我们使用class-transform来临时将对象转换为一个类型对象才能做验证。内置的
ValidationPipe
也可以,可选的返回转换过的对象。只需要转入一个配置对象给管道。传入一个带有transform字段的配置对象,值为true即可。
// cats.controller.ts
@Post()
@UsePipes(new ValidationPipe({ transform: true }))
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto)
}
Hints
import { ValidationPipe } from '@nestjs/common'
因为管道基于class-validator和class-transformer库,还有许多额外的配置。就像上面提到的transform,你可以通过将配置对象传入管道来配置这些这只。下面是内置的选项:
export interface ValidationPipeOptions extends ValidatorOptions {
transform?: boolean;
disableErrorMessage?: boolean;
exceptionFactory?: (error: ValidationError[]) => any;
}
此外,这里有所有的class-validator可用项(继承自
ValidatorOptions
接口)
选项 |
类型 |
描述 |
|---|---|---|
| skipMissingProperties | boolean | 如果设置为true,验证器会跳过验证所有的没传的属性 |
| whilelist | boolean | 如果设置为true,验证器会剥离所有没有使用验证装饰器的验证对象的属性 |
| forbidNonWhitelisted | boolean | 如果设置为true,验证器会抛出异常而不是剥离非白名单的属性 |
| forbidUnknownValues | boolean | 如果设置为true,尝试验证位置对象会立刻失败 |
| disableErrorMessages | boolean | 如果设置为true,验证的错误信息不会返回给客户端 |
| exceptionFactory | Function | 接受一个验证错误信息的数组,并返回一个异常对象来抛出 |
| groups | string[] | 验证对象期间可以成组使用 |
| dismissDefaultMessages | boolean | 如果设置为true,验证将不会使用默认的信息。错误信息如果没有明确的设置,始终会是undefined |
| validationError.target | boolean | 表示是否应该在ValidationError中暴露目标 |
| validationError.value | boolean | 表示是否应该在ValidationError中暴露验证的值 |
Hints
关于class-validator包的仓库可以找到更多信息。
后记
原文地址: docs.nestjs.com/pipes
关于本文
- 文章非复制黏贴,经浏览文档,以自己的理解进行,代码测试,手打书写。本篇为翻译+意译。
- 用作记录自己曾经学习、思考过的问题的一种笔记。
- 用作前端技术交流分享。
- 阅读本文时欢迎随时质疑本文的准确性,将错误的地方告诉我。本人会积极修改,避免文章对读者的误导。
关于我
- 是一只有梦想的肥柴。
- 觉得算法、数据结构、函数式编程、js底层原理等十分有趣的小前端。
- 志同道合的朋友请关注我,一起交流技术,在前端之路上共同成长。
- 如对本人有任何意见建议尽管告诉我哦~ 初为肥柴,请多多关照~
- 前端路漫漫,技术学不完。今天也是美(diao)好(fa)的一天( 跪了...orz