HttpClient 支援一種稱為「攔截器」的中介層形式。
TL;DR: 攔截器是一種中介層,讓重試、快取、記錄與驗證等常見模式能從個別請求中抽象出來,統一處理。
HttpClient 支援兩種攔截器:函式型與 DI 型。我們建議使用函式型攔截器,因為它們的行為較可預期,在複雜設定下尤其如此。本指南的範例使用函式型攔截器,DI 型攔截器則在文末以專節介紹。
攔截器
攔截器通常是可針對每個請求執行的函式,具備廣泛的能力,能影響請求與回應的內容及整體流程。你可以安裝多個攔截器,它們會組成一條攔截器鏈,其中每個攔截器在把請求或回應轉交給鏈中的下一個攔截器之前,先行處理。
你可以使用攔截器實作各種常見模式,例如:
- 為傳送至特定 API 的外送請求加入驗證標頭。
- 以指數退避方式重試失敗的請求。
- 將回應快取一段時間,或快取到被變更操作失效為止。
- 自訂回應的解析方式。
- 量測伺服器回應時間並記錄下來。
- 在網路操作進行期間驅動 UI 元素,例如載入旋轉圖示。
- 收集並批次處理在某段時間內發出的請求。
- 在可設定的期限或逾時之後自動讓請求失敗。
- 定期輪詢伺服器並重新整理結果。
定義攔截器
攔截器的基本形式是一個函式,它接收外送的 HttpRequest,以及一個 next 函式,代表攔截器鏈中的下一個處理步驟。
例如,這個 loggingInterceptor 會在轉交請求之前,把外送請求的 URL 記錄到 console.log:
export function loggingInterceptor(
req: HttpRequest<unknown>,
next: HttpHandlerFn,
): Observable<HttpEvent<unknown>> {
console.log(req.url);
return next(req);
}
要讓這個攔截器真正攔截請求,你必須設定 HttpClient 使用它。
設定攔截器
在透過依賴注入設定 HttpClient 時,使用 withInterceptors 功能來宣告要使用的攔截器集合:
bootstrapApplication(App, {
providers: [provideHttpClient(withInterceptors([loggingInterceptor, cachingInterceptor]))],
});
你設定的攔截器會依你在 providers 中列出的順序串連在一起。在上例中,loggingInterceptor 會先處理請求,然後轉交給 cachingInterceptor。
攔截回應事件
攔截器可以轉換 next 回傳的 HttpEvent 的 Observable 串流,以存取或操作回應。由於此串流包含所有回應事件,為了辨識最終的回應物件,可能需要檢查每個事件的 .type。
export function loggingInterceptor(
req: HttpRequest<unknown>,
next: HttpHandlerFn,
): Observable<HttpEvent<unknown>> {
return next(req).pipe(
tap((event) => {
if (event.type === HttpEventType.Response) {
console.log(req.url, 'returned a response with status', event.status);
}
}),
);
}
TIP: 攔截器天生就能把回應與其對應的外送請求關聯起來,因為它們是在擷取了請求物件的閉包中轉換回應串流。
修改請求
HttpRequest 與 HttpResponse 執行個體的大多數面向都是「不可變」的,攔截器無法直接修改它們。攔截器改以使用 .clone() 操作複製這些物件,並指定在新執行個體中要變更哪些屬性,來套用變更。這可能涉及對值本身進行不可變更新(例如 HttpHeaders 或 HttpParams)。
例如,為請求加入一個標頭:
const reqWithHeader = req.clone({
headers: req.headers.set('X-New-Header', 'new header value'),
});
這種不可變性讓大多數攔截器在同一個 HttpRequest 被多次提交到攔截器鏈時,仍能保持冪等。這種情況可能因多種原因發生,包括請求在失敗後被重試時。
CRITICAL: 請求或回應的主體「不」受深層變更的保護。如果攔截器必須變更主體,務必小心處理同一個請求被多次執行的情況。
攔截器中的依賴注入
攔截器在註冊它的注入器的「注入上下文」中執行,可以使用 Angular 的 inject API 來取得依賴。
例如,假設應用程式有一個名為 AuthService 的服務,負責為外送請求建立驗證權杖。攔截器可以注入並使用這個服務:
export function authInterceptor(req: HttpRequest<unknown>, next: HttpHandlerFn) {
// Inject the current `AuthService` and use it to get an authentication token:
const authToken = inject(AuthService).getAuthToken();
// Clone the request to add the authentication header.
const newReq = req.clone({
headers: req.headers.append('X-Authentication-Token', authToken),
});
return next(newReq);
}
請求與回應的中繼資料
在請求中加入不傳送給後端、而是專門給攔截器使用的資訊,往往很有用。HttpRequest 有一個 .context 物件,它以 HttpContext 執行個體的形式儲存這類中繼資料。這個物件的作用類似具型別的對照表,其鍵的型別為 HttpContextToken。
為了說明這套系統如何運作,我們用中繼資料來控制快取攔截器是否對某個請求啟用。
定義上下文權杖
若要把「快取攔截器是否應快取某個特定請求」存放在該請求的 .context 對照表中,請定義一個新的 HttpContextToken 作為鍵:
export const CACHING_ENABLED = new HttpContextToken<boolean>(() => true);
所提供的函式會為尚未明確設定值的請求建立權杖的預設值。使用函式可確保當權杖的值是物件或陣列時,每個請求都能取得自己的執行個體。
在攔截器中讀取權杖
接著,攔截器可以讀取該權杖,並依其值決定是否套用快取邏輯:
export function cachingInterceptor(req: HttpRequest<unknown>, next: HttpHandlerFn): Observable<HttpEvent<unknown>> {
if (req.context.get(CACHING_ENABLED)) {
// apply caching logic
return ...;
} else {
// caching has been disabled for this request
return next(req);
}
}
發出請求時設定上下文權杖
透過 HttpClient API 發出請求時,你可以為 HttpContextToken 提供值:
const data$ = http.get('/sensitive/data', {
context: new HttpContext().set(CACHING_ENABLED, false),
});
攔截器可以從請求的 HttpContext 讀取這些值。
請求上下文是可變的
與 HttpRequest 的其他屬性不同,相關聯的 HttpContext 是「可變」的。如果攔截器變更了一個之後會被重試的請求的上下文,同一個攔截器在再次執行時會觀察到該上下文變更。在需要跨多次重試傳遞狀態時,這很有用。
合成回應
大多數攔截器只會呼叫 next 處理器,同時轉換請求或回應,但這並非嚴格必要。本節討論攔截器納入更進階行為的幾種方式。
攔截器不一定要呼叫 next。它們可以改為透過其他機制建構回應,例如從快取取得,或透過替代機制傳送請求。
可以使用 HttpResponse 建構函式來建構回應:
const resp = new HttpResponse({
body: 'response body',
});
處理重新導向資訊
當 HttpClient 使用 fetch 後端時,回應會包含一個 redirected 屬性,指出該回應是否為重新導向的結果。此屬性與原生 Fetch API 規格一致,在攔截器處理重新導向情境時很有用。
攔截器可以存取重新導向資訊並據此採取行動:
export function redirectTrackingInterceptor(
req: HttpRequest<unknown>,
next: HttpHandlerFn,
): Observable<HttpEvent<unknown>> {
return next(req).pipe(
tap((event) => {
if (event.type === HttpEventType.Response && event.redirected) {
console.log('Request to', req.url, 'was redirected to', event.url);
// Handle redirect logic - maybe update analytics, security checks, etc.
}
}),
);
}
你也可以使用重新導向資訊,在攔截器中實作條件邏輯:
export function authRedirectInterceptor(
req: HttpRequest<unknown>,
next: HttpHandlerFn,
): Observable<HttpEvent<unknown>> {
return next(req).pipe(
tap((event) => {
if (event.type === HttpEventType.Response && event.redirected) {
// Check if we were redirected to a login page
if (event.url?.includes('/login')) {
// Handle authentication redirect
handleAuthRedirect();
}
}
}),
);
}
處理回應型別
當 HttpClient 使用 fetch 後端時,回應會包含一個 responseType 屬性,指出瀏覽器如何依 CORS 政策與請求模式處理該回應。此屬性與原生 Fetch API 規格一致,為除錯 CORS 問題與了解回應可存取性提供寶貴的線索。
回應的 responseType 屬性可以是下列值:
'basic':同來源回應,所有標頭皆可存取'cors':跨來源回應,CORS 標頭已正確設定'opaque':沒有 CORS 的跨來源回應,標頭與主體可能受限'opaqueredirect':在 no-cors 模式下,來自重新導向請求的回應'error':發生網路錯誤
攔截器可以利用回應型別資訊進行 CORS 除錯與錯誤處理:
export function responseTypeInterceptor(
req: HttpRequest<unknown>,
next: HttpHandlerFn,
): Observable<HttpEvent<unknown>> {
return next(req).pipe(
tap((event) => {
if (event.type === HttpEventType.Response) {
// Handle different response types appropriately
switch (event.responseType) {
case 'opaque':
// Limited access to response data
console.warn('Limited response data due to CORS policy');
break;
case 'cors':
case 'basic':
// Full access to response data
break;
case 'error':
// Handle network errors
console.error('Network error in response');
break;
}
}
}),
);
}
DI 型攔截器
HttpClient 也支援以可注入類別定義、並透過 DI 系統設定的攔截器。DI 型攔截器的能力與函式型攔截器完全相同,但設定機制不同。
DI 型攔截器是一個實作 HttpInterceptor 介面的可注入類別:
@Injectable()
export class LoggingInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, handler: HttpHandler): Observable<HttpEvent<any>> {
console.log('Request URL: ' + req.url);
return handler.handle(req);
}
}
DI 型攔截器透過依賴注入的多重 provider 來設定:
bootstrapApplication(App, {
providers: [
provideHttpClient(
// DI-based interceptors must be explicitly enabled.
withInterceptorsFromDi(),
),
{provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true},
],
});
DI 型攔截器依其 provider 註冊的順序執行。在 DI 設定龐大且具階層的應用程式中,這個順序可能非常難以預測。