什么是管道?🤔
管道(Pipe)是 NestJS 中一个非常重要的概念。它们主要用于两个目的:
- 转换(Transform):将输入数据转换为所需的形式(例如,将字符串转换为整数)
- 验证(Validation):验证输入数据,如果数据无效则抛出异常
想象一下管道就像是一个数据的过滤器,数据在到达控制器之前,需要经过这个过滤器的处理。
Express vs NestJS 对比 🆚
在传统的 Express 应用中,我们通常需要手动处理数据验证和转换:
Express 写法:
javascript
const express = require('express');
const app = express();
// 手动验证和转换
app.post('/users', (req, res) => {
const age = parseInt(req.body.age); // 手动转换
// 手动验证
if (isNaN(age) || age < 0 || age > 120) {
return res.status(400).json({
message: '年龄必须是0-120之间的数字'
});
}
// 处理业务逻辑...
});NestJS 写法:
typescript
@Post()
createUser(@Body(new ValidationPipe()) createUserDto: CreateUserDto) {
// ValidationPipe 会自动处理验证和转换
return this.usersService.create(createUserDto);
}内置管道 🛠️
NestJS 提供了很多开箱即用的内置管道,让我们详细了解每一个:
1. ValidationPipe
最常用的管道之一,用于数据验证和转换。
typescript
// 定义DTO
export class CreateUserDto {
@IsString()
@MinLength(2)
@MaxLength(20)
name: string;
@IsEmail()
email: string;
@IsInt()
@Min(0)
@Max(120)
age: number;
}
// 在控制器中使用
@Post()
@UsePipes(new ValidationPipe({
transform: true, // 自动转换类型
whitelist: true, // 去除未定义的属性
forbidNonWhitelisted: true, // 如果出现未定义的属性则抛出错误
disableErrorMessages: false, // 在生产环境建议设置为 true
}))
create(@Body() createUserDto: CreateUserDto) {
// createUserDto 已经被验证和转换
return this.usersService.create(createUserDto);
}2. ParseIntPipe
将字符串参数转换为整数。
typescript
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
// id 已经被转换为数字
return this.usersService.findOne(id);
}3. ParseFloatPipe
将字符串参数转换为浮点数。
typescript
@Get('price/:price')
findByPrice(@Param('price', ParseFloatPipe) price: number) {
// price 已经被转换为浮点数
return this.productsService.findByPrice(price);
}4. ParseBoolPipe
将字符串参数转换为布尔值。
typescript
@Get('active')
findActive(@Query('isActive', ParseBoolPipe) isActive: boolean) {
// isActive 已经被转换为布尔值
return this.usersService.findActive(isActive);
}5. ParseArrayPipe
将字符串转换为数组。
typescript
@Post('bulk')
createMany(
@Body(new ParseArrayPipe({ items: CreateUserDto }))
createUserDtos: CreateUserDto[],
) {
// createUserDtos 已经被转换为 DTO 数组
return this.usersService.createMany(createUserDtos);
}自定义管道 🎨
有时内置管道可能无法满足我们的需求,这时我们可以创建自定义管道。让我们通过几个实例来学习:
1. 简单的转换管道
typescript
@Injectable()
export class TrimPipe implements PipeTransform {
transform(value: any) {
// 如果是字符串,去除首尾空格
if (typeof value === 'string') {
return value.trim();
}
// 如果是对象,递归处理所有字符串属性
if (typeof value === 'object') {
Object.keys(value).forEach(key => {
value[key] = this.transform(value[key]);
});
}
return value;
}
}
// 使用自定义管道
@Post()
create(@Body(TrimPipe) createUserDto: CreateUserDto) {
// 所有字符串都已经去除首尾空格
return this.usersService.create(createUserDto);
}2. 复杂的验证管道
typescript
@Injectable()
export class PasswordStrengthPipe implements PipeTransform {
private readonly passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,}$/;
transform(value: any) {
if (!this.passwordRegex.test(value)) {
throw new BadRequestException(
'密码必须包含至少8个字符,包括大小写字母和数字'
);
}
return value;
}
}
// 使用密码强度验证管道
@Post('change-password')
changePassword(
@Body('password', PasswordStrengthPipe) password: string
) {
return this.usersService.changePassword(password);
}实战示例 💡
让我们创建一个完整的用户注册系统,展示如何组合使用多个管道:
typescript
// user/dto/create-user.dto.ts
export class CreateUserDto {
@IsString()
@MinLength(2)
@MaxLength(20)
name: string;
@IsEmail()
email: string;
@IsString()
@MinLength(8)
password: string;
@IsOptional()
@IsPhoneNumber()
phone?: string;
@IsDateString()
birthDate: string;
}
// user/pipes/age.pipe.ts
@Injectable()
export class AgeValidationPipe implements PipeTransform {
transform(value: string) {
const birthDate = new Date(value);
const age = this.calculateAge(birthDate);
if (age < 18) {
throw new BadRequestException('用户必须年满18岁');
}
return value;
}
private calculateAge(birthDate: Date): number {
const today = new Date();
let age = today.getFullYear() - birthDate.getFullYear();
const monthDiff = today.getMonth() - birthDate.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
age--;
}
return age;
}
}
// user/controllers/user.controller.ts
@Controller('users')
export class UserController {
@Post('register')
@UsePipes(new ValidationPipe({
transform: true,
whitelist: true,
forbidNonWhitelisted: true
}))
async register(
@Body() createUserDto: CreateUserDto,
@Body('birthDate', AgeValidationPipe) birthDate: string
) {
// 所有数据都已经过验证和转换
return this.userService.register(createUserDto);
}
}管道的执行顺序 🔄
理解管道的执行顺序很重要:
- 全局管道
- 控制器级别的管道
- 路由级别的管道
- 参数级别的管道
typescript
// 全局管道
app.useGlobalPipes(new ValidationPipe());
// 控制器级别管道
@Controller('users')
@UsePipes(new TrimPipe())
export class UserController {
// 路由级别管道
@Post()
@UsePipes(new ValidationPipe())
create(
// 参数级别管道
@Body(new ParseIntPipe()) createUserDto: CreateUserDto
) {}
}最佳实践 ✨
1. 合理使用全局管道
typescript
// main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
transform: true,
whitelist: true,
forbidNonWhitelisted: true,
transformOptions: {
enableImplicitConversion: true,
},
}),
);
await app.listen(3000);
}2. 创建可复用的验证管道
typescript
@Injectable()
export class EntityExistsPipe implements PipeTransform {
constructor(
private readonly repository: Repository<any>,
private readonly field: string = 'id'
) {}
async transform(value: any) {
const entity = await this.repository.findOne({
where: { [this.field]: value }
});
if (!entity) {
throw new NotFoundException(`Entity with ${this.field} ${value} not found`);
}
return value;
}
}3. 错误处理
typescript
@Injectable()
export class SafeParseIntPipe implements PipeTransform<string, number> {
transform(value: string): number {
const parsedValue = parseInt(value, 10);
if (isNaN(parsedValue)) {
throw new BadRequestException(
`Validation failed. "${value}" is not a number`
);
}
if (parsedValue < 0) {
throw new BadRequestException(
`Validation failed. "${value}" must be a positive number`
);
}
return parsedValue;
}
}总结 📝
通过这篇文章,我们深入学习了:
- 管道的基本概念和用途
- NestJS 内置管道的使用方法
- 如何创建和使用自定义管道
- 管道的执行顺序
- 实际开发中的最佳实践
管道是 NestJS 中非常强大的功能,它可以帮助我们:
- 确保数据的有效性
- 自动转换数据类型
- 提供更好的错误处理
- 使代码更加清晰和可维护
下一篇文章,我们将探讨 NestJS 的守卫(Guard)系统。