第 173 章:管道(Pipe)与数据转换
学习目标
- 理解 Pipe 的作用
- 掌握内置 Pipe
- 学会自定义 Pipe
- 掌握 class-validator 数据验证
一、什么是 Pipe
Pipe 用来转换和验证控制器接收的输入数据。
两个核心用途:
- 转换:把输入数据转成期望类型
- 验证:检查数据是否合法,失败抛异常
二、内置 Pipe
2.1 转换类
typescript
import { ParseIntPipe, ParseFloatPipe, ParseBoolPipe, ParseArrayPipe, ParseUUIDPipe, ParseEnumPipe, DefaultValuePipe } from '@nestjs/common';
class DemoController {
// 字符串 → 数字,失败 400
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {}
// 字符串 → 浮点数
@Get('price/:price')
price(@Param('price', ParseFloatPipe) price: number) {}
// 字符串 → boolean
@Get('active/:flag')
active(@Param('flag', ParseBoolPipe) flag: boolean) {}
// 字符串数组 → 数组
@Get('list')
list(@Query('ids', ParseArrayPipe) ids: string[]) {}
// 字符串 → UUID
@Get(':uuid')
one(@Param('uuid', ParseUUIDPipe) uuid: string) {}
// 字符串 → 枚举值
@Get('role/:role')
role(@Param('role', new ParseEnumPipe(Role)) role: Role) {}
// 默认值
@Get('list')
list(@Query('page', new DefaultValuePipe(1)) page: number) {}
}2.2 验证类
typescript
import { ValidationPipe } from '@nestjs/common';
// main.ts 全局启用
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // 剥离未声明字段
forbidNonWhitelisted: true, // 遇到未知字段报错
transform: true, // 自动类型转换
}));三、class-validator 装饰器
3.1 安装
bash
pnpm add class-validator class-transformer3.2 基础验证
typescript
import { IsString, IsInt, IsEmail, MinLength, MaxLength, Min, Max, IsOptional, IsEnum } from 'class-validator';
export class CreateUserDto {
@IsString()
@MinLength(2)
@MaxLength(20)
name: string;
@IsEmail()
email: string;
@IsInt()
@Min(0)
@Max(150)
age: number;
@IsOptional() // 可选
@IsString()
phone?: string;
@IsEnum(['admin', 'user'])
role: 'admin' | 'user';
}3.3 嵌套验证
typescript
export class CreateOrderDto {
@IsString()
productId: string;
@ValidateNested()
@Type(() => AddressDto) // class-transformer
address: AddressDto;
}
export class AddressDto {
@IsString()
city: string;
@IsString()
street: string;
}3.4 数组验证
typescript
import { ArrayMinSize, ArrayMaxSize, IsArray } from 'class-validator';
export class BatchCreateDto {
@IsArray()
@ArrayMinSize(1)
@ArrayMaxSize(100)
@ValidateNested({ each: true })
@Type(() => CreateUserDto)
users: CreateUserDto[];
}四、自定义 Pipe
4.1 实现 PipeTransform
typescript
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
@Injectable()
export class TrimPipe implements PipeTransform {
// value:被转换的值
// metadata:元数据
transform(value: any, metadata: ArgumentMetadata) {
if (typeof value !== 'string') return value;
return value.trim();
}
}
// 使用
@Post()
create(@Body(TrimPipe) dto: CreateUserDto) {}4.2 参数转换 Pipe
typescript
@Injectable()
export class ToNumberPipe implements PipeTransform<string, number> {
transform(value: string, metadata: ArgumentMetadata): number {
const val = parseInt(value, 10);
if (isNaN(val)) {
throw new BadRequestException(`${value} is not a number`);
}
return val;
}
}
@Get(':id')
findOne(@Param('id', ToNumberPipe) id: number) {}4.3 全局验证配置
typescript
import { ValidationPipe } from '@nestjs/common';
app.useGlobalPipes(new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
transformOptions: {
enableImplicitConversion: true, // 隐式类型转换
},
disableErrorMessages: false, // 是否隐藏错误
stopAtFirstError: false, // 是否第一个错误就停止
}));五、参数级别 vs 方法级别 vs 全局级别
5.1 参数级(单个)
typescript
@Post()
create(@Body(ValidationPipe) dto: CreateUserDto) {}5.2 方法级(整个方法)
typescript
@Post()
@UsePipes(new ValidationPipe({ transform: true }))
create(@Body() dto: CreateUserDto) {}5.3 控制器级
typescript
@Controller('users')
@UsePipes(ValidationPipe)
export class UserController {
@Post()
create(@Body() dto: CreateUserDto) {}
}5.4 全局级(推荐)
typescript
// main.ts
app.useGlobalPipes(new ValidationPipe());六、自定义验证装饰器
6.1 简单装饰器
typescript
import { registerDecorator, ValidationOptions } from 'class-validator';
export function IsStrongPassword(options?: ValidationOptions) {
return function (object: object, propertyName: string) {
registerDecorator({
name: 'isStrongPassword',
target: object.constructor,
propertyName,
options,
validator: {
validate(value: any) {
return typeof value === 'string'
&& /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/.test(value);
},
defaultMessage() {
return '密码必须 8 位以上,包含大小写字母和数字';
},
},
});
};
}
// 使用
export class RegisterDto {
@IsStrongPassword()
password: string;
}6.2 复杂约束
typescript
// 两个字段必须相同
export function IsMatch(property: string, options?: ValidationOptions) {
return function (object: object, propertyName: string) {
registerDecorator({
name: 'isMatch',
target: object.constructor,
propertyName,
constraints: [property],
options,
validator: {
validate(value, args) {
const [relatedPropertyName] = args.constraints;
const relatedValue = (args.object as any)[relatedPropertyName];
return value === relatedValue;
},
},
});
};
}
export class RegisterDto {
@IsString()
@MinLength(6)
password: string;
@IsMatch('password')
confirmPassword: string;
}七、文件验证 Pipe
typescript
@Injectable()
export class FileSizeValidationPipe implements PipeTransform {
transform(file: Express.Multer.File) {
const maxSize = 5 * 1024 * 1024; // 5MB
if (file.size > maxSize) {
throw new BadRequestException('文件大小不能超过 5MB');
}
return file;
}
}
@Post('upload')
@UseInterceptors(FileInterceptor('file'))
upload(@UploadedFile(FileSizeValidationPipe) file: Express.Multer.File) {}八、异常处理
typescript
// 自定义异常消息
export class CreateUserDto {
@IsString({ message: '姓名必须是字符串' })
name: string;
@IsInt({ message: '年龄必须是整数' })
@Min(0, { message: '年龄不能小于 0' })
age: number;
}
// 响应示例
{
"statusCode": 400,
"message": [
"姓名必须是字符串",
"年龄不能小于 0"
],
"error": "Bad Request"
}九、分页 DTO
typescript
export class PaginationDto {
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
page: number = 1;
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
@Max(100)
size: number = 10;
@IsOptional()
@IsString()
sort?: string;
}
@Get()
list(@Query() pagination: PaginationDto) {
return this.userService.findAll(pagination);
}重要
@Type(() => Number) 来自 class-transformer,必须装。
十、本章小结
| 类型 | 例子 |
|---|---|
| 转换 Pipe | ParseIntPipe ParseUUIDPipe |
| 验证 Pipe | ValidationPipe |
| 默认值 | DefaultValuePipe |
| 验证装饰器 | @IsString @IsEmail |
| 自定义验证 | registerDecorator |
| 作用范围 | 参数/方法/控制器/全局 |
动手练习
- 创建一个
RegisterDto,验证邮箱、密码、确认密码 - 写一个
TrimPipe自动去掉字符串首尾空格 - 实现一个分页 DTO,带默认值和范围校验
- 写一个
@IsCNPhone自定义验证装饰器
推荐阅读
下一章:第 174 章:守卫(Guard)与权限控制 →