返回文章列表 →

Note

管道

nestjs的管道介绍

发布于 更新于

管道

后端开发中的“管道”(Pipeline)本质上是指将多个处理步骤串联起来,让数据像水流一样依次通过各个节点进行处理的架构模式,常常会把前一个管道处理后的值传给下一个管道。而NestJS 的管道(Pipe)是在 Controller 执行之前生效,对传入的方法参数进行“检查”或“加工”。

❓与中间件、拦截器的区别

作用

  1. 转换数据:把入参转换成 Controller 需要的类型
  2. 验证数据:判断入参是否合法,不合法直接抛出异常,不再执行 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一起使用

常用装饰器

  1. 必填、可选、判空
装饰器 作用
@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 的字符串
  1. 基础类型验证
装饰器 作用
@IsString() 必须是字符串
@IsBoolean() 必须是布尔值
@IsNumber() 必须是数字
@IsInt() 必须是整数
@IsDate() 必须是 Date 对象
@IsArray() 必须是数组
@IsObject() 必须是对象
@IsEnum(Enum) 必须是枚举中的值
@IsInstance(Class) 必须是指定类的实例
  1. 数字验证
装饰器 作用
@Min(value) 数字不能小于指定值
@Max(value) 数字不能大于指定值
@IsPositive() 必须大于 0
@IsNegative() 必须小于 0
@IsDivisibleBy(value) 必须能被指定数字整除
  1. 字符串长度和内容验证
装饰器 作用
@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;
}
  1. 常见格式验证
装饰器 作用
@IsEmail() 邮箱格式
@IsURL() URL 格式
@IsUUID() UUID 格式
@IsIP() IP 地址格式
@IsJSON() JSON 字符串格式
@IsDateString() ISO 日期字符串
@IsNumberString() 数字形式的字符串
@IsBooleanString() 布尔形式的字符串
@IsMongoId() MongoDB ObjectId 字符串
@IsPhoneNumber() 电话号码格式
@IsStrongPassword() 强密码格式
  1. 数组验证
装饰器 作用
@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[];
}
  1. 条件验证和嵌套验证
装饰器 作用
@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使用。不只是处理请求输入,也经常用于处理响应输出

常用修饰器

  1. @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;
  1. @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 本次转换的配置
  1. @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;
}
  1. @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