# 管道
管道是一個用`@Injectable()`裝飾器注解的類,它實現了`PipeTransform`接口。

管道有兩個類型:
- **轉換**:管道將輸入數據轉換為所需的數據輸出(例如,從字符串到整數)
- **驗證**:對輸入數據進行驗證,如果驗證成功繼續傳遞; 驗證失敗則拋出異常;
在這兩種情況下, 管道 `參數(arguments)` 會由 [控制器(controllers)的路由處理程序](https://docs.nestjs.com/controllers#route-parameters) 進行處理. Nest 會在調用這個方法之前插入一個管道,管道會先攔截方法的調用參數,進行轉換或是驗證處理,然后用轉換好或是驗證好的參數調用原方法。
Nest 帶有許多內置管道,您可以開箱即用。您還可以構建自己的自定義管道。在本章中,我們將介紹內置管道并展示如何將它們綁定到路由處理程序。然后,我們將檢查幾個定制的管道,以展示如何從頭開始構建一個。
> 管道在異常區域內運行。這意味著當拋出異常時,它們由核心異常處理程序和應用于當前上下文的 [異常過濾器](https://docs.nestjs.com/exception-filters) 處理。當在 Pipe 中發生異常,controller 不會繼續執行任何方法。
## 內置管道
`Nest` 自帶八個開箱即用的管道,即
- `ValidationPipe`
- `ParseIntPipe`
- `ParseBoolPipe`
- `ParseArrayPipe`
- `ParseUUIDPipe`
- `DefaultValuePipe`
- `ParseEnumPipe`
- `ParseFloatPipe`
他們從 `@nestjs/common` 包中導出。為了更好地理解它們是如何工作的,我們將從頭開始構建它們。
讓我們快速瀏覽一下使用`ParseIntPipe`.這是**轉換**用例的示例,其中管道確保將方法處理程序參數轉換為 JavaScript 整數(或在轉換失敗時引發異常)。在本章后面,我們將展示一個簡單的自定義實現`ParseIntPipe`。下面的示例技術也適用于其他內置轉換管道(`ParseBoolPipe`、`ParseFloatPipe`、和`ParseEnumPipe`,我們將在本章中將其稱為管道)。`ParseArrayPipe``ParseUUIDPipe``Parse*`
#### 綁定管道[#](#binding-pipes)
要使用管道,我們需要將管道類的實例綁定到適當的上下文。在我們的`ParseIntPipe`示例中,我們希望將管道與特定的路由處理程序方法相關聯,并確保它在調用該方法之前運行。我們使用以下構造來做到這一點,我們將其稱為在方法參數級別綁定管道:
~~~typescript
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
return this.catsService.findOne(id);
}
~~~
這確保了以下兩個條件之一為真:我們在`findOne()`方法中收到的參數是一個數字(正如我們對 的調用所期望的那樣`this.catsService.findOne()`),或者在調用路由處理程序之前拋出異常。
例如,假設路由被稱為:
~~~bash
GET localhost:3000/abc
~~~
Nest 會拋出這樣的異常:
~~~json
{
"statusCode": 400,
"message": "Validation failed (numeric string is expected)",
"error": "Bad Request"
}
~~~
該異常將阻止`findOne()`方法的主體執行。
在上面的示例中,我們傳遞了一個類 (`ParseIntPipe`),而不是實例,將實例化的責任留給了框架并啟用了依賴注入。與管道和守衛一樣,我們可以傳遞一個就地實例。如果我們想通過傳遞選項來自定義內置管道的行為,傳遞就地實例很有用:
~~~typescript
@Get(':id')
async findOne(
@Param('id', new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE }))
id: number,
) {
return this.catsService.findOne(id);
}
~~~
綁定其他轉換管道(所有**Parse\***管道)的工作方式類似。這些管道都在驗證路由參數、查詢字符串參數和請求正文值的上下文中工作。
例如使用查詢字符串參數:
~~~typescript
@Get()
async findOne(@Query('id', ParseIntPipe) id: number) {
return this.catsService.findOne(id);
}
~~~
這是一個使用`ParseUUIDPipe`解析字符串參數并驗證它是否為 UUID 的示例。
~~~typescript
@Get(':uuid')
async findOne(@Param('uuid', new ParseUUIDPipe()) uuid: string) {
return this.catsService.findOne(uuid);
}
~~~
> **提示**:使用`ParseUUIDPipe()`時解析版本 3、4 或 5 中的 UUID,如果您只需要特定版本的 UUID,則可以在管道選項中傳遞版本。
上面我們已經看到了綁定各種`Parse*`內置管道系列的示例。綁定驗證管道有點不同;我們將在下一節討論這個問題。
> **提示**:此外,請參閱[驗證技術](https://docs.nestjs.com/techniques/validation)以獲取驗證管道的廣泛示例。
#### 自定義管道[#](https://docs.nestjs.com/pipes#custom-pipes)
如前所述,您可以構建自己的自定義管道。雖然 Nest 提供了強大的內置`ParseIntPipe`和`ValidationPipe`,但讓我們從頭開始構建每個簡單的自定義版本,以了解如何構建自定義管道。
我們從 `ValidationPipe`. 開始。 首先它只接受一個值并立即返回相同的值,其行為類似于一個標識函數。
> validate.pipe.ts
```typescript
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()` 方法。 這個方法有兩個參數:
- `value`
- `metadata`
`value` 是當前處理的參數,而 `metadata` 是其元數據。元數據對象包含一些屬性:
```typescript
export interface ArgumentMetadata {
type: 'body' | 'query' | 'param' | 'custom';
metatype?: Type<unknown>;
data?: string;
}
```
這里有一些屬性描述參數:
| 參數 | 描述 |
| -------- | ------------------------------------------------------------------------------------------------------------------------------ |
| type | 告訴我們該屬性是一個 body `@Body()`,query `@Query()`,param `@Param()` 還是自定義參數 [在這里閱讀更多](https://docs.nestjs.com/custom-decorators)。 |
| metatype | 屬性的元類型,例如 `String`。 如果在函數簽名中省略類型聲明,或者使用原生 JavaScript,則為 `undefined`。 |
| data | 傳遞給裝飾器的字符串,例如 `@Body('string')`。 如果您將括號留空,則為 `undefined`。 |
> `TypeScript`接口在編譯期間消失,所以如果你使用接口而不是類,那么 `metatype` 的值將是一個 `Object`。
## 基于模式的驗證
讓我們的驗證管道更有用一點。仔細看看 的`create()`方法`CatsController`,我們可能希望在嘗試運行我們的服務方法之前確保 post body 對象是有效的。
> cats.controller.ts
```typescript
@Post()
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
```
下面是 `CreateCatDto` 參數. 類型為 CreateCatDto:
> create-cat.dto.ts
```typescript
export class CreateCatDto {
name: string;
age: number;
breed: string;
}
```
我們要確保`create`方法能正確執行,所以必須驗證 `CreateCatDto` 里的三個屬性。我們可以在路由處理程序方法中做到這一點,但是我們會打破單個責任原則(SRP)。另一種方法是創建一個驗證器類并在那里委托任務,但是不得不每次在方法開始的時候我們都必須使用這個驗證器。
如何創建驗證中間件? 這可能是一個好主意,但我們不可能創建一個整個應用程序通用的中間件(因為中間件不知道 `execution context`執行環境,也不知道要調用的函數和它的參數)。
當然,這正是管道設計的用例。因此,讓我們繼續完善我們的驗證管道。
## 對象結構驗證
有幾種方法可以實現,一種常見的方式是使用**基于結構**的驗證。[Joi](https://github.com/hapijs/joi) 庫是允許您使用一個可讀的 API 以非常簡單的方式創建 schema,讓我們構建一個使用基于 Joi 的模式的驗證管道。
首先安裝所需的軟件包:
~~~bash
$ npm install --save joi
$ npm install --save-dev @types/joi
~~~
在下面的代碼示例中,我們創建了一個以模式作為`constructor`參數的簡單類。然后我們應用該`schema.validate()`方法,該方法根據提供的模式驗證我們的傳入參數。
就像是前面說過的,`驗證管道` 要么返回該值,要么拋出一個錯誤。
在下一節中,你將看到我們如何使用 `@UsePipes()` 修飾器給指定的控制器方法提供需要的 schema。這樣做使我們的驗證管道可以跨上下文重用,就像我們開始做的那樣。
```typescript
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;
}
}
```
## 綁定管道
早些時候,我們看到了如何綁定轉換管道(比如`ParseIntPipe`和其他`Parse*`管道)。
綁定驗證管道也非常簡單。
在這種情況下,我們希望在方法調用級別綁定管道。在我們當前的示例中,我們需要執行以下操作才能使用`JoiValidationPipe`:
1. 創建一個實例`JoiValidationPipe`
2. 在管道的類構造函數中傳遞特定于上下文的 Joi 模式
3. 將管道綁定到方法
我們使用`@UsePipes()`如下所示的裝飾器來做到這一點:
```typescript
@Post()
@UsePipes(new JoiValidationPipe(createCatSchema))
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
```
>**提示:**`@UsePipes()`裝飾器是從`@nestjs/common`包 中導入的。
## 類驗證器
> 本節中的技術需要 `TypeScript` ,如果您的應用是使用原始 `JavaScript`編寫的,則這些技術不可用。
讓我們看一下驗證的另外一種實現方式
Nest 與 [class-validator](https://github.com/pleerock/class-validator) 配合得很好。這個優秀的庫允許您使用基于裝飾器的驗證。裝飾器的功能非常強大,尤其是與 Nest 的 Pipe 功能相結合使用時,因為我們可以通過訪問 `metatype` 信息做很多事情,在開始之前需要安裝一些依賴。
```
$ npm i --save class-validator class-transformer
```
一旦安裝了這些,我們就可以在`CreateCatDto`類中添加一些裝飾器。在這里,我們看到了這種技術的一個顯著優勢:`CreateCatDto`該類仍然是我們的 Post body 對象的唯一真實來源(而不是必須創建一個單獨的驗證類)。
> create-cat.dto.ts
```typescript
import { IsString, IsInt } from 'class-validator';
export class CreateCatDto {
@IsString()
name: string;
@IsInt()
age: number;
@IsString()
breed: string;
}
```
> 在[此處](https://github.com/typestack/class-validator#usage)了解有關類驗證器修飾符的更多信息。
現在我們來創建一個 `ValidationPipe` 類。
> validate.pipe.ts
```typescript
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);
}
}
```
>我們已經使用了 [class-transformer](https://github.com/pleerock/class-transformer) 庫。它和 [class-validator](https://github.com/pleerock/class-validator) 庫由同一個作者開發,所以他們配合的很好。
讓我們來看看這個代碼。首先你會發現 `transform()` 函數是 `異步` 的, Nest 支持**同步**和**異步**管道。這樣做的原因是因為有些 `class-validator` 的驗證是[可以異步的](typestack/class-validator#custom-validation-classes)(利用Promises)
接下來請注意,我們正在使用解構賦值(從 `ArgumentMetadata` 中提取參數)到方法中。這是一個先獲取全部 `ArgumentMetadata` 然后用附加語句提取某個變量的簡寫方式。
下一步,請觀察 `toValidate()` 方法。當驗證類型不是 JavaScript 的數據類型時,跳過驗證。
接下來,我們使用 `class-transformer` 的 `plainToClass()` 方法我們的純 JavaScript 參數對象轉換為類型化對象,以便我們可以應用驗證。我們必須這樣做的原因是傳入的 `post body` 對象在從網絡請求反序列化時**沒有任何類型信息**(這是底層平臺(例如 Express)的工作方式)。Class-validator 需要使用我們之前為 DTO 定義的驗證裝飾器,因此我們需要執行此轉換以將傳入的主體視為經過適當裝飾的對象,而不僅僅是普通的 `vanilla` 對象。
最后,如前所述,這就是一個驗證管道,它要么返回值不變,要么拋出異常。
最后一步是設置 `ValidationPipe` 。管道,與[異常過濾器](exceptionfilters.md)相同,它們可以是可以是參數范圍、方法范圍、控制器范圍或全局范圍。早些時候,在我們基于 Joi 的驗證管道中,我們看到了在方法級別綁定管道的示例。在下面的示例中,我們將管道實例綁定到路由處理程序`@Body()`裝飾器,以便調用我們的管道來驗證。
> cats.controller.ts
```typescript
@Post()
async create(@Body(new ValidationPipe()) createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
```
當驗證邏輯僅涉及一個指定的參數時,參數范圍的管道非常有用。要在方法級別設置管道,您需要使用 `UsePipes()` 裝飾器。
> cats.controller.ts
```typescript
@Post()
@UsePipes(new ValidationPipe())
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
```
## 全局管道[#](https://docs.nestjs.com/pipes#global-scoped-pipes)
由于 `ValidationPipe` 被創建為盡可能通用,所以我們將把它設置為一個**全局作用域**的管道,用于整個應用程序中的每個路由處理器。
> main.ts
```typescript
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();
```
> 在 [混合應用](https://docs.nestjs.com/faq/hybrid-application)中 `useGlobalPipes()` 方法不會為網關和微服務設置管道, 對于標準(非混合) 微服務應用使用 `useGlobalPipes()` 全局設置管道。
全局管道用于整個應用程序、每個控制器和每個路由處理程序。請注意,就依賴注入而言,從任何模塊外部注冊的全局管道(如上例所示)無法注入依賴,因為它們不屬于任何模塊。為了解決這個問題,可以使用以下構造直接為任何模塊設置管道:
> app.module.ts
```typescript
import { Module } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_PIPE,
useClass: ValidationPipe
}
]
})
export class AppModule {}
```
> 請注意使用上述方式依賴注入時,請牢記無論你采用那種結構模塊管道都是全局的,那么它應該放在哪里呢?使用 `ValidationPipe` 定義管道 另外,useClass 并不是處理自定義提供者注冊的唯一方法。在[這里](https://docs.nestjs.com/fundamentals/custom-providers)了解更多。
## 內置驗證管道
幸運的是,由于 `ValidationPipe` 和 `ParseIntPipe` 是內置管道,因此您不必自己構建這些管道(請記住, `ValidationPipe` 需要同時安裝 `class-validator` 和 `class-transformer` 包)。與本章中構建ValidationPipe的示例相比,該內置的功能提供了更多的選項,為了說明管道的基本原理,該示例一直保持基本狀態。您可以在[此處](https://docs.nestjs.com/techniques/validation)找到完整的詳細信息以及許多示例。
## 轉換管道
驗證不是管道唯一的用處。在本章的開始部分,我已經提到管道也可以將輸入數據**轉換**為所需的輸出。這是可以的,因為從 `transform` 函數返回的值完全覆蓋了參數先前的值。
在什么時候使用?有時從客戶端傳來的數據需要經過一些修改(例如字符串轉化為整數),然后處理函數才能正確的處理。還有種情況,比如有些數據具有默認值,用戶不必傳遞帶默認值參數,一旦用戶不傳就使用默認值。**轉換管道**可以通過在客戶端請求和請求處理程序之間插入處理功能來執行這些功能。
這是一個`ParseIntPipe`負責將字符串解析為整數值的簡單程序。(如上所述,Nest 具有`ParseIntPipe`更復雜的內置功能;我們將其作為自定義轉換管道的簡單示例包含在內)。
> parse-int.pipe.ts
```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;
}
}
```
如下所示, 我們可以很簡單的配置管道來處理所參數 id:
```typescript
@Get(':id')
async findOne(@Param('id', new ParseIntPipe()) id) {
return await this.catsService.findOne(id);
}
```
由于上述結構,`ParseIntpipe` 將在請求觸發相應的處理程序之前執行。
另一個有用的例子是按 ID 從數據庫中選擇一個現有的**用戶實體**。
```typescript
@Get(':id')
findOne(@Param('id', UserByIdPipe) userEntity: UserEntity) {
return userEntity;
}
```
如果愿意你還可以試試 `ParseUUIDPipe` 管道, 它用來分析驗證字符串是否是 UUID.
```typescript
@Get(':id')
async findOne(@Param('id', new ParseUUIDPipe()) id) {
return await this.catsService.findOne(id);
}
```
> `ParseUUIDPipe` 會使用 UUID 3,4,5 版本 來解析字符串, 你也可以單獨設置需要的版本.
你也可以試著做一個管道自己通過 id 找到實體數據:
```typescript
@Get(':id')
findOne(@Param('id', UserByIdPipe) userEntity: UserEntity) {
return userEntity;
}
```
請讀者自己實現, 這個管道接收 id 參數并返回 UserEntity 數據, 這樣做就可以抽象出一個根據 id 得到 UserEntity 的公共管道, 你的程序變得更符合聲明式(Declarative 更好的代碼語義和封裝方式), 更 [DRY (Don't repeat yourself 減少重復代碼) ](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself)編程規范.
#### 提供默認值[#](#providing-defaults)
`Parse*`管道期望定義一個參數的值。`null`他們在接收或`undefined`值時拋出異常。`Parse*`為了允許端點處理丟失的查詢字符串參數值,我們必須在管道對這些值進行操作之前提供要注入的默認值。服務于這個`DefaultValuePipe`目的。`DefaultValuePipe`只需在相關管道之前在`@Query()`裝飾器中實例化 a`Parse*`,如下所示:
~~~typescript
@Get()
async findAll(
@Query('activeOnly', new DefaultValuePipe(false), ParseBoolPipe) activeOnly: boolean,
@Query('page', new DefaultValuePipe(0), ParseIntPipe) page: number,
) {
return this.catsService.findAll({ activeOnly, page });
}
~~~
- 介紹
- 概述
- 第一步
- 控制器
- 提供者
- 模塊
- 中間件
- 異常過濾器
- 管道
- 守衛
- 攔截器
- 自定義裝飾器
- 基礎知識
- 自定義提供者
- 異步提供者
- 動態模塊
- 注入作用域
- 循環依賴
- 模塊參考
- 懶加載模塊
- 應用上下文
- 生命周期事件
- 跨平臺
- 測試
- 技術
- 數據庫
- Mongo
- 配置
- 驗證
- 緩存
- 序列化
- 版本控制
- 定時任務
- 隊列
- 日志
- Cookies
- 事件
- 壓縮
- 文件上傳
- 流式處理文件
- HTTP模塊
- Session(會話)
- MVC
- 性能(Fastify)
- 服務器端事件發送
- 安全
- 認證(Authentication)
- 授權(Authorization)
- 加密和散列
- Helmet
- CORS(跨域請求)
- CSRF保護
- 限速
- GraphQL
- 快速開始
- 解析器(resolvers)
- 變更(Mutations)
- 訂閱(Subscriptions)
- 標量(Scalars)
- 指令(directives)
- 接口(Interfaces)
- 聯合類型
- 枚舉(Enums)
- 字段中間件
- 映射類型
- 插件
- 復雜性
- 擴展
- CLI插件
- 生成SDL
- 其他功能
- 聯合服務
- 遷移指南
- Websocket
- 網關
- 異常過濾器
- 管道
- 守衛
- 攔截器
- 適配器
- 微服務
- 概述
- Redis
- MQTT
- NATS
- RabbitMQ
- Kafka
- gRPC
- 自定義傳輸器
- 異常過濾器
- 管道
- 守衛
- 攔截器
- 獨立應用
- Cli
- 概述
- 工作空間
- 庫
- 用法
- 腳本
- Openapi
- 介紹
- 類型和參數
- 操作
- 安全
- 映射類型
- 裝飾器
- CLI插件
- 其他特性
- 遷移指南
- 秘籍
- CRUD 生成器
- 熱重載
- MikroORM
- TypeORM
- Mongoose
- 序列化
- 路由模塊
- Swagger
- 健康檢查
- CQRS
- 文檔
- Prisma
- 靜態服務
- Nest Commander
- 問答
- Serverless
- HTTP 適配器
- 全局路由前綴
- 混合應用
- HTTPS 和多服務器
- 請求生命周期
- 常見錯誤
- 實例
- 遷移指南
- 發現
- 誰在使用Nest?