🎄Hi~ 大家好,我是小鑫同学,资深 IT 从业者,InfoQ 的签约作者,擅长前端开发并在这一领域有多年的经验,致力于分享我在技术方面的见解和心得
🚀技术&代码分享
- 我在 94Code 总结技术学习;
- 我在 1024Code 在线编写代码;
- 我在 Github 参与开源学习;
😇推荐几个好用的工具
- var-conv 适用于VSCode IDE的代码变量名称快速转换工具
- generator-vite-plugin 快速生成Vite插件模板项目
- generator-babel-plugin 快速生成Babel插件模板项目
进入正题
在 Nestjs 中管道是具有 @Injectable()
装饰器且已实现 PipeTransform
接口的类。
管道(Pipe)的作用
管道(Pipe)作用在每个控制器的处理方法上,也就是当每一个请求被路由到具体的控制器的方法后会先通过管道(Pipe)对传入的请求参数进行 转换 和 验证,保证数据在被正式处理前是完全合法的。
管道(Pipe)的使用
Nestjs 中内置了下列的9个管道,利用这些管道可以轻松的验证路由参数、查询参数和请求正文是否合法,下面通过两个例子一起看一下管道的使用。
ParseIntPipe | ParseFloatPipe |
ParseBoolPipe | ParseArrayPipe |
ParseUUIDPipe | ParseEnumPipe |
ParseFilePipe | |
DefaultValuePipe | ValidationPipe |
findUserById
是用来根据用户 ID 获取用户信息的处理函数,期望id
由客户端传来的必须是数字类型。
@Controller('users')
export class UsersController {
@Get(':id')
findUserById(@Param('id') id: number): string {
return `The ID of this user is ${id}`;
}
}
现在由于缺少对路由参数类型的校验,此时客户端在传递非数字类型的ID时并不会收到合理的提醒,这样很容易造成服务端业务逻辑的异常,有入库的操作的话还会造成垃圾数据。所以可将 ParseIntPipe
管道类直接添加到 @Param()
装饰器的第二位参数,如下图:
@Controller('users')
export class UsersController {
@Get(':id')
findUserById(@Param('id', ParseIntPipe) id: number): string {
return `The ID of this user is ${id}`;
}
}
增加 ParseIntPipe
管道的限制后,当客户端再次传递非数字类型的ID时就会收到对应的提示。
上面的例子中使用了管道类而非管道的实例是因为 Nestjs 基于 IoC 的设计在框架内部可以自动对类进行实例化操作,管道同时也支持通过构造函数传递选项的方式自定义内置管道的行为。
下面这个 findUserByUUID
函数中使用的 ParseUUIDPipe
管道默认情况下是支持接收不同版本的 UUID 的,但在例子中我们限制只可以接收 v5 版本的 UUID,就需要实例化 ParseUUIDPipe
并在构造函数中指定具体的 version
。
@Get(':uuid')
findUserByUUID(
@Param('uuid', new ParseUUIDPipe({ version: '5' })) uuid: string,
): string {
return `The UUID of this user is ${uuid}`;
}
基于 schema 的验证
在 createUser
处理函数中要求客户端传递一份包含 name
、age
和 gender
的数据,对于这种复杂的数据结构来说可以引入 schema
(前端表单校验常用技术)来配合自定义管道实现。
export class CreateUserDto {
name: string;
age: number;
gender: boolean;
}
@Post()
createUser(@Body() createUserDto: CreateUserDto): string {
return `${createUserDto.name} is the 100th user`;
}
首先需要引入 joi 模块和 @types/joi
模块,使用 ES 模块导入的方式导入 joi 时需要在 tsconfig.json
中启用 esModuleInterop
选项。接着使用 Joi 模块将 CreateUserDto 中的三个属性均设置为必填项。
import Joi from 'joi';
export const createUserSchema = Joi.object({
name: Joi.string().required(),
age: Joi.number().required(),
gender: Joi.bool().required(),
});
定义完 schema
后可以使用 nest g pi joi-validation
创建一个公共的管道,在 transform
函数中使用已经注入的ObjectSchema
对象提供的 validate
函数对请求参数 value
做验证,当验证不通过是抛出合理的异常,反之通过。
@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;
}
}
这里的管道就需要绑定到 createUser
处理函数级别了,需要用到 @UsePipes()
装饰器,并传入通过 Joi 定义的 schema
。
@Post()
@UsePipes(new JoiValidationPipe(createUserSchema))
createUser(@Body() createUserDto: CreateUserDto): string {
return `${createUserDto.name} is the 100th user`;
}
当客户端未传递其中某一个字段时就会收到如下的提示信息。
基于 dto 的验证
在基于 schema 的验证中不仅编写了通用的 joi-validation
管道,还用 Joi 库编写了一份和 CreateUserDto
几乎一样的 schema
文件,每当 DTO 文件有变更时就需要同步维护 schema
文件。
基于 dto 的验证就可以利用为已创建的 CreateUserDto
增加验证相关的装饰器并配合通过的管道即可完成,从而可以少维护一份文件,避免不一致造成的问题。
首先执行 npm i --save class-validator class-transformer
安装必要的模块,接着为 CreateUserDto
增加验证相关的装饰器。
import { IsString, IsNumber, IsBoolean, IsNotEmpty } from 'class-validator';
export class CreateUserDto {
@IsString()
@IsNotEmpty()
name: string;
@IsNumber()
@IsNotEmpty()
age: number;
@IsBoolean()
@IsNotEmpty()
gender: boolean;
}
接着执行 nest g pi dto-validation
创建一个公共的管道,在这个管道中需要做这么几件事情:
- 解构 metadata 参数,获取请求体参数的元类型。
- 定义私有函数
toValidation
,跳过非DTO的类型(非Javascript原类型)。 - 使用
plainToInstance
将元类型和请求体参数转为可验证的类型对象。 - 通过
validate
函数执行校验,校验未通过则抛出合理的异常信息。
import {
ArgumentMetadata,
BadRequestException,
Injectable,
PipeTransform,
} from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToInstance } from 'class-transformer';
@Injectable()
export class DtoValidationPipe implements PipeTransform {
async transform(value: any, metadata: ArgumentMetadata) {
const { metatype } = metadata;
if (!metatype || !this.toValidation(metatype)) {
return value;
}
const object = plainToInstance(metatype, value);
const errors = await validate(object);
if (errors.length > 0) {
throw new BadRequestException('Validation failed');
}
return value;
}
/**
* 当 metatype 所指的参数的元类型仅为Javascript原生类型的话则跳过校验,这里只关注了对定义的DTO的校验
*/
private toValidation(metatype: Function): boolean {
const types: Function[] = [String, Boolean, Number, Array, Object];
return !types.includes(metatype);
}
}
再接着将 DtoValidationPipe
管道绑定到 createUser
处理方法并作验证。
@Post()
createUser(
@Body(new DtoValidationPipe()) createUserDto: CreateUserDto,
): string {
return `${createUserDto.name} is the 100th user`;
}
PS:Nestjs 提供的 ValidationPipe 管道可以完全支持上述两种验证方式,我们不必为自定义验证管道花费时间。
提供默认值
提供默认值可以看做是管道在转换场景的一个体现,增加默认值的处理可以使得服务端的代码更加的健壮。这里使用到了内置的 DefaultValuePipe
管道。
@Get()
findAllUsers(
@Query('activeOnly', new DefaultValuePipe(false), ParseBoolPipe)
activeOnly: boolean,
@Query('page', new DefaultValuePipe(10), ParseIntPipe) page: number,
): string {
return `This action return all users,request parameters:activeOnly: ${activeOnly},page:${page}`;
}
全局管道注册
除上述管道的注册位置,还支持全局注册,注册方式同全局异常过滤器的注册,一个是基于 app
实例的注册,另一个是基础跟模块的注册。
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();
import { Module } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_PIPE,
useClass: ValidationPipe
}
]
})
export class AppModule {}
总结
以上就是 Nest 中管道类的使用方式,也是保证参数正常接收、正常入库的必要手段。
如果看完觉得有收获,欢迎点赞、评论、分享支持一下。你的支持和肯定,是我坚持写作的动力~