管道是具有 @Injectable() 裝飾器的類。管道應(yīng)實(shí)現(xiàn) PipeTransform 接口。
管道有兩個(gè)類型:
在這兩種情況下, 管道 參數(shù)(arguments) 會(huì)由 控制器(controllers)的路由處理程序 進(jìn)行處理. Nest 會(huì)在調(diào)用這個(gè)方法之前插入一個(gè)管道,管道會(huì)先攔截方法的調(diào)用參數(shù),進(jìn)行轉(zhuǎn)換或是驗(yàn)證處理,然后用轉(zhuǎn)換好或是驗(yàn)證好的參數(shù)調(diào)用原方法。
管道在異常區(qū)域內(nèi)運(yùn)行。這意味著當(dāng)拋出異常時(shí),它們由核心異常處理程序和應(yīng)用于當(dāng)前上下文的 異常過濾器 處理。當(dāng)在 Pipe 中發(fā)生異常,controller 不會(huì)繼續(xù)執(zhí)行任何方法。
Nest 自帶八個(gè)開箱即用的管道,即
他們從 @nestjs/common 包中導(dǎo)出。為了更好地理解它們是如何工作的,我們將從頭開始構(gòu)建它們。
我們從 ValidationPipe. 開始。 首先它只接受一個(gè)值并立即返回相同的值,其行為類似于一個(gè)標(biāo)識(shí)函數(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> 是一個(gè)通用接口,其中 T 表示 value 的類型,R 表示 transform() 方法的返回類型。
每個(gè)管道必須提供 transform() 方法。 這個(gè)方法有兩個(gè)參數(shù):
value 是當(dāng)前處理的參數(shù),而 metadata 是其元數(shù)據(jù)。元數(shù)據(jù)對(duì)象包含一些屬性:
export interface ArgumentMetadata {
type: 'body' | 'query' | 'param' | 'custom';
metatype?: Type<unknown>;
data?: string;
}
這里有一些屬性描述參數(shù):
參數(shù) | 描述 |
---|---|
type | 告訴我們?cè)搶傩允且粋€(gè) body @Body() ,query @Query() ,param @Param() 還是自定義參數(shù) 。 |
metatype | 屬性的元類型,例如 String 。 如果在函數(shù)簽名中省略類型聲明,或者使用原生 JavaScript,則為 undefined 。 |
data | 傳遞給裝飾器的字符串,例如 @Body('string') 。 如果您將括號(hào)留空,則為 undefined 。 |
TypeScript接口在編譯期間消失,所以如果你使用接口而不是類,那么 metatype 的值將是一個(gè) Object。
我們來關(guān)注一下 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í)行,所以必須驗(yàn)證 CreateCatDto 里的三個(gè)屬性。我們可以在路由處理程序方法中做到這一點(diǎn),但是我們會(huì)打破單個(gè)責(zé)任原則(SRP)。另一種方法是創(chuàng)建一個(gè)驗(yàn)證器類并在那里委托任務(wù),但是不得不每次在方法開始的時(shí)候我們都必須使用這個(gè)驗(yàn)證器。那么驗(yàn)證中間件呢? 這可能是一個(gè)好主意,但我們不可能創(chuàng)建一個(gè)整個(gè)應(yīng)用程序通用的中間件(因?yàn)橹虚g件不知道 execution context執(zhí)行環(huán)境,也不知道要調(diào)用的函數(shù)和它的參數(shù))。
在這種情況下,你應(yīng)該考慮使用管道。
有幾種方法可以實(shí)現(xiàn),一種常見的方式是使用基于結(jié)構(gòu)的驗(yàn)證。Joi 庫(kù)是允許您使用一個(gè)可讀的 API 以非常簡(jiǎn)單的方式創(chuàng)建 schema,讓我們來試一下基于 Joi 的驗(yàn)證管道。
首先安裝依賴:
$ npm install --save @hapi/joi
$ npm install --save-dev @types/hapi__joi
在下面的代碼中,我們先創(chuàng)建一個(gè)簡(jiǎn)單的 class,在構(gòu)造函數(shù)中傳遞 schema 參數(shù). 然后我們使用 schema.validate() 方法驗(yàn)證.
就像是前面說過的,驗(yàn)證管道 要么返回該值,要么拋出一個(gè)錯(cuò)誤。 在下一節(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 或是其方法上)非常簡(jiǎn)單。我們使用 @UsePipes() 裝飾器并創(chuàng)建一個(gè)管道實(shí)例,并將其傳遞給 Joi 驗(yàn)證。
@Post()
@UsePipes(new JoiValidationPipe(createCatSchema))
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
本節(jié)中的技術(shù)需要 TypeScript ,如果您的應(yīng)用是使用原始 JavaScript編寫的,則這些技術(shù)不可用。
讓我們看一下驗(yàn)證的另外一種實(shí)現(xiàn)方式
Nest 與 class-validator 配合得很好。這個(gè)優(yōu)秀的庫(kù)允許您使用基于裝飾器的驗(yàn)證。裝飾器的功能非常強(qiáng)大,尤其是與 Nest 的 Pipe 功能相結(jié)合使用時(shí),因?yàn)槲覀兛梢酝ㄟ^訪問 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;
}
在此處了解有關(guān)類驗(yàn)證器修飾符的更多信息。
現(xiàn)在我們來創(chuàng)建一個(gè) 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 庫(kù)。它和 class-validator 庫(kù)由同一個(gè)作者開發(fā),所以他們配合的很好。
讓我們來看看這個(gè)代碼。首先你會(huì)發(fā)現(xiàn) transform() 函數(shù)是 異步 的, Nest 支持同步和異步管道。這樣做的原因是因?yàn)橛行?nbsp;class-validator 的驗(yàn)證是可以異步的(Promise)
接下來請(qǐng)注意,我們正在使用解構(gòu)賦值(從 ArgumentMetadata 中提取參數(shù))到方法中。這是一個(gè)先獲取全部 ArgumentMetadata 然后用附加語句提取某個(gè)變量的簡(jiǎn)寫方式。
下一步,請(qǐng)觀察 toValidate() 方法。當(dāng)驗(yàn)證類型不是 JavaScript 的數(shù)據(jù)類型時(shí),跳過驗(yàn)證。
下一步,我們使用 class-transformer 的 plainToClass() 方法來轉(zhuǎn)換 JavaScript 的參數(shù)為可驗(yàn)證的類型對(duì)象。一個(gè)請(qǐng)求中的 body 數(shù)據(jù)是不包含類型信息的,Class-validator 需要使用前面定義過的 DTO,就需要做一個(gè)類型轉(zhuǎn)換。
最后,如前所述,這就是一個(gè)驗(yàn)證管道,它要么返回值不變,要么拋出異常。
最后一步是設(shè)置 ValidationPipe 。管道,與異常過濾器相同,它們可以是方法范圍的、控制器范圍的和全局范圍的。另外,管道可以是參數(shù)范圍的。我們可以直接將管道實(shí)例綁定到路由參數(shù)裝飾器,例如@Body()。讓我們來看看下面的例子:
cats.controller.ts
@Post()
async create(@Body(new ValidationPipe()) createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
當(dāng)驗(yàn)證邏輯僅涉及一個(gè)指定的參數(shù)時(shí),參數(shù)范圍的管道非常有用。要在方法級(jí)別設(shè)置管道,您需要使用 UsePipes() 裝飾器。
cats.controller.ts
@Post()
@UsePipes(new ValidationPipe())
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
@UsePipes() 修飾器是從 @nestjs/common 包中導(dǎo)入的。
在上面的例子中 ValidationPipe 的實(shí)例已就地立即創(chuàng)建。另一種可用的方法是直接傳入類(而不是實(shí)例),讓框架承擔(dān)實(shí)例化責(zé)任,并啟用依賴注入。
cats.controller.ts
@Post()
@UsePipes(ValidationPipe)
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
由于 ValidationPipe 被創(chuàng)建為盡可能通用,所以我們將把它設(shè)置為一個(gè)全局作用域的管道,用于整個(gè)應(yīng)用程序中的每個(gè)路由處理器。
main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();
在 混合應(yīng)用中 useGlobalPipes() 方法不會(huì)為網(wǎng)關(guān)和微服務(wù)設(shè)置管道, 對(duì)于標(biāo)準(zhǔn)(非混合) 微服務(wù)應(yīng)用使用 useGlobalPipes() 全局設(shè)置管道。
全局管道用于整個(gè)應(yīng)用程序、每個(gè)控制器和每個(gè)路由處理程序。就依賴注入而言,從任何模塊外部注冊(cè)的全局管道(如上例所示)無法注入依賴,因?yàn)樗鼈儾粚儆谌魏文K。為了解決這個(gè)問題,可以使用以下構(gòu)造直接為任何模塊設(shè)置管道:
app.module.ts
import { Module } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_PIPE,
useClass: ValidationPipe
}
]
})
export class AppModule {}
請(qǐng)注意使用上述方式依賴注入時(shí),請(qǐng)牢記無論你采用那種結(jié)構(gòu)模塊管道都是全局的,那么它應(yīng)該放在哪里呢?使用 ValidationPipe 定義管道 另外,useClass 并不是處理自定義提供者注冊(cè)的唯一方法。在這里了解更多。
驗(yàn)證不是管道唯一的用處。在本章的開始部分,我已經(jīng)提到管道也可以將輸入數(shù)據(jù)轉(zhuǎn)換為所需的輸出。這是可以的,因?yàn)閺?nbsp;transform 函數(shù)返回的值完全覆蓋了參數(shù)先前的值。在什么時(shí)候使用?有時(shí)從客戶端傳來的數(shù)據(jù)需要經(jīng)過一些修改(例如字符串轉(zhuǎn)化為整數(shù)),然后處理函數(shù)才能正確的處理。還有種情況,比如有些數(shù)據(jù)具有默認(rèn)值,用戶不必傳遞帶默認(rèn)值參數(shù),一旦用戶不傳就使用默認(rèn)值。轉(zhuǎn)換管道被插入在客戶端請(qǐng)求和請(qǐng)求處理程序之間用來處理客戶端請(qǐng)求。
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;
}
}
如下所示, 我們可以很簡(jiǎn)單的配置管道來處理所參數(shù) id:
@Get(':id')
async findOne(@Param('id', new ParseIntPipe()) id) {
return await this.catsService.findOne(id);
}
由于上述結(jié)構(gòu),ParseIntpipe 將在請(qǐng)求觸發(fā)相應(yīng)的處理程序之前執(zhí)行。
另一個(gè)有用的例子是按 ID 從數(shù)據(jù)庫(kù)中選擇一個(gè)現(xiàn)有的用戶實(shí)體。
@Get(':id')
findOne(@Param('id', UserByIdPipe) userEntity: UserEntity) {
return userEntity;
}
如果愿意你還可以試試 ParseUUIDPipe 管道, 它用來分析驗(yàn)證字符串是否是 UUID.
@Get(':id')
async findOne(@Param('id', new ParseUUIDPipe()) id) {
return await this.catsService.findOne(id);
}
ParseUUIDPipe 會(huì)使用 UUID 3,4,5 版本 來解析字符串, 你也可以單獨(dú)設(shè)置需要的版本.
你也可以試著做一個(gè)管道自己通過 id 找到實(shí)體數(shù)據(jù):
@Get(':id')
findOne(@Param('id', UserByIdPipe) userEntity: UserEntity) {
return userEntity;
}
請(qǐng)讀者自己實(shí)現(xiàn), 這個(gè)管道接收 id 參數(shù)并返回 UserEntity 數(shù)據(jù), 這樣做就可以抽象出一個(gè)根據(jù) id 得到 UserEntity 的公共管道, 你的程序變得更符合聲明式(Declarative 更好的代碼語義和封裝方式), 更 DRY (Don’t repeat yourself 減少重復(fù)代碼) 編程規(guī)范.
幸運(yùn)的是,由于 ValidationPipe 和 ParseIntPipe 是內(nèi)置管道,因此您不必自己構(gòu)建這些管道(請(qǐng)記住, ValidationPipe 需要同時(shí)安裝 class-validator 和 class-transformer 包)。與本章中構(gòu)建ValidationPipe的示例相比,該內(nèi)置的功能提供了更多的選項(xiàng),為了說明管道的基本原理,該示例一直保持基本狀態(tài)。您可以在此處找到完整的詳細(xì)信息以及許多示例。
更多建議: