Skip to content

什么是管道?🤔 ​

管道(Pipe)是 NestJS 中一个非常重要的概念。它们主要用于两个目的:

  1. 转换(Transform):将输入数据转换为所需的形式(例如,将字符串转换为整数)
  2. 验证(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);
  }
}

管道的执行顺序 🔄 ​

理解管道的执行顺序很重要:

  1. 全局管道
  2. 控制器级别的管道
  3. 路由级别的管道
  4. 参数级别的管道
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;
  }
}

总结 📝 ​

通过这篇文章,我们深入学习了:

  1. 管道的基本概念和用途
  2. NestJS 内置管道的使用方法
  3. 如何创建和使用自定义管道
  4. 管道的执行顺序
  5. 实际开发中的最佳实践

管道是 NestJS 中非常强大的功能,它可以帮助我们:

  • 确保数据的有效性
  • 自动转换数据类型
  • 提供更好的错误处理
  • 使代码更加清晰和可维护

下一篇文章,我们将探讨 NestJS 的守卫(Guard)系统。

相关资源 📚 ​