HTTP Interceptor

什麼是 HTTP Interceptor?

HTTP Interceptor 是 Angular 提供的一種機制,可以攔截和修改 HTTP 請求和響應。它們像是中間件,允許我們在 HTTP 請求離開應用程式之前和響應到達應用程式之前執行程式碼。

常見用途

  • 添加認證令牌到每個請求

  • 處理和統一錯誤響應

  • 記錄 HTTP 請求和響應

  • 轉換請求和響應的格式

  • 添加加載指示器

  • 實現重試機制

基本實現

在 Angular 現代版本中,HTTP 攔截器通常是作為函數實現的,特別是在 Angular 14+ 的獨立元件中。

請求攔截器

下面是一個基本的請求攔截器,它會在每個請求中添加自定義標頭:

import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import {
  HttpHandlerFn,
  HttpRequest,
  provideHttpClient,
  withInterceptors,
} from '@angular/common/http';

function loggingInterceptor(request: HttpRequest<unknown>, next: HttpHandlerFn) {
  // 克隆請求並添加自定義標頭
  const modifiedRequest = request.clone({
    headers: request.headers.set('X-DEBUG', 'TESTING'),
  });
  
  console.log('[Outgoing Request]');
  console.log(request);
  
  // 將修改後的請求傳遞給下一個處理程序
  return next(modifiedRequest);
}

bootstrapApplication(AppComponent, {
  providers: [provideHttpClient(withInterceptors([loggingInterceptor]))],
}).catch((err) => console.error(err));

當你使用 HttpClient 發送請求時,這個攔截器會自動為每個請求添加 X-DEBUG: TESTING 標頭。

響應攔截器

攔截器也可以處理從服務器返回的響應:

import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import {
  HttpEventType,
  HttpHandlerFn,
  HttpRequest,
  provideHttpClient,
  withInterceptors,
} from '@angular/common/http';
import { tap } from 'rxjs';

function loggingInterceptor(request: HttpRequest<unknown>, next: HttpHandlerFn) {
  console.log('[Outgoing Request]');
  console.log(request);
  
  // 處理請求後的響應
  return next(request).pipe(
    tap({
      next: (event) => {
        if (event.type === HttpEventType.Response) {
          console.log('[Incoming Response]');
          console.log(event.status);
          console.log(event.body);
        }
      },
    })
  );
}

bootstrapApplication(AppComponent, {
  providers: [provideHttpClient(withInterceptors([loggingInterceptor]))],
}).catch((err) => console.error(err));

這個攔截器會記錄所有傳出請求和傳入響應的詳細信息。

同時處理請求和響應

一個完整的攔截器示例,同時處理請求和響應:

import { HttpEventType, HttpHandlerFn, HttpRequest } from '@angular/common/http';
import { tap } from 'rxjs';

export function authInterceptor(request: HttpRequest<unknown>, next: HttpHandlerFn) {
  // 從本地存儲獲取令牌
  const token = localStorage.getItem('auth_token');
  
  // 如果有令牌,添加到請求標頭
  let modifiedRequest = request;
  if (token) {
    modifiedRequest = request.clone({
      headers: request.headers.set('Authorization', `Bearer ${token}`)
    });
  }
  
  // 記錄請求
  console.log(`[Request] ${request.method} ${request.url}`);
  
  // 發送修改後的請求並處理響應
  return next(modifiedRequest).pipe(
    tap({
      next: (event) => {
        // 只處理完整的響應
        if (event.type === HttpEventType.Response) {
          console.log(`[Response] ${event.status} for ${request.url}`);
          
          // 處理令牌過期情況
          if (event.status === 401) {
            console.log('Token expired. Redirecting to login...');
            // 這裡可以添加重定向到登錄頁面的邏輯
          }
        }
      },
      error: (error) => {
        console.error(`[Error] ${request.url}:`, error);
      }
    })
  );
}

錯誤處理攔截器

專門用於統一處理錯誤的攔截器:

import { HttpHandlerFn, HttpRequest } from '@angular/common/http';
import { catchError, throwError } from 'rxjs';

export function errorInterceptor(request: HttpRequest<unknown>, next: HttpHandlerFn) {
  return next(request).pipe(
    catchError(error => {
      // 處理不同類型的錯誤
      if (error.status === 404) {
        console.error('Resource not found');
        // 可以顯示通知或導航到 404 頁面
      } else if (error.status === 500) {
        console.error('Server error');
        // 可以顯示服務器錯誤的通知
      } else if (!navigator.onLine) {
        console.error('No internet connection');
        // 可以顯示離線提示
      }
      
      // 將自定義錯誤消息添加到錯誤對象
      const customError = {
        message: error.error?.message || 'An unknown error occurred',
        status: error.status,
        url: request.url
      };
      
      // 將錯誤傳播給組件
      return throwError(() => customError);
    })
  );
}

載入指示器攔截器

用於顯示全局載入指示器的攔截器:

import { HttpEventType, HttpHandlerFn, HttpRequest } from '@angular/common/http';
import { finalize, tap } from 'rxjs';
import { inject } from '@angular/core';
import { LoadingService } from './loading.service';

export function loadingInterceptor(request: HttpRequest<unknown>, next: HttpHandlerFn) {
  const loadingService = inject(LoadingService);
  
  // 增加活躍請求計數
  loadingService.increaseRequestCount();
  
  return next(request).pipe(
    tap({
      next: (event) => {
        // 當收到完整響應時減少請求計數
        if (event.type === HttpEventType.Response) {
          loadingService.decreaseRequestCount();
        }
      }
    }),
    // 確保無論成功還是失敗都會減少請求計數
    finalize(() => {
      loadingService.decreaseRequestCount();
    })
  );
}

搭配的 LoadingService 可能如下所示:

import { Injectable, signal } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class LoadingService {
  private requestCount = signal(0);
  isLoading = this.requestCount.asReadonly();
  
  increaseRequestCount() {
    this.requestCount.update(count => count + 1);
  }
  
  decreaseRequestCount() {
    this.requestCount.update(count => Math.max(0, count - 1));
  }
}

註冊多個攔截器

攔截器按照註冊的順序執行,因此順序很重要:

bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient(
      withInterceptors([
        loadingInterceptor,  // 首先啟動載入指示器
        authInterceptor,     // 然後添加認證
        loggingInterceptor,  // 接著記錄
        errorInterceptor     // 最後處理錯誤
      ])
    )
  ],
}).catch((err) => console.error(err));

最佳實踐

  1. 保持攔截器輕量:攔截器會處理每個 HTTP 請求,所以應避免在其中執行複雜操作。

  2. 處理所有可能的情況:確保攔截器能夠處理所有可能的請求和響應場景。

  3. 正確處理流:使用 RxJS 運算符如 tapcatchErrorfinalize 來確保流正確完成。

  4. 克隆請求:始終使用 request.clone() 來修改請求,因為 HttpRequest 對象是不可變的。

  5. 考慮副作用:避免在攔截器中產生不必要的副作用,特別是那些可能影響應用狀態的操作。

Last updated