在開發中型以上的Angular專案時,建立完整的HTTP服務管理體系是必要的。本文將從RestAPI設計規範開始,深入探討Token安全管理、錯誤處理機制,最後比較Interceptor與Http-base.service兩種架構選擇,幫助開發者建構穩定可靠的HTTP服務層。
在實際專案開發中,HTTP服務層的設計並非獨立的技術選擇,而是一個需要整體考量的系統:
相互依存的關係:API規範影響錯誤處理的設計,Token管理機制決定了服務架構的選擇,錯誤處理策略又會影響使用者體驗的設計。
統一的設計決策:當我們選擇Interceptor時,Token的處理方式、錯誤的攔截機制、API回應格式的解析都需要在同一個層級考慮。選擇Http-base.service時,這些邏輯則需要封裝在服務層中。
實務經驗分享:在專案中,我通常在同一個HTTP服務中處理這些問題,因此一起討論更貼近實際開發情境,也能提供更具參考價值的整合方案。
REST(REpresentational State Transfer)是以資源為導向的架構風格。讓我們了解幾個常見HTTP動詞的實際應用:
GET:用於資料查詢,參數附在URL上,適合取得列表或單筆資料。由於參數公開且有長度限制,不適合敏感資訊。
POST:主要用於新增資料,資料放在Body中傳送。實務上,當查詢參數過多或涉及隱私時,也會改用POST。
PUT:理論上用於整筆資料置換,但實際開發中較少使用,多以POST和PATCH取代。
PATCH:用於部分資料修改,如更新使用者地址或email。
DELETE:刪除資料時使用,參數同樣加在URL上。
OPTIONS:瀏覽器在跨域請求時自動發送的預檢請求,用於確認安全性。
當你發送跨域API請求時:
fetch('https://api.example.com/users', {
method: 'POST',
// ...
});
瀏覽器會自動執行以下步驟:
1. 使用複數名詞表示資源
*# 正確*
GET /api/users *# 取得所有使用者*
GET /api/users/123 *# 取得特定使用者*
POST /api/orders *# 新增訂單#*
2. 資源層級關係
*# 巢狀資源*
GET /api/users/123/orders *# 取得使用者的訂單*
POST /api/users/123/orders *# 為使用者新增訂單*
GET /api/orders/456/items *# 取得訂單的商品項目*
3. 查詢參數設計
*# 分頁*
GET /api/users?page=1&limit=20
*# 排序*
GET /api/users?sort=created_at&order=desc
*# 篩選*
GET /api/users?status=active&role=admin&search=john
*# 複合查詢*
GET /api/orders?date_from=2024-01-01&date_to=2024-01-31&status=completed
成功回應
200 OK
:GET、PUT、PATCH成功201 Created
:POST新增成功204 No Content
:DELETE成功或PUT更新無回傳內容客戶端錯誤
400 Bad Request
:請求格式錯誤401 Unauthorized
:未授權,需要登入403 Forbidden
:已登入但無權限404 Not Found
:資源不存在422 Unprocessable Entity
:資料驗證失敗伺服器錯誤
500 Internal Server Error
:伺服器內部錯誤503 Service Unavailable
:服務暫時無法使用統一回應格式
typescript
interface ApiResponse<T> {
success: boolean;
data: T;
message: string;
timestamp: string;
pagination?: {
page: number;
limit: number;
total: number;
totalPages: number;
};
}
*// 成功範例*
{
"success": true,
"data": {
"id": 123,
"name": "John Doe",
"email": "john@example.com"
},
"message": "取得使用者資料成功",
"timestamp": "2024-08-21T10:30:00Z"
}
*// 列表範例*
{
"success": true,
"data": [...],
"message": "取得使用者列表成功",
"timestamp": "2024-08-21T10:30:00Z",
"pagination": {
"page": 1,
"limit": 20,
"total": 150,
"totalPages": 8
}
}
1. localStorage
// 儲存與取得
localStorage.setItem('token', 'eyJhbGciOiJIUzI1...');
const token = localStorage.getItem('token');
優點:持久儲存、容量大(5-10MB)、同源分頁共享
缺點:容易受XSS攻擊、無法設定過期時間、SSR首次渲染取不到
2. sessionStorage
sessionStorage.setItem('token', 'eyJhbGciOiJIUzI1...');
優點:分頁關閉自動清除、不同分頁獨立
缺點:同樣有XSS風險、使用者體驗較差
3. httpOnly Cookie(推薦)
// 只能由後端設定
res.cookie('token', 'eyJhbGciOiJIUzI1...', {
httpOnly: true, // JavaScript無法讀取
secure: true, // 只在HTTPS傳送
sameSite: 'strict' // CSRF防護
});
優點:最安全、自動帶入請求、可設定過期
缺點:需要後端配合、需額外CSRF防護
通常有兩種方式處理
一個是在底層的服務設定預設的Header,如果後端通知過期的時候,統一登出
一個是利用攔截器,例如 authInterceptor ,直接在每次收發http請求的時候做處理,後端通知過期的時候也可以統一登出
比較項目 | Interceptor | Http-base.service |
---|---|---|
影響範圍 | 所有HTTP請求(包含第三方API) | 只影響透過service的請求 |
控制精度 | 較粗粒度,難以排除特定請求 | 細粒度控制,可選擇性使用 |
使用方式 | 開發者無感知,自動生效 | 需主動使用,有學習成本 |
程式碼侵入性 | 低,不需修改現有API呼叫 | 高,需將所有API改用service |
錯誤處理 | 統一在interceptor層處理 | 可在service層客製化處理 |
第三方API | 自動加token(可能不需要,或是有規格差異) | 不會影響直接的HTTP呼叫 |
設定複雜度 | 簡單,一次設定全域生效 | 中等,需封裝各種HTTP方法 |
團隊採用 | 容易推廣,自動生效 | 需團隊約定統一使用 |
彈性度 | 較低,全域統一邏輯 | 較高,可針對不同需求客製 |
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const authService = inject(AuthService);
const isAuthRequest = req.url.includes('/auth/login');
const token = authService.getToken();
if (!isAuthRequest && token) {
req = req.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
}
return next(req).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401) {
authService.logout();
}
return throwError(() => error);
})
);
};
export class HttpBaseService {
/**
* 執行HTTP GET請求
*/
public get<T>(endpoint: string, options: RequestOptions = {}): Observable<T> {
return this.request<T>(HttpMethod.GET, endpoint, options);
}
/**
* 通用請求方法
*/
private request<T>(method: HttpMethod, endpoint: string, options: RequestOptions = {}): Observable<T> {
const url = this.buildUrl(endpoint);
const requestOptions = this.buildRequestOptions(options);
this.requestStarted();
let request: Observable<any>;
switch (method) {
case HttpMethod.GET:
request = this.http.get<ApiResponse<T>>(url, requestOptions);
break;
// 其他HTTP方法...
default:
throw new Error(`不支持的HTTP方法: ${method}`);
}
return request.pipe(
timeout(options.timeoutMs || this.defaultTimeout),
retry({ count: options.retryCount ?? this.defaultRetryCount }),
this.extractData<T>(),
catchError(err => this.handleError<T>(err, options.skipErrorHandler)),
finalize(() => this.requestFinished())
);
}
/**
* 構建HTTP請求選項
*/
private buildRequestOptions(options: RequestOptions): any {
const responseType = options.responseType || 'json';
const defaultOptions: any = {
headers: this.getDefaultHeaders(options.ignoreAuthHeader, options.removeContentType),
responseType: responseType,
withCredentials: options.withCredentials ?? false,
reportProgress: options.reportProgress ?? false
};
// 處理自訂headers和params
if (options.headers) {
// headers處理邏輯
}
if (options.params) {
defaultOptions.params = options.params;
}
return defaultOptions;
}
/**
* 預設HTTP Headers
*/
private getDefaultHeaders(ignoreAuthHeader = false, removeContentType = false): HttpHeaders {
let headers = new HttpHeaders({
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
'Cache-Control': 'no-cache'
});
if (removeContentType) {
headers = headers.delete('Content-Type');
}
if (!ignoreAuthHeader) {
const token = localStorage.getItem('auth_token');
if (token) {
headers = headers.set('Authorization', `Bearer ${token}`);
}
}
return headers;
}
}
// Service中使用Http-base.service
constructor(private httpBaseService: HttpBaseService) {}
// 取得使用者資料
getUserData(id: string): Observable<User> {
return this.httpBaseService.get(`/user/${id}`);
}
// 上傳檔案(不需要Content-Type)
uploadFile(file: FormData): Observable<any> {
return this.httpBaseService.post('/upload', {
body: file,
removeContentType: true
});
}
建構完整的Angular HTTP服務架構需要考慮多個層面:
API設計層面:遵循RestAPI規範,使用標準的HTTP狀態碼和統一的回應格式,讓API更易於維護和使用。
安全層面:優先選擇httpOnly Cookie儲存Token,建立完善的授權機制,確保應用程式的安全性。
錯誤處理層面:設計統一的錯誤格式和使用者友善的錯誤訊息,提升使用者體驗和除錯效率。
架構選擇層面:根據專案複雜度選擇Interceptor或Http-base.service。Interceptor適合簡單統一的場景,Http-base.service則提供更高的彈性和控制力。
這些設計原則相互配合,就能構建出相對穩定、安全且易於維護的HTTP服務架構。