在之前的搭建CRUD代码内容中,我们提到过管道配合class-validator做验证,这一节我们来看一下什么是管道,以及验证的一个流程。
我们先通过nest g res phones --no-spec生成一个新的模块并将其controller与service中的预置内容清空掉,至于entity与dto暂时先不关注,我们先通过简单示例来说一下管道。
初始代码内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import { Controller, Get, Post, Body, Patch, Param, Delete, } from '@nestjs/common' import { PhonesService } from './phones.service' import { CreatePhoneDto } from './dto/create-phone.dto' import { UpdatePhoneDto } from './dto/update-phone.dto' @Controller ('phones' )export class PhonesController { constructor (private readonly phonesService: PhonesService ) {} }
1 2 3 4 5 6 7 import { Injectable } from '@nestjs/common' import { CreatePhoneDto } from './dto/create-phone.dto' import { UpdatePhoneDto } from './dto/update-phone.dto' @Injectable ()export class PhonesService {}
管道 管道有两个典型的应用场景:
转换 :管道将输入数据转换为所需的数据输出(例如,将字符串转换为整数)
验证 :对输入数据进行验证,如果验证成功继续传递; 验证失败则抛出异常
Nest 自带九个开箱即用的管道,即
ValidationPipe
ParseIntPipe
ParseFloatPipe
ParseBoolPipe
ParseArrayPipe
ParseUUIDPipe
ParseEnumPipe
DefaultValuePipe
ParseFilePipe
转换管道 ParseIntPipe 先来看一个用法简单的管道ParseIntPipe,我们如果想要在方法参数级别绑定管道,实现管道与特定的路由处理程序方法相关联,并确保它在该方法被调用之前运行,可以通过如下方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import { Controller, Get, Post, Body, Patch, Param, Delete, ParseIntPipe, } from '@nestjs/common' import { PhonesService } from './phones.service' import { CreatePhoneDto } from './dto/create-phone.dto' import { UpdatePhoneDto } from './dto/update-phone.dto' @Controller ('phones' )export class PhonesController { constructor (private readonly phonesService: PhonesService ) {} @Get ('findBy/:ram' ) findByRam (@Param ('ram' , ParseIntPipe) ram: number ) { return { ram, ramType : typeof ram, } } }
接下来调试接口:
1 2 3 4 5 { "ram" : 8 , "ramType" : "number" }
我们传递了一个param参数,毫无疑问ram参数是一个字符串,但我们得到的结果,ram参数是一个number,这就是ParseIntPie的能力。
如果不能转换为数值会怎么样呢?
1 2 3 4 5 6 { "statusCode" : 400 , "message" : "Validation failed (numeric string is expected)" , "error" : "Bad Request" }
答案是会抛出一个错误,将abc转换为数值本就不可能的。截一段ParseIntPipe的源码,内部transform的实现:
1 2 3 4 5 6 async transform (value, metadata ) { if (!this .isNumeric(value)) { throw this .exceptionFactory('Validation failed (numeric string is expected)' ); } return parseInt (value, 10 ); }
现在肯定有人有疑问,我们之前绑定内容都是用的@UserXXXX装饰器,为什么管道不是这样呢?其实管道绑定也有一个@UserPipes装饰器,我们后边会说,这里这么用因为比较方便,可以看一下@Param的类型声明就懂了:
1 2 export declare function Param (property: string , ...pipes: (Type<PipeTransform> | PipeTransform)[] ): ParameterDecorator ;
其他的一些转换管道其实是查不多的,我们来大概看一下。
ParseFloatPipe controller实现:
1 2 3 4 5 6 7 8 @Get ('parseFloat/:n' )parseFloat (@Param ('n' , ParseFloatPipe) n: number ) { return { n, dataType : typeof n, } }
调试接口测试:
1 2 3 4 5 { "n" : 1.2345 , "dataType" : "number" }
ParseBoolPipe controller实现:
pipe不仅可以应用于param,query和body装饰器也是可以的
1 2 3 4 5 6 7 8 @Get ('parseBool' )parseBool (@Query ('flag' , ParseBoolPipe) flag: boolean ) { return { flag, dataType : typeof flag, } }
调试接口:
1 2 3 4 5 { "flag" : false , "dataType" : "boolean" }
ParseArrayPipe controller实现:
1 2 3 4 5 6 7 8 @Get ('parseArray' )parseArray (@Query ('array' , ParseArrayPipe) array: string [] ) { return { array, dataType : typeof array, } }
调试接口:
1 2 3 4 5 6 7 8 9 10 11 { "array" : [ "a" , "b" , "c" , "d" , "e" ], "dataType" : "object" }
不难发现,ParseArrayPipe默认是以,分割字符串实现的。
可以以其他字符来分割吗?当然可以,看一下其声明文件就能发现了,我们可以传入配置项,不过这样的话我们就要注入一个实例,而不是一个类了。
1 2 3 4 5 6 7 8 9 10 @Get ('parseArray' )parseArray ( @Query ('array' , new ParseArrayPipe({ separator: '.' })) array: string [], ) { return { array, dataType : typeof array, } }
以.分割字符调试接口:
1 2 3 4 5 6 7 8 9 10 11 { "array" : [ "a" , "b" , "c" , "d" , "e" ], "dataType" : "object" }
能得到同样的响应内容。
ParseUUIDPipe ParseUUIDPipe应该算是转换管道还是验证管道呢?看一下其transform方法实现能发现,如果参数不是uuid会抛出错误,如果是uuid则将这个值返回,看起来好像是一个验证管道哦。不过既然命名为ParseXXXXPipe应该还是转换管道的。是否可以理解为,将字符串转换为uuid,失败则抛出错误。
1 2 3 4 5 6 7 async transform (value, metadata ) { if (!this .isUUID(value, this .version)) { throw this .exceptionFactory(`Validation failed (uuid${this .version ? ` v ${this .version} ` : '' } is expected)` ); } return value; }
很简单,就不演示了吧。
ParseEnumPipe parseEnumPipe用来判断你传入的参数是否是一个枚举值,我们先创建一个枚举:
1 2 3 4 5 enum Brand { APPLE = 'apple' , HUAWEI = 'huawei' , SAMSUNG = 'samsung' , }
controller实现:
1 2 3 4 5 6 7 @Get ('parseEnum' )parseEnum (@Query ('brand' , new ParseEnumPipe(Brand)) brand: string ) { return { brand, dataType : typeof brand, } }
调试接口:
1 2 3 4 5 { "brand" : "samsung" , "dataType" : "string" }
samsung确实是我们的一个枚举值,如果我们传入一个错误的值呢?
1 2 3 4 5 6 { "statusCode" : 400 , "message" : "Validation failed (enum string is expected)" , "error" : "Bad Request" }
这就是ParseEnumPipe的用途。
自定义管道 每个管道必须要实现的泛型接口PipeTransform<T, R>,泛型 T 表明输入的 value 的类型,R 表明 transfrom() 方法的返回类型。
1 2 3 4 5 6 7 8 9 10 export interface PipeTransform<T = any, R = any> { transform(value: T, metadata : ArgumentMetadata): R; }
不难发现每个管道都要实现transform方法,该方法接受两个参数:
value:当前处理的方法参数
metadata:当前处理的方法参数的元数据
什么是元数据呢?可以看一下ArgumentMetadata的类型声明:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 export interface ArgumentMetadata { readonly type : Paramtype; readonly metatype?: Type<any > | undefined ; readonly data?: string | undefined ; }
上边的内容可能看起来不太容易懂,来一个小demo看一下就好了
我们新建double.pipe.ts来创建一个管道:
1 2 3 4 5 6 7 8 9 10 11 12 13 import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common' @Injectable ()export class DoublePipe implements PipeTransform { transform (value: any , metadata: ArgumentMetadata ) { console .log('value:' , value) console .log('type:' , metadata.type) console .log('metatype:' , metadata.metatype) console .log('data:' , metadata.data) return value } }
在phones.controller.ts中应用这个管道:
1 2 3 4 5 @Get ('diyPipe' )diyPipe (@Query ('num' , DoublePipe) num: string ) { return 'success' }
现在我们访问接口http://localhost:3000/nest/phones/diyPipe?num=1234。此时我们看一下控制台的打印内容:
1 2 3 4 value: 1234 type: query metatype: [Function: String ]data: num
我们依次来看:
value:就是我们通过query传递的实际内容
type:因为我们使用`@Query装饰器,所以type是query
data:我们给@Query装饰器传入了'num',所以data是num
metatype:为什么是[Function: String],以为我们在controller给num参数声明类型为string。
现在我们知道参数都是什么了,那我们让这个管道,实现将传入内容乘以2的效果:
我们这里没有考虑各种条件,而是直接乘以2返回了,如果想做好,可以参考ParseIntPipe的实现
1 2 3 4 5 6 7 8 9 10 import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common' @Injectable ()export class DoublePipe implements PipeTransform { transform (value: any , metadata: ArgumentMetadata ) { return parseInt (value, 10 ) * 2 } }
在controller中应用:
1 2 3 4 5 6 7 @Get ('diyPipe' )diyPipe (@Query ('num' , DoublePipe) num: string ) { return { double : num, } }
现在调试接口:
验证管道 在之前的使用MySql搭建CRUD代码的文章中我们在最后提了一嘴dto验证,现在我们学过了管道的知识再来看一下验证管道的内容。
在之前我们是实现了一个自定义管道,其实Nestjs有内置的ValidationPipe,这一次我们就使用内置管道来实现验证功能。
由于此管道使用了 class-validator 和 class-transformer 库,所以需要先安装依赖:
1 $ npm i --save class-validator class-transformer
ValidationPipe的构造函数接受一个可选的配置项:
1 2 3 4 5 6 7 8 9 10 11 export interface ValidationPipeOptions extends ValidatorOptions { transform?: boolean ; disableErrorMessages?: boolean ; transformOptions?: ClassTransformOptions; errorHttpStatusCode?: ErrorHttpStatusCode; exceptionFactory?: (errors: ValidationError[] ) => any ; validateCustomDecorators?: boolean ; expectedType?: Type<any >; validatorPackage?: ValidatorPackage; transformerPackage?: TransformerPackage; }
继承自ValidatorOptions的配置项还有很多,声明的是class-validator可用选项,关于那些请查看class-validator文档。
我们将管道绑定到全局,这样方便我们演示:
1 2 3 4 5 6 7 async function bootstrap ( ) { const app = await NestFactory.create(ApplicationModule); app.useGlobalPipes(new ValidationPipe()); await app.listen(3000 ); } bootstrap();
验证Body 我们创建create-phone.dto.ts:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import { IsString, IsNotEmpty, IsOptional, IsNumber, IsBoolean, } from 'class-validator' export class CreatePhoneDto { @IsNotEmpty ({ message : '不能为空' }) @IsString ({ message : 'name为string类型' }) name : string @IsNotEmpty ({ message : '不能为空' }) @IsString ({ message : 'name为string类型' }) brand : string @IsOptional () @IsNumber ({ allowNaN : false }, { message : 'ram为number' }) ram : number @IsOptional () @IsNumber ({ allowNaN : false }, { message : 'ram为number' }) rom : number @IsOptional () @IsBoolean ({ message : 'isActive是boolean类型' }) isActive : boolean }
在controller中创建一个路由程序处理请求:
1 2 3 4 5 @Post ('create' )createPhone (@Body () createPhoneDto: CreatePhoneDto ) { return createPhoneDto }
我们先通过一个不合规的数据请求接口:
1 2 3 4 5 { "brand" : "Apple" , "ram" : 4 }
得到响应如下:
1 2 3 4 5 6 7 8 { "statusCode" : 400 , "message" : [ "name为string类型" , "不能为空" ], "error" : "Bad Request" }
这样我们就实现一个简单的dto验证了。
验证Param ValidationPipe不仅能对Body进行验证,其他请求数据也是可以的:
我们新建一个find-phone.dto.ts:
1 2 3 4 5 6 import { IsNumberString } from 'class-validator' export class FindPhoneDto { @IsNumberString () id : string }
在controller中应用:
1 2 3 4 5 @Get ('find/:id' )findById (@Param () findPhoneDto: FindPhoneDto ) { return findPhoneDto }
剥离属性 ValidationPipe 还可以过滤掉方法处理程序不应该接收的属性。在这种情况下,我们可以对可接受的属性进行白名单 ,白名单中不包含的任何属性都会自动从结果对象中删除。
先来看一下不使用白名单的情况来调用创建接口:
1 2 3 4 5 6 7 { "name" : "Iphone14" , "brand" : "Apple" , "ram" : 4 , "remark" : "color pink" }
由于我们的处理程序直接将接收到的内容返回了,所以响应如下:
1 2 3 4 5 6 { "name" : "Iphone14" , "brand" : "Apple" , "ram" : 4 , "remark" : "color pink" }
现在我们开启白名单功能:
1 2 3 4 5 6 7 8 9 10 11 async function bootstrap ( ) { const app = await NestFactory.create(AppModule) app.setGlobalPrefix('nest' ) app.useGlobalPipes( new ValidationPipe({ + whitelist: true , }), ) await app.listen(3000 ) }
此时同样发送请求得到的响应为:
1 2 3 4 5 { "name" : "Iphone14" , "brand" : "Apple" , "ram" : 4 }
可以发现,开启白名单之后,只会接收dto中定义的属性,其余无关属性会过滤掉。
映射类型 我们有了创建实例的dto,可能还会有一个用于更新的dto。在更新的时候我们可以复用create-phone.dto.ts中的内容,且更新过程中可能所有字段都是可选的,Nest 提供了 PartialType() 函数来让这个任务变得简单。
我们创建一个update-phone.dto.ts:
1 2 3 4 5 6 7 8 9 10 import { PartialType } from '@nestjs/mapped-types' import { IsNotEmpty } from 'class-validator' import { CreatePhoneDto } from './create-phone.dto' export class UpdatePhoneDto extends PartialType (CreatePhoneDto ) { @IsNotEmpty ({ message : '请传入phone的id' }) id : string }
现在我们执行更新操作时,传入的body必须有一个id属性,还可以拥有任何create-phone.dto.ts中定义的属性。
除了PartialType函数,Nest还提供了几个函数方便构建dto格式:
PickType():通过挑出输入类型的一组属性构造一个新的类型
OmitType() :通过挑出输入类型中的全部属性,然后移除一组特定的属性构造一个类型
IntersectionType() :将两个类型合并成一个类型
映射类型函数是可以组合使用的。