iT邦幫忙

2021 iThome 鐵人賽

DAY 12
1
Modern Web

NestJS 帶你飛!系列 第 12

[NestJS 帶你飛!] DAY12 - Interceptor

什麼是 Interceptor?

中文名稱為攔截器,受到 剖面導向程式設計 (Aspect Oriented Programming) 的啟發,為原功能的擴展邏輯,其特點如下:

  • 可以在執行 Controller 的方法 之前之後 設計邏輯。
  • 執行於 Controller 的方法 之前 的 Interceptor 邏輯會在 Pipe 執行 之前 觸發。
  • 執行於 Middleware 之後
  • 可以更動資料與 Exception。

https://ithelp.ithome.com.tw/upload/images/20210405/201193386hVCd5hrJj.png

設計 Interceptor

Interceptor 可以透過 CLI 產生:

$ nest generate interceptor <INTERCEPTOR_NAME>

注意<INTERCEPTOR_NAME> 可以含有路徑,如:interceptors/hello-world,這樣就會在 src 資料夾下建立該路徑並含有 Interceptor。

這邊我建立一個 HelloWorldInterceptorinterceptors 資料夾下:

$ nest generate interceptor interceptors/hello-world

src 底下會看見一個名為 interceptors 的資料夾,裡面有 hello-world.interceptor.ts 以及 hello-world.interceptor.spec.ts
https://ithelp.ithome.com.tw/upload/images/20210405/20119338z3UPmMzUqs.png

建立出來的 Interceptor 骨架如下,會發現 Interceptor 其實也是帶有 @Injectable 裝飾器的 class,不過它必須實作 NestInterceptor 介面,並設計 intercept(context: ExecutionContext, next: CallHandler) 方法:

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

@Injectable()
export class HelloWorldInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle();
  }
}

CallHandler

CallHandler 為 Interceptor 的重要成員,它實作了 handle() 來調用路由處理的方法,進而導入對應的 Controller 方法,也就是說,如果在 Interceptor 不回傳 CallHandlerhandle(),將會使路由處理失去運作。

由於 CallHandlerintercept 方法的參數,故其一定是在 intercept 中被呼叫,也就是說,可以在回傳 handle() 之前 寫一段邏輯,使其可以在進入 Controller 的方法前被執行,又因為 handle() 回傳的是 Observable,故可以透過 pipe 的方式 對回傳值做調整,使其可以在 Controller 的方法執行之後處理其他邏輯。

注意handle()Observable,我們把它作為 intercept 的回傳值是希望 Nest 可以去 subscribe 它,根據 Observable 的特性,若沒有去 subscribe 它則不會執行其內部邏輯,這也是為什麼不回傳 handle() 的話將會使路由處理失去運作的原因。

ExecutionContext

ExecutionContext 是繼承 ArgumentsHostclass,其提供了更多關於此請求的相關訊息,下方為它提供的兩個方法,透過這兩個方法可以大幅提升應用的靈活性:

取得 Controller Class

透過 getClass() 取得當前請求對應的 Controller Class:

const Controller: TodoController = context.getClass<TodoController>();

取得 Controller method

透過 getHandler() 取得當前請求對應的 Controller method,假設當前請求會呼叫 TodoControllergetAll(),那就會回傳 getAll 這個函式:

const method: Function = context.getHandler();

使用 Interceptor

在使用之前,先將 hello-world.interceptor.ts 修改一下,在進入 Interceptor 時印出 Hello World! 並使用變數儲存進入的時間,再透過 tap 印出結束的時間與進入的時間差:

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

@Injectable()
export class HelloWorldInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    console.log('Hello World!');
    const input = Date.now();
    const handler = next.handle();
    return handler.pipe(
      tap(() => console.log(`${ Date.now() - input } ms`))
    );
  }
}

修改完以後就來使用此 Interceptor,透過 @UseInterceptors 裝飾器即可輕鬆套用,使用的方式大致上可以分成兩種:

  1. 單一資源:在 Controller 的方法中套用 @UseInterceptors 裝飾器,只會針對該資源套用。
  2. Controller:直接在 Controller 上套用 @UseInterceptors 裝飾器,會針對整個 Controller 中的資源套用。

下方以套用在 Controller 為例,修改 app.controller.ts

import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { AppService } from './app.service';
import { HelloWorldInterceptor } from './interceptors/hello-world.interceptor';

@Controller()
@UseInterceptors(HelloWorldInterceptor)
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

透過瀏覽器查看 http://localhost:3000 會發現終端機出現了下方結果:

Hello World!
3 ms

全域 Interceptor

如果設計了一個共用的 Interceptor 要套用在所有資源上的話,只需要修改 main.ts 即可,透過 useGlobalInterceptors 來配置全域 Interceptor:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { HelloWorldInterceptor } from './interceptors/hello-world.interceptor';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalInterceptors(new HelloWorldInterceptor());
  await app.listen(3000);
}
bootstrap();

用依賴注入實作全域 Interceptor

上面的方法是透過模組外部完成全域配置的,與 Pipe 一樣可以用依賴注入的方式,透過指定 Provider 的 tokenAPP_INTERCEPTOR 來實現,這裡是用 useClass 來指定要建立實例的類別:

import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { HelloWorldInterceptor } from './interceptors/hello-world.interceptor';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [
    AppService,
    {
      provide: APP_INTERCEPTOR,
      useClass: HelloWorldInterceptor
    }
  ]
})
export class AppModule {}

小結

Interceptor 可以在不修改 Controller 的情況下去擴充邏輯,是十分方便的功能。這裡附上今天的懶人包:

  1. Interceptor 執行於 Middleware 之後,但可執行於 Pipe 與 Controller 之前與之後。
  2. 可以在不變動原本邏輯的情況下去擴充邏輯。
  3. CallHandler 為重要成員,需要呼叫其 handle() 來讓路由機制得以運行。
  4. ExecutionContext 提供了 getClass()getHandler() 來提升靈活性。
  5. 全域 Interceptor 可以透過依賴注入的方式實作。

上一篇
[NestJS 帶你飛!] DAY11 - Middleware
下一篇
[NestJS 帶你飛!] DAY13 - Guard
系列文
NestJS 帶你飛!32

1 則留言

0
mihuartuanr
iT邦新手 5 級 ‧ 2021-11-05 11:40:12

CallHandler 為 Interceptor 的重要成員,它實作了 handle() 來調用路由處理的方法,進而導入對應的 Controller 方法,也就是說,如果在 Interceptor 不調用 CallHandler 的 handle(),將會使路由處理失去運作。

您好,根据文中示例,尝试了一下intercept返回不是CallHandler.handle()及其rxjs/operators处理结果的情况,发现也會使路由處理失去運作。

如果在 Interceptor 不調用 CallHandler 的 handle(),將會使路由處理失去運作。

改为"如果在 Interceptor 不調用 CallHandler 的 handle()并将其结果返回,將會使路由處理失去運作。"是不是更精确些呢

HAO iT邦新手 3 級 ‧ 2021-11-05 15:47:48 檢舉

你好,不好意思,我說明的不夠精確,handle() 基本上就是呼叫 Controller 對應的 handler,並以 Observable 的形式讓我們可以用 RxJS 的技巧來針對 handler 處理完的資料做進一步的處理,如果返回值不是 handle() 所串聯的事件流,那 NestJS 就無法 subscribe 到它,而 Observable 沒有被 subscribe 的情況下是不會被執行的,就會造成所謂「路由處理失去運作」的情況。

我有更新文章進行補充了,感謝你的建議!

我要留言

立即登入留言