基本概念
管道(Pipes)是处理输入数据并在它实际到达路由处理程序之前执行某些操作的类。管道可以做的操作包括但不限于:
- 数据转换(Transformation) :更改输入数据的格式或类型,例如将字符串转换为数字,将用户提交的日期字符串转换为 Date 对象等。
- 数据验证(Validation) :检查输入数据是否满足某些条件和约束,如果不满足,则抛出异常。
管道使用 @Injectable()
装饰器来标识它们可以被 NestJS 的依赖注入系统管理,并且它们实现 PipeTransform
接口,该接口定义了一个必须实现的 transform
方法
typescript
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
transform(value: string, metadata: ArgumentMetadata): number {
const val = parseInt(value, 10);
if (isNaN(val)) {
throw new BadRequestException('Validation failed');
}
return val;
}
}
在上面的例子中,ParseIntPipe
是一个管道,它将一个字符串格式的数值转换为一个 JavaScript 的 number
类型。如果输入的字符串不是一个有效的数字,则抛出一个 BadRequestException
。
管道可以以多种方式应用:
- 在单个路由处理程序级别
- 在控制器级别
- 作为全局管道
typescript
import { Controller, Get, Param, ParseIntPipe } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Get('/:id')
findOne(@Param('id', ParseIntPipe) id: number) {
// `id` 是一个被 ParseIntPipe 管道转换后的数字
return `This action returns a cat with id ${id}`;
}
}
总之,管道是一个功能强大的特性,能够使我们更安全、更高效地处理和验证路由处理程序的输入参数。
内置管道
Nest 附带九个开箱即用的管道:
ValidationPipe
ParseIntPipe
ParseFloatPipe
ParseBoolPipe
ParseArrayPipe
ParseUUIDPipe
ParseEnumPipe
DefaultValuePipe
ParseFilePipe
绑定管道
绑定管道到方法参数级别
less
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
return this.catsService.findOne(id);
}
请求:GET localhost:3000/abc
抛出异常:
json
{
"statusCode": 400,
"message": "Validation failed (numeric string is expected)",
"error": "Bad Request"
}
该异常将阻止 findOne()
方法的主体执行。
在上面的示例中,我们传递了一个类 (ParseIntPipe
),而不是一个实例,将实例化的责任留给了框架并启用依赖注入。
传递管道实例与传递管道类
在 NestJS 中使用管道时,可以选择传递一个管道类 或者传递一个管道实例。
- 传递管道类时,Nest 会自动处理实例化,并使得依赖注入成为可能。
- 而传递一个管道实例允许我们自定义管道行为,例如传递配置选项
typescript
import { Controller, Get, HttpStatus, Param } from '@nestjs/common';
import { ParseIntPipe } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Get('/:id')
async findOne(
@Param('id', new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE }))
id: number,
) {
// `id` 参数会由自定义的 ParseIntPipe 实例进行转换
// 传递了配置选项 { errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE }
return `This action returns a cat with id ${id}`;
}
}
在上述方法中,我们创建了一个 ParseIntPipe
的实例,并通过构造函数传递了一个对象 { errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE }
作为其配置选项。这样就配置了管道,当转换失败时抛出的 HTTP 状态代码不再是默认的 400 Bad Request
,而是 406 Not Acceptable
。
定制管道
typescript
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
@Injectable()
export class ValidationPipe implements PipeTransform {
transform(value: any, { metatype, type, data }: ArgumentMetadata) {
// 只对参数类型为 'body' 的数据进行验证
if (type === 'body') {
// 检查 metatype 是否存在且是否为 JavaScript 类型
if (!metatype || !this.toValidate(metatype)) {
return value;
}
// 对于标记了类型的参数,进行类型安全的验证
const object = plainToClass(metatype, value);
const errors = validateSync(object);
// 如果验证失败,抛出异常
if (errors.length > 0) {
throw new BadRequestException('Validation failed');
}
}
// 如果没有问题,则返回原样的值
return value;
}
private toValidate(metatype: Function): boolean {
const types: Function[] = [String, Boolean, Number, Array, Object];
return !types.includes(metatype);
}
}
PipeTransform<T, R>
是一个通用接口,任何管道都必须实现。泛型接口用 T
表示输入 value
的类型,用 R
表示 transform()
方法的返回类型。
每个管道都必须实现 transform()
方法来履行 PipeTransform
接口契约。这个方法有两个参数:
value
metadata
value
参数是当前处理的方法参数(在被路由处理方法接收之前),metadata
是当前处理的方法参数的元数据。
元数据对象具有以下属性:
typescript
export interface ArgumentMetadata {
type: 'body' | 'query' | 'param' | 'custom';
metatype?: Type<unknown>;
data?: string;
}
type |
指示参数是主体 @Body() 、查询 @Query() 、参数 @Param() 还是自定义参数(了解更多 此处)。 |
---|---|
metatype |
提供参数的元类型,例如 String 。注意:如果你在路由处理程序方法签名中省略类型声明或使用普通 JavaScript,则该值为 undefined 。 |
data |
传递给装饰器的字符串,例如 @Body('string') 。如果将装饰器括号留空,则为 undefined 。 |
验证管道
基于模式的验证❎
less
@Post()
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
typescript
//create-cat.dto.ts
export class CreateCatDto {
name: string;
age: number;
breed: string;
}
我们需要验证传入 create()
方法的对象,在执行服务逻辑之前确保其有效性。虽然我们可以在路由处理方法内验证数据 ,但这会违反单一职责原则 (SRP),因为这个方法既要处理 HTTP 请求,又要负责验证输入数据。
如果在每个需要验证数据的方法中手动进行验证,很容易漏掉验证步骤,导致代码中出现不一致和难以维护的验证逻辑
拓展:验证器类的方法和验证中间件的方法❎
文档中提到两种不适合的方法:
另一种方法可能是创建一个验证器类并在那里委派任务。这样做的缺点是我们必须记住在每个方法的开头调用此验证器。
如何创建验证中间件?这可能有效,但不幸的是,不可能创建可在整个应用的所有上下文中使用的通用中间件。这是因为中间件不知道执行上下文,包括将被调用的处理程序及其任何参数。
验证器类的方法
这种方法的核心思想是将验证逻辑封装到单独的服务或类中,然后在需要进行验证的每个路由处理程序中调用这个服务或类。例如:
scss
export class CatValidator {
static validateCreateCat(createCatDto: CreateCatDto) {
const errors = [];
if (!createCatDto.name) {
errors.push('Name is required');
}
if (!createCatDto.age || createCatDto.age < 0) {
errors.push('Age is required and must be a positive number');
}
if (!createCatDto.breed) {
errors.push('Breed is required');
}
if (errors.length > 0) {
throw new BadRequestException(errors);
}
}
}
@Controller('cats')
export class CatsController {
@Post()
async create(@Body() createCatDto: CreateCatDto) {
CatValidator.validateCreateCat(createCatDto);
// ... catsService.create(createCatDto);
}
}
缺点:
- 重复性:开发者必须记住在每个需要验证的控制器方法中调用验证器服务。这增加了出错的风险,因为可能会遗漏某些验证调用。
- 责任分散:路由处理程序负责处理业务逻辑,但还将承担数据验证的责任,这破坏了单一职责原则。
验证中间件的方法
关于验证中间件,我们可以尝试将验证逻辑放入一个中间件中。中间件在请求和响应的生命周期中的特定阶段被执行,可以对请求对象做修改。例如:
typescript
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class CatValidationMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
const createCatDto = req.body;
// 进行验证逻辑...
// 如果验证失败,可以直接响应错误:
// return res.status(400).json({ error: 'Validation failed' });
// 如果验证通过,调用 next 继续请求流程
next();
}
}
缺点:
- 上下文不可知:中间件工作在请求对象级别,不了解请求是如何映射到特定的控制器方法的。也就是说,它没有足够的信息来决定应该怎样验证请求,因为不同的路由可能需要不同的验证规则。
- 参数信息不可用 :中间件不知道即将被调用的控制器方法使用了什么参数和装饰器,因此无法利用例如
class-validator
中的装饰器提供的元数据信息来进行验证。
正是因为这些限制,验证中间件通常不如验证管道适用于处理输入验证。管道可以访问路由参数的上下文信息,包括参数的类型和装饰器,这让管道能够提供更精细化和声明式的验证。
最终,NestJS 中的 ValidationPipe
结合了管道的上下文感知能力和 class-validator
提供的装饰器,使得验证逻辑变得既简洁又强大。在我们的控制器中,我们可以使用 @UsePipes
装饰器来应用 ValidationPipe
,自动执行声明式的验证,而无需编写重复的验证逻辑或依赖上下文不明的中间件。
解决方法:验证管道✅
为了解决这问题,NestJS 提供了管道(Pipes),它是一种用于数据转换和验证的技术。
管道的理念是用声明方式实现验证逻辑,附加在特定的路由处理函数上,而不需要在每个函数中编写验证代码。这符合单一职责原则,并能确保一致性和可维护性
NestJS 提供了一些内置的验证管道,如 ValidationPipe
,它可以配合类验证器 (class-validator
) 和类转换器 (class-transformer
) 库使用,自动地进行复杂的验证逻辑。
less
import { Controller, Post, Body, UsePipes, ValidationPipe } from '@nestjs/common';
import { CreateCatDto } from './create-cat.dto';
import { CatsService } from './cats.service';
@Controller('cats')
export class CatsController {
constructor(private catsService: CatsService) {}
@Post()
@UsePipes(ValidationPipe)
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
}
而 CreateCatDto
中的类成员需要使用装饰器进行标注,这样 ValidationPipe
就能理解如何验证每个成员:
less
import { IsString, IsInt, Min, Max } from 'class-validator';
export class CreateCatDto {
@IsString() //类成员使用装饰器进行标注
name: string;
@IsInt()
@Min(0)
@Max(20)
age: number;
@IsString()
breed: string;
}
现在,当请求到达 create()
方法时,ValidationPipe
会自动取出 createCatDto
对象,并使用 class-validator
进行验证。如果对象不满足条件,则管道会抛出异常,这样控制器中的方法就不会执行。