管道
管道 (Pipes)
是一种用于处理输入数据的机制。它允许你对进入应用程序的数据进行转换、验证和转变。
管道组件和函数式编程的管道函数理念一致,特定的输入,数据经过中间处理,返回固定的输出。
管道有下面经典的应用场景:
- 数据转换:将输入的数据转换成为你需要的数据输出,比如字符串转数字。
- 数据验证:对输入的数据进行校验,验证通过继续传递,验证失败则会抛出异常。
- 数据清理:从数据中删除任何无效的内容或过滤掉不需要的数据。
管道的调用时机在请求进入到路由方法之前,管道会拦截路由方法的参数,进行处理之后,会继续传递数据。
实际上通过参数装饰器,对数据处理之后,使用 Reflect.defineMetadata
储存起来,在进入路由方法时,继续传递下去执行。
内置管道
Nest
内置了几个开箱即用的管道:
ParseIntPipe
:校验、转换数据为Number
类型。ParseFloatPipe
:校验、转换数据为Float
类型。ParseBoolPipe
:校验、转换数据为Boolean
类型。ParseArrayPipe
:校验、转换数据为Array
类型。ParseUUIDPipe
:校验、转换数据为UUID
类型。ParseEnumPipe
:校验、转换数据为Enum
类型。ValidationPipe
:结构验证,有类结构和对象结构的验证,可对DTO
进行属性限制验证。DefaultValuePipe
:提供默认值,当接收到null
或undefined
时,程序可能会抛出异常,为了程序正常运行,给参数提供默认值。
以上的管道,如果校验、转换失败,则会抛出类型不符的内置异常。
使用管道
本节介绍 ParseIntPipe
、ParseArrayPipe
、ParseEnumPipe
、DefaultValuePipe
、ValidationPipe
其余的用法大致和 ParseIntPipe
用法类似。
HTTP
在数据传输过程中,query
、 param
参数是以键值对的字符串进行传输的,body
请求体支持文本数据(json、html、xml)
、二进制数据、表单数据。在进行数据库查询时,数据类型不一致也会查不出来,所以需要对传输的参数进行校验和转换。
这时管道的作用就体现出来了,它能帮助我们对 HTTP
传输的数据进行校验和转换。
管道函数内部实现了一个 transform
方法,对每个内置的管道类型,进行不同类型的校验、转换。
定义一组 mock
数据,方便后续测试:
点击查看代码 src/pipes/constants/user.ts
export enum Gender {
MALE = 'male',
FEMALE = 'female'
}
export const users = [
{
id: 1,
name: '王洋',
gender: Gender.MALE
},
{
id: 2,
name: '李秀英',
gender: Gender.FEMALE
},
{
id: 3,
name: '杨敏',
gender: Gender.FEMALE
},
{
id: 4,
name: '吴静',
gender: Gender.FEMALE
},
{
id: 5,
name: '黄敏',
gender: Gender.FEMALE
}
]
绑定管道函数,使其工作在参数中。
import { Controller, Get, Param, ParseIntPipe } from '@nestjs/common'
import { PiepsService } from './pieps.service'
@Controller('pipes')
export class PipesController {
constructor(private readonly piepsService: PiepsService) {}
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
console.log(typeof id)
return this.piepsService.findOne(id)
}
}
import { Injectable } from '@nestjs/common'
import { users } from './constants/user'
@Injectable()
export class PiepsService {
/**
* 查询用户信息
* @param id
*/
findOne(id: number) {
return users.find((item) => item.id === id)
}
}
当用户请求接口的时候,如果不满足我们预期的数据结构,将会提前返回错误信息给用户。
当满足预期的数据时,会通过校验并返回数据:
数据转化
在某些时候,前后端约定数据结构传递的时候,对于 Get
操作的接口,不太喜欢用数组接收,而是传递一个以逗号分隔的字符串(传输方便,转换不容易出错),这种场景就可以使用自定义管道将参数转化为数组处理。
所有的自定义管道函数都需要实现 PipeTransform
接口:
import { ArgumentMetadata, PipeTransform } from '@nestjs/common'
export class ListTransformPipe implements PipeTransform<string, Array<string>> {
transform(value: any, metadata: ArgumentMetadata) {
return value.split(',').filter(Boolean)
}
}
value
是当前处理的参数数值metadata
是当前处理的参数的元数据,元数据类型如下
export interface ArgumentMetadata {
type: 'body' | 'query' | 'param' | 'custom'
metatype?: Type<unknown>
data?: string
}
type | 接收参数的是什么类型的参数装饰器,比如 @Body 、@Query 、@Param |
---|---|
metadata | 处理参数的元类型JavaScript数据类型 ,即 @Query('id') id:number 的 id:number 所描述的类型(JS 原始数据类型)。 |
data | 传递给参数装饰器的提取参数字符串,例如 @Query('id') 的 id ,如果为空则是 undefined 。 |
在对应方法处挂载使用自定义管道:
import { Controller, Get, Query } from '@nestjs/common'
import { ListTransformPipe } from './pipe/list.transform.pipe'
@Controller('pipes')
export class PipesController {
@Get('/ids')
transformIds(@Query('ids', ListTransformPipe) ids: string[]) {
return {
data: ids
}
}
}
结构验证
在数据转化例子中,自定义的管道与用户传递的数据强耦合,自定义管道函数复用性极低。如果编写一个通用的校验函数呢?这显然是做不到创建一个函数,就能涵盖整个应用校验参数的情况。
Dto
数据传输对象,是一种以类的形式,实际为数据的对象。用于前后端或程序间的传输。例如有一个用户对象:
export class user {
id: string
name: string
email: string
}
TIP
在创建 Dto
时,需要注意它必需作为类而非接口实现。
这是因为 TypeScript
接口仅用于编译时的类型检查,由于使用 class-validator
、class-transformer
在运行时处理验证逻辑,所以 Dto
只能作为类存在而不是接口。
如果将验证参数是否合法的过程放在控制器的具体方法中,违反了单一任务原则,管道参数校验的函数复用性极低。
可以通过对象模式验证、类验证器来处理转换、验证的过程。这是一种在 DTO
对象中添加校验规则,只需要使用一种管道验证 ValidationPipe
就可以重复校验。
dto
学习更多关于 dto
类型,请点击 这里。
JOI
JOI
是用于检测数据类型是否合法的组件,直接创建校验模型 Schema
pnpm i joi -w
pnpm i joi -D -w @types/joi
添加一个用于验证 userDto
的模型:
import * as Joi from 'joi'
export const UserSchema = Joi.object({
name: Joi.string().required(),
id: Joi.string().required(),
email: Joi.string().required()
})
创建一个 JOI
的管道验证参数:
import { ArgumentMetadata, BadRequestException, Injectable, PipeTransform } from '@nestjs/common'
import { ObjectSchema } from 'joi'
@Injectable()
export class JoiPipe implements PipeTransform {
constructor(private schema: ObjectSchema) {}
transform(value: any, metadata: ArgumentMetadata) {
const { error } = this.schema.validate(value)
if (error) {
throw new BadRequestException(`${error.details.map((d) => d.message).join('\n')} \n 验证失败`)
}
return value
}
}
为控制器添加一个 post
的方法,接收参数并进行校验:
import { Body, Controller, Post, UsePipes } from '@nestjs/common'
import { JoiPipe } from './pipe/joi.pipe'
import { UserSchema } from './schema/user.schema'
@Controller('pipes')
export class PipesController {
/**
* joi自定义校验
* @param body
*/
@Post('joi')
joiValidatorHandle(@Body(new JoiPipe(UserSchema)) body: UserDto) {
return {
message: 'received',
data: body
}
}
}
Class-Validator
还有一种类验证方法,相对于 JOI
的链式声明数据类型,它采用了装饰器的方式来声明数据类型。
TIP
由于使用了装饰器,假如你的 Nest
项目使用的 JavasScript
,该技术则不可用。
我们需要安装必需的包:
pnpm i -S class-validator class-transformer
安装完之后,我们在 UserDto
类中,添加一些用于校验类型的装饰器。
import { IsString } from 'class-validator'
export class UserDto {
@IsString()
name: string
@IsString()
id: string
@IsString()
email: string
}
创建一个验证的管道函数:
import { ArgumentMetadata, HttpException, HttpStatus, PipeTransform } from '@nestjs/common'
import { plainToInstance } from 'class-transformer'
import { validate } from 'class-validator'
export class ClassValidatePipe implements PipeTransform {
async transform(value: any, metadata: ArgumentMetadata): Promise<any> {
// 1. 根据DTO类,借助 class-transformer 插件实例化对象
const userDto = plainToInstance(metadata.metatype, value)
// 2. 根据DTO类属性的 装饰器验证规则rules,获得一组特殊的Dto对象,它们包含验证rules,然后对这组数据进行校验
const errors = await validate(userDto)
// 3. 如果errors长度为0,说明所有验证通过,否则校验失败,返回错误信息给前端
if (errors.length) {
throw new HttpException(
errors.map((item) => Object.values(item.constraints)[0]),
HttpStatus.BAD_REQUEST
)
}
return value
}
}
为控制器的参数添加校验:
import { Body, Controller, Post } from '@nestjs/common'
import { UserDto } from './dto/user.dto'
import { ClassValidatePipe } from './pipe/class-validate.pipe'
@Controller('pipes')
export class PipesController {
/**
* class-validator自定义校验
* @param body
*/
@Post()
classValidatorHandle(@Body(new ClassValidatePipe()) body: UserDto) {
return {
message: 'received',
data: body
}
}
}
TIP
joi
和 class-validator
都可以对数据进行校验、转化,
ValidationPipe
我们学习了 joi
、class-validator
的使用。对于 class-validator
无需自定义验证方法,Nest
提供了开箱即用的 ValidationPipe
管道验证。它提供了一种方便的方法,通过在每个模块的 本地类/DTO 声明中使用简单的注解来强制执行所有传入客户端数据的验证规则。
自动验证
我们将全局使用 ValidationPipe
,它将会自动验证所有使用 class-validator
的类。
上面的 class-validator 例子,我们可以不用自定义的 ClassValidatePipe
方法,而去绑定内置的校验管道函数:
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
import { ValidationPipe } from '@nestjs/common'
async function bootstrap() {
const app = await NestFactory.create(AppModule)
// 使用内置验证管道
// 大部分管道验证函数都会注册为全局管道
app.useGlobalPipes(new ValidationPipe())
await app.listen(3000)
}
bootstrap()
绑定位置
@UsePipes(new JoiPipe(UserSchema))
与之前使用管道的方式略微不同。管道的使用根据作用域的不同,有不同的使用方式:
全局作用域、全局依赖注入作用域、控制器作用域、方法作用域、参数作用域
参数作用域直接将管道函数绑定在参数上,如果需要传递参数给管道函数,则需要使用 new
的方式:
// 1.直接绑定
@Get('/ids')
transformIds(@Query('ids', ListTransformPipe) ids: string[]) {
// ...
}
// 2.new传递参数
@Post('joi')
joiValidatorHandle(@Body(new JoiPipe(UserSchema)) body: UserDto) {
// ...
}
全局作用域、控制器作用域、方法作用域都使用 @UsePipes
装饰器:
// 1.全局作用域
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new JoiPipe(UserSchema));
// ...
}
// 2.控制器作用域
@UsePipes(new JoiPipe(UserSchema))
@Controller('pipes')
export class PipesController {}
// 3.方法作用域
@Post('joi')
joiValidatorHandle(@Body(new JoiPipe(UserSchema)) body: UserDto) {
// ...
}
依赖注入
app.useGlobalPipe
的方式无法使用依赖注入的服务。我们可以在模块中使用依赖注入的方式,这样我们的自定义管道也能使用依赖服务做一些事情,比如在管道中查询数据库,判断是否存在该用户的 id。学习更多依赖注入点击这里。
import { Module } from '@nestjs/common'
import { APP_PIPE } from '@nestjs/core'
@Module({
providers: [
{
provide: APP_PIPE,
useClass: ValidationPipe
},
AppService
]
})
export class AppModule {}
这样注册后,在 ValidationPipe
方法中就能使用 AppService
这个注入的服务。
多重管道
管道函数作为装饰器,多个叠加使用,同样会有顺序的问题。管道函数的颗粒度最细可以控制到参数,在执行顺序上,依次是全局、控制器、方法和参数。
多个同类型的管道(指的是多个参数管道,或者多个控制器管道等等)绑定使用,Nest
会从左到右依次执行。
@Post('multi-pipe')
multiPipe(
@Body('ids', SplitTransformPipe, TransformArrayItemToIntPipe) ids: number[],
) {
return {
data: ids,
};
}
export class SplitTransformPipe implements PipeTransform<string, Array<string>> {
transform(value: any, metadata: ArgumentMetadata) {
return value.split(',').filter(Boolean)
}
}
export class TransformArrayItemToIntPipe implements PipeTransform<string, Array<string>> {
transform(value: any, metadata: ArgumentMetadata) {
console.log('previous pipe passed params', value)
return value.map(Number)
}
}
明显看到在 TransformArrayItemToIntPipe
管道接收上一个管道处理后的参数值
验证了Nest
会从左到右依次执行多个同类型的装饰器。
案例 1:结合 Dto
校验参数
使用 class-vaildator
、class-transformer
插件,对用户传递的用户信息进行校验。
定义 userDto
:
// src/user/dto/create-user.dto.ts
import { IsString, IsNotEmpty, Matches } from 'class-validator'
export class CreateUserDto {
@IsString()
@IsNotEmpty({
message: '姓名不能为空'
})
name: string
@IsNotEmpty({ message: '手机号不能为空' })
@Matches(/^1[3456789]\d{9}$/, {
message: '手机号格式不正确'
})
phone: string
}
接着定义管道校验函数 Validate
:
// src/common/pipes/validation.pipe.ts
import { ArgumentMetadata, Injectable, PipeTransform, BadRequestException } from '@nestjs/common'
import { validate } from 'class-validator'
import { plainToClass } from 'class-transformer'
@Injectable()
export class ValidationPipe implements PipeTransform {
async transform(value: any, { metatype }: ArgumentMetadata) {
if (!metatype || !this.toValidate(metatype)) {
// 如果没有传入验证规则,则不验证,直接返回数据
return value
}
// 将对象转换为 Class 来验证
const object = plainToClass(metatype, value)
const errors = await validate(object)
console.log(errors)
if (errors.length > 0) {
const msg = Object.values(errors[0].constraints)[0] // 只需要取第一个错误信息并返回即可
throw new BadRequestException(`Validation failed: ${msg}`)
}
return value
}
private toValidate(metatype: any): boolean {
const types: any[] = [String, Boolean, Number, Array, Object]
return !types.includes(metatype)
}
}
全局进行注册:
// src/main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule)
app.useGlobalPipes(new ValidationPipe())
await app.listen(3000)
}
bootstrap()
在控制器中使用 dto
作为 body
参数的类型:
// src/user/user.controller.ts
@Controller('user')
export class UserController {
/**
* 创建用户
* @param body
*/
@Post()
create(@Body() body: CreateUserDto) {
return `this action will adds a new user, user info is ${JSON.stringify(body)}`
}
}
当用户发送不满足 dto
校验的类型,返回相关的错误信息:
当请求携带满足预期的参数,会正确响应结果:
总结
- 管道的定位是做数据的转化和校验的。
- 可以结合
dto
,配合class-validator、class-transformer
完成结构验证。 - 管道的执行顺序是全局管道->控制器管道->方法管道->参数装饰器管道。
- 同个类型的管道叠加执行顺序是从左到右。