Note
管道
nestjs的管道介绍
管道
后端开发中的“管道”(Pipeline)本质上是指将多个处理步骤串联起来,让数据像水流一样依次通过各个节点进行处理的架构模式,常常会把前一个管道处理后的值传给下一个管道。而NestJS 的管道(Pipe)是在 Controller 执行之前生效,对传入的方法参数进行“检查”或“加工”。
❓与中间件、拦截器的区别
作用
- 转换数据:把入参转换成
Controller需要的类型 - 验证数据:判断入参是否合法,不合法直接抛出异常,不再执行
Controller方法
常用的内置管道
| 管道 | 主要作用 |
|---|---|
ValidationPipe |
根据 DTO 验证和转换对象 |
ParseIntPipe |
转换并验证整数 |
ParseFloatPipe |
转换并验证浮点数 |
ParseBoolPipe |
转换并验证布尔值 |
ParseArrayPipe |
转换并验证数组 |
ParseUUIDPipe |
验证 UUID |
ParseEnumPipe |
验证枚举值 |
DefaultValuePipe |
参数缺失时提供默认值 |
ParseFilePipe |
验证上传文件 |
ParseDatePipe |
转换并验证日期 |
ParseBoolPipe
@Get()
findAll(@Query('enabled', ParseBoolPipe) enabled: boolean) {
console.log(typeof enabled); // boolean
}
// 请求
GET /cats?enabled=true
// 原始数据
"true"
// 管道处理后
true
DefaultValuePipe
@Get()
findAll(
@Query('page',new DefaultValuePipe(1),ParseIntPipe,) page: number) {
return this.catsService.findAll(page);
}
// 请求,没有page参数
GET /cats
// 处理顺序
undefined
↓ DefaultValuePipe(1)
1
↓ ParseIntPipe
数字 1
↓
Controller
// 最终findAll收到参数1
ValidationPipe
是NestJS最核心的一个管道,它是NestJS内置的一个通用验证管道(从 @nestjs/common 引入),负责在 Controller 方法执行前读取 Controller 参数对应的 DTO,接着会调用 class-transformer 转换请求数据和调用 class-validator 执行 DTO 上的验证规则。在验证失败时抛出 HTTP 异常,成功后把处理后的数据传给 Controller。ValidationPipe 本身不是另一套验证规则库,它更像一个执行器和连接器,把NestJS的请求参数、DTO 、数据转换( class-transformer ) 和 数据验证( class-validator ) 串联起来,让 DTO 上写的装饰器规则能够在每次请求中自动生。
作用域
- 方法级:只作用于指定的方法
@Post()
// 只作用于create方法
@UsePipes(ValidationPipe)
create(@Body() dto: CreateCatDto) {}
- Controller级:只作用于CatsController中的路由
@Controller("cats")
// 只作用于CatsController控制器
@UsePipes(ValidationPipe)
export class CatsController {}
-
全局:最推荐的做法,处理整个应用中的路由参数
main.ts
// 全局管道,会作用于应用中的 Controller 和路由处理器
app.useGlobalPipes(
new ValidationPipe({
// 自动转换数据类型,一定开启。如将自动将普通请求对象转换成 DTO 实例,把 query 中的数字字符串转成 number
transform: true,
// 自动剔除请求中传入的 DTO 中没有定义的字段
whitelist: true,
// 当传递了DTO中不存在的字段,会抛出异常,而不是静默删除。可以不开启
forbidNonWhitelisted: true,
// 同一个字段一旦有一个校验规则失败,就不再继续检查该字段剩余的校验规则
stopAtFirstError: true,
}),
);
也可通过在任意模块中使用
APP_PIPES + useClass进行全局注册。
class-validator
是一个基于类和装饰器的数据验证库,可搭配validationPipe一起使用
常用装饰器
- 必填、可选、判空
| 装饰器 | 作用 |
|---|---|
@IsDefined() |
不能是 undefined 或 null |
@IsOptional() |
为 undefined 或 null 时跳过改字段的其他验证,即参数可选 |
@IsNotEmpty() |
不能为 ''、undefined 或 null ,即参数必填 |
@IsEmpty() |
必须为空 |
@Equals(value) |
必须严格等于指定值 |
@NotEquals(value) |
不能等于指定值 |
@IsIn(array) |
必须是指定数组中的一个值 |
@IsNotIn(array) |
不能是指定数组中的值 |
@Allow() |
没有其他验证规则时,允许字段通过白名单 |
export class UpdateUserDto {
@IsOptional()
@IsString()
@Length(2, 20)
username?: string;
}
// 表示:可以没有username,如果有,username必须是长度为 2~20 的字符串
- 基础类型验证
| 装饰器 | 作用 |
|---|---|
@IsString() |
必须是字符串 |
@IsBoolean() |
必须是布尔值 |
@IsNumber() |
必须是数字 |
@IsInt() |
必须是整数 |
@IsDate() |
必须是 Date 对象 |
@IsArray() |
必须是数组 |
@IsObject() |
必须是对象 |
@IsEnum(Enum) |
必须是枚举中的值 |
@IsInstance(Class) |
必须是指定类的实例 |
- 数字验证
| 装饰器 | 作用 |
|---|---|
@Min(value) |
数字不能小于指定值 |
@Max(value) |
数字不能大于指定值 |
@IsPositive() |
必须大于 0 |
@IsNegative() |
必须小于 0 |
@IsDivisibleBy(value) |
必须能被指定数字整除 |
- 字符串长度和内容验证
| 装饰器 | 作用 |
|---|---|
@Length(min, max) |
字符串长度在指定范围内 |
@MinLength(min) |
最小长度 |
@MaxLength(max) |
最大长度 |
@Contains(text) |
必须包含指定字符串 |
@NotContains(text) |
不能包含指定字符串 |
@Matches(regex) |
必须匹配正则表达式 |
@IsAlpha() |
只能包含字母 |
@IsAlphanumeric() |
只能包含字母和数字 |
export class RegisterDto {
@IsString()
@Matches(/^[a-zA-Z0-9_]+$/, {
message: "用户名只能包含字母、数字和下划线",
})
account: string;
}
- 常见格式验证
| 装饰器 | 作用 |
|---|---|
@IsEmail() |
邮箱格式 |
@IsURL() |
URL 格式 |
@IsUUID() |
UUID 格式 |
@IsIP() |
IP 地址格式 |
@IsJSON() |
JSON 字符串格式 |
@IsDateString() |
ISO 日期字符串 |
@IsNumberString() |
数字形式的字符串 |
@IsBooleanString() |
布尔形式的字符串 |
@IsMongoId() |
MongoDB ObjectId 字符串 |
@IsPhoneNumber() |
电话号码格式 |
@IsStrongPassword() |
强密码格式 |
- 数组验证
| 装饰器 | 作用 |
|---|---|
@ArrayNotEmpty() |
数组不能为空 |
@ArrayMinSize(n) |
数组至少有 n 项 |
@ArrayMaxSize(n) |
数组最多有 n 项 |
@ArrayContains(values) |
必须包含指定内容 |
@ArrayNotContains(values) |
不能包含指定内容 |
@ArrayUnique() |
数组元素不能重复 |
如果要验证数组中的每一个元素,对应装饰器中传入的object中设置 each :true 即可
export class CreateArticleDto {
@IsArray()
@ArrayNotEmpty()
// 校验数组每个元素是否为字符串
@IsString({ each: true })
tags: string[];
}
// 客户端传入以下内容会报错,因为第二项不是字符串
{
"tags": ["NestJS", 123]
}
export class CreateArticleDto {
@IsArray()
// 数组每个元素的字符长度不能超过20
@MaxLength(20, { each: true })
tags: string[];
}
- 条件验证和嵌套验证
| 装饰器 | 作用 |
|---|---|
@ValidateIf(callback) |
条件成立时才验证属性的全部校验规则 |
@ValidateNested() |
验证嵌套对象 |
@ValidatePromise() |
对 Promise 解析后的结果进行验证 |
export class CreateOrderDto {
@IsBoolean()
needInvoice: boolean;
@IsString()
@IsNotEmpty()
@ValidateIf((dto) => dto.needInvoice)
invoiceTitle?: string;
}
// needInvoice 为 false
// → 不检查 invoiceTitle 任何的条件
// needInvoice 为 true
// → invoiceTitle 必须存在并且是字符串
@ValidateIf 和 @IsOptional() 的区别是 @IsOptional() 只根据当前字段是否为 null 或 undefined 决定是否跳过校验。而 @ValidateIf() 是根据条件结果true/false 决定是否跳过校验
自定义错误信息
几乎所有 class-validator 装饰器都可以传入验证选项,并且还可以使用变量。在传入的object中声明message属性即可
export class RegisterDto {
@IsEmail(
{},
{
message: "邮箱格式不正确",
}
)
email: string;
@Length(2, 20, {
message: "用户名长度必须在 2~20 个字符之间",
})
username: string;
@Min(18, {
message: "年龄不能小于 18 岁",
})
age: number;
@MinLength(8, {
message: "密码长度不能少于 $constraint1 个字符",
})
password: string;
}
// 常见变量:
$value 当前值
$property 属性名
$target 类名
$constraint1 第一个规则参数
$constraint2 第二个规则参数
参数校验发生在进入 Controller 之前
class-transformer
是一个数据类型转化库,搭配validationpipi使用。不只是处理请求输入,也经常用于处理响应输出
常用修饰器
- @Type():把字段转换为指定的类型
import { Type } from 'class-transformer';
import { IsInt, Min } from 'class-validator';
export class QueryUserDto {
@Type(() => Number)
@IsInt()
page: number;
}
// 请求
GET /users?page=2
// 原始值
page === '2'
// 转换后
page === 2
// 然后检查是否为整数
export class CreateUserDto {
@Type(() => Date)
@IsDate()
birthday: Date;
}
// 请求
{
"birthday": "2000-01-01"
}
// 转换后
birthday instanceof Date; // true
export class CreateUserDto {
// 检查是否为嵌套对象。@ValidateNested() 要求嵌套值是对应类的实例(不能作用于普通对象上面),而 @Type(() => AddressDto) 负责将普通嵌套对象转换成 AddressDto 实例
@ValidateNested()
// 把普通对象转换为DTO实例
@Type(() => AddressDto)
address: AddressDto;
}
@Type不是验证器,它只负责转换,不保证结果合法:
@Type(() => Number)
age: number;
// 输入
{
"age": "abc"
}
// 得到的结果可能是NaN
谨慎使用布尔值转换:@Type(() => Boolean)
// 因为因为非空字符串本身是真值,所以会出现下面的情况,传入字符串false转成布尔后得到了true
Boolean('false') === true
// 对于直接参数可以使用管道ParseBoolPipe
@Query('enabled', ParseBoolPipe)
enabled: boolean;
@IsBoolean()
// 也可以使用@Transform()书写自定义函数进行转换,总之就是谨慎使用@Type(() => Boolean)
@Transform(({ value }) => {
if (value === true || value === 'true') {
return true;
}
if (value === false || value === 'false') {
return false;
}
return value;
})
enabled: boolean;
- @Transform():通过回调函数,进行自定义转换。例举一些常见用法:
字符串去空
import { Transform } from 'class-transformer';
export class CreateUserDto {
@Transform(({ value }) =>
typeof value === 'string' ? value.trim() : value,
)
@IsString()
username: string;
}
// 输入
{
"username": " xiaoming "
}
// 转换后
username === 'xiaoming'
转为小写
@Transform(({ value }) =>
typeof value === 'string'
? value.trim().toLowerCase()
: value,
)
@IsEmail()
email: string;
字符串拆成数组
export class ArticleQueryDto {
@Transform(({ value }) => {
if (Array.isArray(value)) {
return value;
}
if (typeof value !== 'string') {
return [];
}
return value
.split(',')
.map(item => item.trim())
.filter(Boolean);
})
@IsString({ each: true })
tags: string[];
}
// 请求
GET /articles?tags=nestjs,typescript,node
@Transform() 的回调可以获取的参数如下,这些参数由 class-transformer 官方 API 提供:
| 参数 | 含义 |
|---|---|
value |
当前属性的原始值 |
key |
当前属性名 |
obj |
整个来源对象 |
type |
当前转换方向 |
options |
本次转换的配置 |
- @Exclude():在转换时排除指定字段。常用于响应时设置不返回给客户端的字段
import { Exclude } from "class-transformer";
export class UserEntity {
id: number;
username: string;
email: string;
// 把实例转换成普通对象, 转换后的结果不会包含passwordHash字段
@Exclude()
passwordHash: string;
// 也可以反过来设置。把普通对象转换成实例时,不会包含passwordHash字段。
// @Exclude({ toPlainOnly: true })
// passwordHash: string;
}
- @Expose():用于改变字段名称、暴露getter
改变字段名
export class UserResponseDto {
// 输出时把id改为user_id
@Expose({ name: 'user_id' })
id: number;
}
// 转换后的输出
{
"user_id": 1
}
暴露计算属性
export class UserResponseDto {
firstName: string;
lastName: string;
// 转换后会,输出会添加fullName字段
@Expose()
get fullName(): string {
return `${this.firstName} ${this.lastName}`;
}
}
// 输出
{
"firstName": "张",
"lastName": "三",
"fullName": "张 三"
}
管道流程
外部原始数据
↓
Pipe
├─ 验证是否合法
├─ 转换数据类型
├─ ...其他设置的操作,如:清理多余字段、提供默认值
└─ 不合法则抛出异常
↓
安全、规范的数据
↓
Controller