小白从0开始——NestJS官网文档精读:管道

基本概念

管道(Pipes)是处理输入数据并在它实际到达路由处理程序之前执行某些操作的类。管道可以做的操作包括但不限于:

  1. 数据转换(Transformation) :更改输入数据的格式或类型,例如将字符串转换为数字,将用户提交的日期字符串转换为 Date 对象等。
  2. 数据验证(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 进行验证。如果对象不满足条件,则管道会抛出异常,这样控制器中的方法就不会执行。

还没写完,剩下的下周上班更新(周末不学习),电脑有时候卡死机,怕没保存,先发布了再说

相关推荐
知否技术4 小时前
为什么nodejs成为后端开发者的新宠?
前端·后端·node.js
谢尔登9 小时前
【Node.js】worker_threads 多线程
node.js
osnet14 小时前
showdoc二次开发
node.js·vue
泯泷14 小时前
「生产必看」在企业环境中正确使用 Node.js 的九大原则
前端·后端·node.js
太阳火神的美丽人生15 小时前
Vant WeApp 开启 NPM 遇到的问题总结
前端·npm·node.js
qingshun1 天前
Node 系列之预热知识(1)
node.js
余生H2 天前
前端的全栈混合之路Meteor篇:RPC方法注册及调用
前端·rpc·node.js·全栈
前端 贾公子2 天前
Node.js env 环境变量多种配置方式
node.js
sooRiverling2 天前
VUE 开发——Node.js学习(一)
vue.js·学习·node.js
_清豆°2 天前
NodeJS下载、安装及环境配置教程,内容详实
javascript·node.js