nestjs Interceptor 提供一個元件可以攔截 Controller 內部 handler 處理的前後邏輯,並且可以對輸入輸出結果做處理。
nestjs Interceptor 元件主要是受到 AOP (也就是關注點導向程式設計)的啟發。希望透過 Interceptor 把一些非功能性的處理邏輯(比如錯誤處理或是驗證授權)獨立出來。能夠在不影響功能性的處理邏輯下,增強原本的邏輯。
在原本的物件導向世界,是透過物件把職責切分,物件間的互動來建構整個系統功能。然後一些在功能性的一些邏輯,比如在上傳檔案功能中的驗證授權或是稽核紀錄,這類邏輯往往無法在一開始就做設計,變成需要不斷去修改架構。為了這些功能可以更符合開閉原則,在其之上衍生出關注點導向的設計(也就是AOP),把這類邏輯稱為關注點,透過代理物件把這類邏輯隔離出來。
關注點導向的設計(也就是AOP)特色,在於基於原本的邏輯,把一些非功能性的職責獨立在關注點的部份。比如對原本邏輯上的例外處理或是權限驗證。這個部份,在 nestjs 框架提供了 Interceptor 元件來做這樣的邏輯管理。
要實作 Interceptor,需要實作以下條件:
import {
CallHandler,
ExecutionContext,
HttpException,
Injectable,
Logger,
NestInterceptor,
} from '@nestjs/common';
import { Response } from 'express';
import { Observable, catchError, tap, throwError } from 'rxjs';
import { RequestService } from 'src/request.service';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger(LoggingInterceptor.name);
constructor(private readonly requestService: RequestService) {}
intercept(
context: ExecutionContext,
next: CallHandler<unknown>,
): Observable<unknown> | Promise<Observable<unknown>> {
const request = context.switchToHttp().getRequest();
const userAgent = request.get('user-agent') ?? '';
const { ip, method, path: url } = request;
this.logger.log(
`${method} ${url} ${userAgent} ${ip}: ${context.getClass().name} ${
context.getHandler().name
} invoked...`,
);
this.logger.debug('userId:', this.requestService.getUserId());
const now = Date.now();
return next.handle().pipe(
tap((res) => {
const response = context.switchToHttp().getResponse();
const { statusCode } = response;
const contentLength = response.get('content-length');
this.logger.log(
`${method} ${url} ${statusCode} ${contentLength} - ${userAgent} ${ip}: ${
Date.now() - now
} ms`,
);
this.logger.debug('Response:', res);
}),
catchError((error) => {
const response: Response = context.switchToHttp().getResponse();
// const { statusCode } = response;
const httpError = error as HttpException;
const statusCode =
httpError.getStatus() ?? response.statusCode ?? undefined;
this.logger.error(
`${method} ${url} ${statusCode} - ${userAgent} ${ip}: ${
context.getClass().name
} ${context.getHandler().name} ${Date.now() - now}ms`,
);
return throwError(() => error);
}),
);
}
}
作用範圍一共有三種,可以需依據去自行套用
只想針對某個 method 做攔截,直接套用在某個 Controller 的 route handler 上,如下:
@Post()
@UseInterceptors(LoggingInterceptor)
createPet(@Body() createPetDto: CreatePetDto) {
return this.petsService.createPet(createPetDto);
}
針對某個 Prefix 的 route 做攔截,直接套用在某個 Controller 的 class 上,如下:
@UseInterceptors(LoggingInterceptor)
export class PetsController {}
要針對所有 route 做攔截,有兩種作法:
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new LoggingInterceptor());
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
@Module({
providers: [
{ provide: APP_INTERCEPTOR, useClass: LoggingInterceptor }
],
})
export class AppModule {}
nestjs Interceptor 提供了可以優雅地擴充原本 route 處理邏輯的方法。一般來說會把非功能性邏輯,比如例外處理,設置 timeout 等等邏輯放在上面。要注意的事,不要把一些職責的任務切分過細,有時反而會讓邏輯細碎。