NestJS 管道

2023-09-08 11:36 更新

管道是具有 @Injectable() 裝飾器的類。管道應實現(xiàn) PipeTransform 接口。

9

管道有兩個類型:

  • 轉(zhuǎn)換:管道將輸入數(shù)據(jù)轉(zhuǎn)換為所需的數(shù)據(jù)輸出
  • 驗證:對輸入數(shù)據(jù)進行驗證,如果驗證成功繼續(xù)傳遞; 驗證失敗則拋出異常;

在這兩種情況下, 管道 參數(shù)(arguments) 會由 控制器(controllers)的路由處理程序 進行處理. Nest 會在調(diào)用這個方法之前插入一個管道,管道會先攔截方法的調(diào)用參數(shù),進行轉(zhuǎn)換或是驗證處理,然后用轉(zhuǎn)換好或是驗證好的參數(shù)調(diào)用原方法。

管道在異常區(qū)域內(nèi)運行。這意味著當拋出異常時,它們由核心異常處理程序和應用于當前上下文的 異常過濾器 處理。當在 Pipe 中發(fā)生異常,controller 不會繼續(xù)執(zhí)行任何方法。

內(nèi)置管道

Nest 自帶八個開箱即用的管道,即

  • ValidationPipe
  • ParseIntPipe
  • ParseBoolPipe
  • ParseArrayPipe
  • ParseUUIDPipe
  • DefaultValuePipe
  • ParseEnumPipe
  • ParseFloatPipe

他們從 @nestjs/common 包中導出。為了更好地理解它們是如何工作的,我們將從頭開始構(gòu)建它們。

我們從 ValidationPipe. 開始。 首先它只接受一個值并立即返回相同的值,其行為類似于一個標識函數(shù)。

validate.pipe.ts
import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';

@Injectable()
export class ValidationPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    return value;
  }
}

PipeTransform<T, R> 是一個通用接口,其中 T 表示 value 的類型,R 表示 transform() 方法的返回類型。

每個管道必須提供 transform() 方法。 這個方法有兩個參數(shù):

  • value
  • metadata

value 是當前處理的參數(shù),而 metadata 是其元數(shù)據(jù)。元數(shù)據(jù)對象包含一些屬性:

export interface ArgumentMetadata {
  type: 'body' | 'query' | 'param' | 'custom';
  metatype?: Type<unknown>;
  data?: string;
}

這里有一些屬性描述參數(shù):

參數(shù)描述
type告訴我們該屬性是一個 body @Body(),query @Query(),param @Param() 還是自定義參數(shù) 。
metatype屬性的元類型,例如 String。 如果在函數(shù)簽名中省略類型聲明,或者使用原生 JavaScript,則為 undefined
data傳遞給裝飾器的字符串,例如 @Body('string')。 如果您將括號留空,則為 undefined。

TypeScript接口在編譯期間消失,所以如果你使用接口而不是類,那么 metatype 的值將是一個 Object。

測試用例

我們來關注一下 CatsController 的 create() 方法:

cats.controller.ts
@Post()
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

下面是 CreateCatDto 參數(shù). 類型為 CreateCatDto:

create-cat.dto.ts
export class CreateCatDto {
  name: string;
  age: number;
  breed: string;
}

我們要確保create方法能正確執(zhí)行,所以必須驗證 CreateCatDto 里的三個屬性。我們可以在路由處理程序方法中做到這一點,但是我們會打破單個責任原則(SRP)。另一種方法是創(chuàng)建一個驗證器類并在那里委托任務,但是不得不每次在方法開始的時候我們都必須使用這個驗證器。那么驗證中間件呢? 這可能是一個好主意,但我們不可能創(chuàng)建一個整個應用程序通用的中間件(因為中間件不知道 execution context執(zhí)行環(huán)境,也不知道要調(diào)用的函數(shù)和它的參數(shù))。

在這種情況下,你應該考慮使用管道。

對象結(jié)構(gòu)驗證

有幾種方法可以實現(xiàn),一種常見的方式是使用基于結(jié)構(gòu)的驗證。Joi 庫是允許您使用一個可讀的 API 以非常簡單的方式創(chuàng)建 schema,讓我們來試一下基于 Joi 的驗證管道。

首先安裝依賴:

$ npm install --save @hapi/joi
$ npm install --save-dev @types/hapi__joi

在下面的代碼中,我們先創(chuàng)建一個簡單的 class,在構(gòu)造函數(shù)中傳遞 schema 參數(shù). 然后我們使用 schema.validate() 方法驗證.

就像是前面說過的,驗證管道 要么返回該值,要么拋出一個錯誤。 在下一節(jié)中,你將看到我們?nèi)绾问褂?nbsp;@UsePipes() 修飾器給指定的控制器方法提供需要的 schema。

import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { ObjectSchema } from '@hapi/joi';

@Injectable()
export class JoiValidationPipe implements PipeTransform {
  constructor(private schema: ObjectSchema) {}

  transform(value: any, metadata: ArgumentMetadata) {
    const { error } = this.schema.validate(value);
    if (error) {
      throw new BadRequestException('Validation failed');
    }
    return value;
  }
}

綁定管道

綁定管道(可以綁在 controller 或是其方法上)非常簡單。我們使用 @UsePipes() 裝飾器并創(chuàng)建一個管道實例,并將其傳遞給 Joi 驗證。

@Post()
@UsePipes(new JoiValidationPipe(createCatSchema))
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

類驗證器

本節(jié)中的技術(shù)需要 TypeScript ,如果您的應用是使用原始 JavaScript編寫的,則這些技術(shù)不可用。

讓我們看一下驗證的另外一種實現(xiàn)方式

Nest 與 class-validator 配合得很好。這個優(yōu)秀的庫允許您使用基于裝飾器的驗證。裝飾器的功能非常強大,尤其是與 Nest 的 Pipe 功能相結(jié)合使用時,因為我們可以通過訪問 metatype 信息做很多事情,在開始之前需要安裝一些依賴。

$ npm i --save class-validator class-transformer

安裝完成后,我們就可以向 CreateCatDto 類添加一些裝飾器。

create-cat.dto.ts
import { IsString, IsInt } from 'class-validator';

export class CreateCatDto {
  @IsString()
  name: string;

  @IsInt()
  age: number;

  @IsString()
  breed: string;
}

此處了解有關類驗證器修飾符的更多信息。

現(xiàn)在我們來創(chuàng)建一個 ValidationPipe 類。

validate.pipe.ts
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToClass } from 'class-transformer';

@Injectable()
export class ValidationPipe implements PipeTransform<any> {
  async transform(value: any, { metatype }: ArgumentMetadata) {
    if (!metatype || !this.toValidate(metatype)) {
      return value;
    }
    const object = plainToClass(metatype, value);
    const errors = await validate(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);
  }
}

我們已經(jīng)使用了 class-transformer 庫。它和 class-validator 庫由同一個作者開發(fā),所以他們配合的很好。

讓我們來看看這個代碼。首先你會發(fā)現(xiàn) transform() 函數(shù)是 異步 的, Nest 支持同步和異步管道。這樣做的原因是因為有些 class-validator 的驗證是可以異步的(Promise)

接下來請注意,我們正在使用解構(gòu)賦值(從 ArgumentMetadata 中提取參數(shù))到方法中。這是一個先獲取全部 ArgumentMetadata 然后用附加語句提取某個變量的簡寫方式。

下一步,請觀察 toValidate() 方法。當驗證類型不是 JavaScript 的數(shù)據(jù)類型時,跳過驗證。

下一步,我們使用 class-transformer 的 plainToClass() 方法來轉(zhuǎn)換 JavaScript 的參數(shù)為可驗證的類型對象。一個請求中的 body 數(shù)據(jù)是不包含類型信息的,Class-validator 需要使用前面定義過的 DTO,就需要做一個類型轉(zhuǎn)換。

最后,如前所述,這就是一個驗證管道,它要么返回值不變,要么拋出異常。

最后一步是設置 ValidationPipe 。管道,與異常過濾器相同,它們可以是方法范圍的、控制器范圍的和全局范圍的。另外,管道可以是參數(shù)范圍的。我們可以直接將管道實例綁定到路由參數(shù)裝飾器,例如@Body()。讓我們來看看下面的例子:

cats.controller.ts
@Post()
async create(@Body(new ValidationPipe()) createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

當驗證邏輯僅涉及一個指定的參數(shù)時,參數(shù)范圍的管道非常有用。要在方法級別設置管道,您需要使用 UsePipes() 裝飾器。

cats.controller.ts
@Post()
@UsePipes(new ValidationPipe())
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

@UsePipes() 修飾器是從 @nestjs/common 包中導入的。

在上面的例子中 ValidationPipe 的實例已就地立即創(chuàng)建。另一種可用的方法是直接傳入類(而不是實例),讓框架承擔實例化責任,并啟用依賴注入。

cats.controller.ts
@Post()
@UsePipes(ValidationPipe)
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

由于 ValidationPipe 被創(chuàng)建為盡可能通用,所以我們將把它設置為一個全局作用域的管道,用于整個應用程序中的每個路由處理器。

main.ts
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}
bootstrap();

在 混合應用中 useGlobalPipes() 方法不會為網(wǎng)關和微服務設置管道, 對于標準(非混合) 微服務應用使用 useGlobalPipes() 全局設置管道。

全局管道用于整個應用程序、每個控制器和每個路由處理程序。就依賴注入而言,從任何模塊外部注冊的全局管道(如上例所示)無法注入依賴,因為它們不屬于任何模塊。為了解決這個問題,可以使用以下構(gòu)造直接為任何模塊設置管道:

app.module.ts
import { Module } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';

@Module({
  providers: [
    {
      provide: APP_PIPE,
      useClass: ValidationPipe
    }
  ]
})
export class AppModule {}

請注意使用上述方式依賴注入時,請牢記無論你采用那種結(jié)構(gòu)模塊管道都是全局的,那么它應該放在哪里呢?使用 ValidationPipe 定義管道 另外,useClass 并不是處理自定義提供者注冊的唯一方法。在這里了解更多。

轉(zhuǎn)換管道

驗證不是管道唯一的用處。在本章的開始部分,我已經(jīng)提到管道也可以將輸入數(shù)據(jù)轉(zhuǎn)換為所需的輸出。這是可以的,因為從 transform 函數(shù)返回的值完全覆蓋了參數(shù)先前的值。在什么時候使用?有時從客戶端傳來的數(shù)據(jù)需要經(jīng)過一些修改(例如字符串轉(zhuǎn)化為整數(shù)),然后處理函數(shù)才能正確的處理。還有種情況,比如有些數(shù)據(jù)具有默認值,用戶不必傳遞帶默認值參數(shù),一旦用戶不傳就使用默認值。轉(zhuǎn)換管道被插入在客戶端請求和請求處理程序之間用來處理客戶端請求。

parse-int.pipe.ts
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;
  }
}

如下所示, 我們可以很簡單的配置管道來處理所參數(shù) id:

@Get(':id')
async findOne(@Param('id', new ParseIntPipe()) id) {
  return await this.catsService.findOne(id);
}

由于上述結(jié)構(gòu),ParseIntpipe 將在請求觸發(fā)相應的處理程序之前執(zhí)行。

另一個有用的例子是按 ID 從數(shù)據(jù)庫中選擇一個現(xiàn)有的用戶實體。

@Get(':id')
findOne(@Param('id', UserByIdPipe) userEntity: UserEntity) {
  return userEntity;
}

如果愿意你還可以試試 ParseUUIDPipe 管道, 它用來分析驗證字符串是否是 UUID.

@Get(':id')
async findOne(@Param('id', new ParseUUIDPipe()) id) {
  return await this.catsService.findOne(id);
}

ParseUUIDPipe 會使用 UUID 3,4,5 版本 來解析字符串, 你也可以單獨設置需要的版本.

你也可以試著做一個管道自己通過 id 找到實體數(shù)據(jù):

@Get(':id')
findOne(@Param('id', UserByIdPipe) userEntity: UserEntity) {
  return userEntity;
}

請讀者自己實現(xiàn), 這個管道接收 id 參數(shù)并返回 UserEntity 數(shù)據(jù), 這樣做就可以抽象出一個根據(jù) id 得到 UserEntity 的公共管道, 你的程序變得更符合聲明式(Declarative 更好的代碼語義和封裝方式), 更 DRY (Don’t repeat yourself 減少重復代碼) 編程規(guī)范.

內(nèi)置驗證管道

幸運的是,由于 ValidationPipe 和 ParseIntPipe 是內(nèi)置管道,因此您不必自己構(gòu)建這些管道(請記住, ValidationPipe 需要同時安裝 class-validator 和 class-transformer 包)。與本章中構(gòu)建ValidationPipe的示例相比,該內(nèi)置的功能提供了更多的選項,為了說明管道的基本原理,該示例一直保持基本狀態(tài)。您可以在此處找到完整的詳細信息以及許多示例。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號