NestJS 攔截器

2023-09-08 11:36 更新

攔截器是使用 @Injectable() 裝飾器注解的類。攔截器應(yīng)該實(shí)現(xiàn) NestInterceptor 接口。

11

攔截器具有一系列有用的功能,這些功能受面向切面編程(AOP)技術(shù)的啟發(fā)。它們可以:

  • 在函數(shù)執(zhí)行之前/之后綁定額外的邏輯
  • 轉(zhuǎn)換從函數(shù)返回的結(jié)果
  • 轉(zhuǎn)換從函數(shù)拋出的異常
  • 擴(kuò)展基本函數(shù)行為
  • 根據(jù)所選條件完全重寫(xiě)函數(shù) (例如, 緩存目的)

基礎(chǔ)

每個(gè)攔截器都有 intercept() 方法,它接收2個(gè)參數(shù)。 第一個(gè)是 ExecutionContext 實(shí)例(與守衛(wèi)完全相同的對(duì)象)。 ExecutionContext 繼承自 ArgumentsHost 。 ArgumentsHost 是傳遞給原始處理程序的參數(shù)的一個(gè)包裝 ,它根據(jù)應(yīng)用程序的類型包含不同的參數(shù)數(shù)組。你可以在這里讀更多關(guān)于它的內(nèi)容(在異常過(guò)濾器章節(jié)中)。

執(zhí)行上下文

通過(guò)擴(kuò)展 ArgumentsHost,ExecutionContext 還添加了幾個(gè)新的幫助程序方法,這些方法提供有關(guān)當(dāng)前執(zhí)行過(guò)程的更多詳細(xì)信息。這些詳細(xì)信息有助于構(gòu)建可以在廣泛的控制器,方法和執(zhí)行上下文中使用的更通用的攔截器。ExecutionContext 在此處了解更多信息。

調(diào)用處理程序

第二個(gè)參數(shù)是 CallHandler。如果不手動(dòng)調(diào)用 handle() 方法,則主處理程序根本不會(huì)進(jìn)行求值。這是什么意思?基本上,CallHandler是一個(gè)包裝執(zhí)行流的對(duì)象,因此推遲了最終的處理程序執(zhí)行。

比方說(shuō),有人提出了 POST /cats 請(qǐng)求。此請(qǐng)求指向在 CatsController 中定義的 create() 處理程序。如果在此過(guò)程中未調(diào)用攔截器的 handle() 方法,則 create() 方法不會(huì)被計(jì)算。只有 handle() 被調(diào)用(并且已返回值),最終方法才會(huì)被觸發(fā)。為什么?因?yàn)镹est訂閱了返回的流,并使用此流生成的值來(lái)為最終用戶創(chuàng)建單個(gè)響應(yīng)或多個(gè)響應(yīng)。而且,handle() 返回一個(gè) Observable,這意味著它為我們提供了一組非常強(qiáng)大的運(yùn)算符,可以幫助我們進(jìn)行例如響應(yīng)操作。

截取切面

第一個(gè)用例是使用攔截器在函數(shù)執(zhí)行之前或之后添加額外的邏輯。當(dāng)我們要記錄與應(yīng)用程序的交互時(shí),它很有用,例如 存儲(chǔ)用戶調(diào)用,異步調(diào)度事件或計(jì)算時(shí)間戳。作為一個(gè)例子,我們來(lái)創(chuàng)建一個(gè)簡(jiǎn)單的例子 LoggingInterceptor。

logging.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    console.log('Before...');

    const now = Date.now();
    return next
      .handle()
      .pipe(
        tap(() => console.log(`After... ${Date.now() - now}ms`)),
      );
  }
}

NestInterceptor<T,R> 是一個(gè)通用接口,其中 T 表示已處理的 Observable<T> 的類型(在流后面),而 R 表示包含在返回的 Observable<R> 中的值的返回類型。

攔截器的作用與控制器,提供程序,守衛(wèi)等相同,這意味著它們可以通過(guò)構(gòu)造函數(shù)注入依賴項(xiàng)。

由于 handle() 返回一個(gè)RxJS Observable,我們有很多種操作符可以用來(lái)操作流。在上面的例子中,我們使用了 tap() 運(yùn)算符,該運(yùn)算符在可觀察序列的正?;虍惓=K止時(shí)調(diào)用函數(shù)。

綁定攔截器

為了設(shè)置攔截器, 我們使用從 @nestjs/common 包導(dǎo)入的 @UseInterceptors() 裝飾器。與守衛(wèi)一樣, 攔截器可以是控制器范圍內(nèi)的, 方法范圍內(nèi)的或者全局范圍內(nèi)的。

cats.controller.ts
@UseInterceptors(LoggingInterceptor)
export class CatsController {}

@UseInterceptors() 裝飾器從 @nestjs/common 導(dǎo)入。

由此,CatsController 中定義的每個(gè)路由處理程序都將使用 LoggingInterceptor。當(dāng)有人調(diào)用 GET /cats 端點(diǎn)時(shí),您將在控制臺(tái)窗口中看到以下輸出:

Before...
After... 1ms

請(qǐng)注意,我們傳遞的是 LoggingInterceptor 類型而不是實(shí)例,讓框架承擔(dān)實(shí)例化責(zé)任并啟用依賴注入。另一種可用的方法是傳遞立即創(chuàng)建的實(shí)例:

cats.controller.ts
@UseInterceptors(new LoggingInterceptor())
export class CatsController {}

如上所述, 上面的構(gòu)造將攔截器附加到此控制器聲明的每個(gè)處理程序。如果我們決定只限制其中一個(gè), 我們只需在方法級(jí)別設(shè)置攔截器。為了綁定全局?jǐn)r截器, 我們使用 Nest 應(yīng)用程序?qū)嵗?nbsp;useGlobalInterceptors() 方法:

const app = await NestFactory.create(ApplicationModule);
app.useGlobalInterceptors(new LoggingInterceptor());

全局?jǐn)r截器用于整個(gè)應(yīng)用程序、每個(gè)控制器和每個(gè)路由處理程序。在依賴注入方面, 從任何模塊外部注冊(cè)的全局?jǐn)r截器 (如上面的示例中所示) 無(wú)法插入依賴項(xiàng), 因?yàn)樗鼈儾粚儆谌魏文K。為了解決此問(wèn)題, 您可以使用以下構(gòu)造直接從任何模塊設(shè)置一個(gè)攔截器:

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

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

另一種選擇是使用執(zhí)行上下文功能。另外,useClass 并不是處理自定義提供商注冊(cè)的唯一方法。在這里了解更多。

響應(yīng)映射

我們已經(jīng)知道, handle() 返回一個(gè) Observable。此流包含從路由處理程序返回的值, 因此我們可以使用 map() 運(yùn)算符輕松地對(duì)其進(jìn)行改變。

響應(yīng)映射功能不適用于特定于庫(kù)的響應(yīng)策略(禁止直接使用 @Res() 對(duì)象)。

讓我們創(chuàng)建一個(gè) TransformInterceptor, 它將打包響應(yīng)并將其分配給 data 屬性。

transform.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

export interface Response<T> {
  data: T;
}

@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
  intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
    return next.handle().pipe(map(data => ({ data })));
  }
}

Nest 攔截器就像使用異步 intercept() 方法的魅力一樣, 意思是, 如果需要,您可以毫不費(fèi)力地將方法切換為異步。

之后,當(dāng)有人調(diào)用GET /cats端點(diǎn)時(shí),請(qǐng)求將如下所示(我們假設(shè)路由處理程序返回一個(gè)空 arry []):

{
    "data": []
}

攔截器在創(chuàng)建用于整個(gè)應(yīng)用程序的可重用解決方案時(shí)具有巨大的潛力。例如,我們假設(shè)我們需要將每個(gè)發(fā)生的 null 值轉(zhuǎn)換為空字符串 ''。我們可以使用一行代碼并將攔截器綁定為全局代碼。由于這一點(diǎn),它會(huì)被每個(gè)注冊(cè)的處理程序自動(dòng)重用。

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable()
export class ExcludeNullInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next
      .handle()
      .pipe(map(value => value === null ? '' : value ));
  }
}

異常映射

另一個(gè)有趣的用例是利用 catchError() 操作符來(lái)覆蓋拋出的異常:

exception.interceptor.ts
import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  BadGatewayException,
  CallHandler,
} from '@nestjs/common';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';

@Injectable()
export class ErrorsInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next
      .handle()
      .pipe(
        catchError(err => throwError(new BadGatewayException())),
      );
  }
}

Stream 重寫(xiě)

有時(shí)我們可能希望完全阻止調(diào)用處理程序并返回不同的值 (例如, 由于性能問(wèn)題而從緩存中獲取), 這是有多種原因的。一個(gè)很好的例子是緩存攔截器,它將使用一些TTL存儲(chǔ)緩存的響應(yīng)。不幸的是, 這個(gè)功能需要更多的代碼并且由于簡(jiǎn)化, 我們將僅提供簡(jiǎn)要解釋主要概念的基本示例。

cache.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable, of } from 'rxjs';

@Injectable()
export class CacheInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const isCached = true;
    if (isCached) {
      return of([]);
    }
    return next.handle();
  }
}

這是一個(gè) CacheInterceptor,帶有硬編碼的 isCached 變量和硬編碼的響應(yīng) [] 。我們?cè)谶@里通過(guò) of 運(yùn)算符創(chuàng)建并返回了一個(gè)新的流, 因此路由處理程序根本不會(huì)被調(diào)用。當(dāng)有人調(diào)用使用 CacheInterceptor 的端點(diǎn)時(shí), 響應(yīng) (一個(gè)硬編碼的空數(shù)組) 將立即返回。為了創(chuàng)建一個(gè)通用解決方案, 您可以利用 Reflector 并創(chuàng)建自定義修飾符。反射器 Reflector 在守衛(wèi)章節(jié)描述的很好。

更多操作符

使用 RxJS 運(yùn)算符操作流的可能性為我們提供了許多功能。讓我們考慮另一個(gè)常見(jiàn)的用例。假設(shè)您要處理路由請(qǐng)求超時(shí)。如果您的端點(diǎn)在一段時(shí)間后未返回任何內(nèi)容,則您將以錯(cuò)誤響應(yīng)終止。以下構(gòu)造可實(shí)現(xiàn)此目的:

timeout.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, RequestTimeoutException } from '@nestjs/common';
import { Observable, throwError, TimeoutError } from 'rxjs';
import { catchError, timeout } from 'rxjs/operators';

@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      timeout(5000),
      catchError(err => {
        if (err instanceof TimeoutError) {
          return throwError(new RequestTimeoutException());
        }
        return throwError(err);
      }),
    );
  };
};

5秒后,請(qǐng)求處理將被取消。您還可以在拋出之前添加自定義邏輯RequestTimeoutException(例如,釋放資源)。


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

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)