攔截器是使用 @Injectable() 裝飾器注解的類。攔截器應該實現(xiàn) NestInterceptor 接口。
攔截器具有一系列有用的功能,這些功能受面向切面編程(AOP)技術的啟發(fā)。它們可以:
每個攔截器都有 intercept() 方法,它接收2個參數(shù)。 第一個是 ExecutionContext 實例(與守衛(wèi)完全相同的對象)。 ExecutionContext 繼承自 ArgumentsHost 。 ArgumentsHost 是傳遞給原始處理程序的參數(shù)的一個包裝 ,它根據(jù)應用程序的類型包含不同的參數(shù)數(shù)組。你可以在這里讀更多關于它的內(nèi)容(在異常過濾器章節(jié)中)。
通過擴展 ArgumentsHost,ExecutionContext 還添加了幾個新的幫助程序方法,這些方法提供有關當前執(zhí)行過程的更多詳細信息。這些詳細信息有助于構(gòu)建可以在廣泛的控制器,方法和執(zhí)行上下文中使用的更通用的攔截器。ExecutionContext 在此處了解更多信息。
第二個參數(shù)是 CallHandler。如果不手動調(diào)用 handle() 方法,則主處理程序根本不會進行求值。這是什么意思?基本上,CallHandler是一個包裝執(zhí)行流的對象,因此推遲了最終的處理程序執(zhí)行。
比方說,有人提出了 POST /cats 請求。此請求指向在 CatsController 中定義的 create() 處理程序。如果在此過程中未調(diào)用攔截器的 handle() 方法,則 create() 方法不會被計算。只有 handle() 被調(diào)用(并且已返回值),最終方法才會被觸發(fā)。為什么?因為Nest訂閱了返回的流,并使用此流生成的值來為最終用戶創(chuàng)建單個響應或多個響應。而且,handle() 返回一個 Observable,這意味著它為我們提供了一組非常強大的運算符,可以幫助我們進行例如響應操作。
第一個用例是使用攔截器在函數(shù)執(zhí)行之前或之后添加額外的邏輯。當我們要記錄與應用程序的交互時,它很有用,例如 存儲用戶調(diào)用,異步調(diào)度事件或計算時間戳。作為一個例子,我們來創(chuàng)建一個簡單的例子 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> 是一個通用接口,其中 T 表示已處理的 Observable<T> 的類型(在流后面),而 R 表示包含在返回的 Observable<R> 中的值的返回類型。
攔截器的作用與控制器,提供程序,守衛(wèi)等相同,這意味著它們可以通過構(gòu)造函數(shù)注入依賴項。
由于 handle() 返回一個RxJS Observable,我們有很多種操作符可以用來操作流。在上面的例子中,我們使用了 tap() 運算符,該運算符在可觀察序列的正?;虍惓=K止時調(diào)用函數(shù)。
為了設置攔截器, 我們使用從 @nestjs/common 包導入的 @UseInterceptors() 裝飾器。與守衛(wèi)一樣, 攔截器可以是控制器范圍內(nèi)的, 方法范圍內(nèi)的或者全局范圍內(nèi)的。
cats.controller.ts
@UseInterceptors(LoggingInterceptor)
export class CatsController {}
@UseInterceptors() 裝飾器從 @nestjs/common 導入。
由此,CatsController 中定義的每個路由處理程序都將使用 LoggingInterceptor。當有人調(diào)用 GET /cats 端點時,您將在控制臺窗口中看到以下輸出:
Before...
After... 1ms
請注意,我們傳遞的是 LoggingInterceptor 類型而不是實例,讓框架承擔實例化責任并啟用依賴注入。另一種可用的方法是傳遞立即創(chuàng)建的實例:
cats.controller.ts
@UseInterceptors(new LoggingInterceptor())
export class CatsController {}
如上所述, 上面的構(gòu)造將攔截器附加到此控制器聲明的每個處理程序。如果我們決定只限制其中一個, 我們只需在方法級別設置攔截器。為了綁定全局攔截器, 我們使用 Nest 應用程序?qū)嵗?nbsp;useGlobalInterceptors() 方法:
const app = await NestFactory.create(ApplicationModule);
app.useGlobalInterceptors(new LoggingInterceptor());
全局攔截器用于整個應用程序、每個控制器和每個路由處理程序。在依賴注入方面, 從任何模塊外部注冊的全局攔截器 (如上面的示例中所示) 無法插入依賴項, 因為它們不屬于任何模塊。為了解決此問題, 您可以使用以下構(gòu)造直接從任何模塊設置一個攔截器:
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 并不是處理自定義提供商注冊的唯一方法。在這里了解更多。
我們已經(jīng)知道, handle() 返回一個 Observable。此流包含從路由處理程序返回的值, 因此我們可以使用 map() 運算符輕松地對其進行改變。
響應映射功能不適用于特定于庫的響應策略(禁止直接使用 @Res() 對象)。
讓我們創(chuàng)建一個 TransformInterceptor, 它將打包響應并將其分配給 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() 方法的魅力一樣, 意思是, 如果需要,您可以毫不費力地將方法切換為異步。
之后,當有人調(diào)用GET /cats端點時,請求將如下所示(我們假設路由處理程序返回一個空 arry []):
{
"data": []
}
攔截器在創(chuàng)建用于整個應用程序的可重用解決方案時具有巨大的潛力。例如,我們假設我們需要將每個發(fā)生的 null 值轉(zhuǎn)換為空字符串 ''。我們可以使用一行代碼并將攔截器綁定為全局代碼。由于這一點,它會被每個注冊的處理程序自動重用。
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 ));
}
}
另一個有趣的用例是利用 catchError() 操作符來覆蓋拋出的異常:
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())),
);
}
}
有時我們可能希望完全阻止調(diào)用處理程序并返回不同的值 (例如, 由于性能問題而從緩存中獲取), 這是有多種原因的。一個很好的例子是緩存攔截器,它將使用一些TTL存儲緩存的響應。不幸的是, 這個功能需要更多的代碼并且由于簡化, 我們將僅提供簡要解釋主要概念的基本示例。
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();
}
}
這是一個 CacheInterceptor,帶有硬編碼的 isCached 變量和硬編碼的響應 [] 。我們在這里通過 of 運算符創(chuàng)建并返回了一個新的流, 因此路由處理程序根本不會被調(diào)用。當有人調(diào)用使用 CacheInterceptor 的端點時, 響應 (一個硬編碼的空數(shù)組) 將立即返回。為了創(chuàng)建一個通用解決方案, 您可以利用 Reflector 并創(chuàng)建自定義修飾符。反射器 Reflector 在守衛(wèi)章節(jié)描述的很好。
使用 RxJS 運算符操作流的可能性為我們提供了許多功能。讓我們考慮另一個常見的用例。假設您要處理路由請求超時。如果您的端點在一段時間后未返回任何內(nèi)容,則您將以錯誤響應終止。以下構(gòu)造可實現(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秒后,請求處理將被取消。您還可以在拋出之前添加自定義邏輯RequestTimeoutException(例如,釋放資源)。
更多建議: