HttpClient 進階
基本使用方式
通常將 HTTP 請求放在 ngOnInit
生命週期鉤子中執行。在 Angular 16+ 中,可以使用 inject
函數注入 HttpClient,並使用 DestroyRef 來管理訂閱生命週期。
private httpClient = inject(HttpClient);
private destroyRef = inject(DestroyRef);
ngOnInit(): void {
const getPlaces = this.httpClient
.get<{ places: Place[] }>('http://localhost:3000/places')
.subscribe({
next: (resData) => console.log(resData),
});
// 組件銷毀時取消訂閱,避免記憶體洩漏
this.destroyRef.onDestroy(() => {
getPlaces.unsubscribe();
});
}

注意:
DestroyRef
是 Angular 16+ 引入的功能,用於在組件銷毀時執行清理工作,替代了傳統的 ngOnDestroy 方法中手動管理訂閱。
HTTP 回應類型的定制
獲取完整 HTTP 回應
通過 observe: 'response'
選項,可以獲取完整的 HTTP 回應,包括狀態碼、標頭等信息:
ngOnInit(): void {
const getPlaces = this.httpClient
.get<{ places: Place[] }>('http://localhost:3000/places', {
observe: 'response',
})
.subscribe({
next: (response) => {
console.log(response); // 完整的 HTTP 回應
console.log(response.body?.places); // 只獲取回應內容中的 places 數據
},
});
this.destroyRef.onDestroy(() => {
getPlaces.unsubscribe();
});
}

觀察 HTTP 事件
通過 observe: 'events'
選項,可以觀察 HTTP 請求的整個生命週期:
import { HttpEventType } from '@angular/common/http';
ngOnInit(): void {
const getPlaces = this.httpClient
.get<{ places: Place[] }>('http://localhost:3000/places', {
observe: 'events',
})
.subscribe({
next: (event) => {
console.log(event);
// 可以根據事件類型進行不同處理
if (event.type === HttpEventType.Sent) {
console.log('請求已發送');
}
if (event.type === HttpEventType.Response) {
console.log('請求完成,收到回應');
console.log(event.body);
}
},
});
this.destroyRef.onDestroy(() => {
getPlaces.unsubscribe();
});
}

說明:HttpEventType 包含多種事件類型,如 Sent(請求發送)、ResponseHeader(收到回應標頭)、DownloadProgress(下載進度)、Response(完整回應)等,適合用於監控請求進度或處理文件上傳/下載。
使用 Pipe 處理回應數據
在 subscribe 之前,可以使用 RxJS 的運算符處理回應數據:
places = signal<Place[] | undefined>(undefined);
ngOnInit(): void {
const getPlaces = this.httpClient
.get<{ places: Place[] }>('http://localhost:3000/places')
.pipe(map((resData) => resData.places)) // 轉換回應格式
.subscribe({
next: (places) => {
this.places.set(places); // 直接存儲處理後的數據
},
});
this.destroyRef.onDestroy(() => {
getPlaces.unsubscribe();
});
}
處理載入狀態和錯誤
使用 Subscribe 選項
可以使用 signal 管理載入狀態,並在 subscribe 中處理不同的情況:
isFetching = signal<boolean>(false);
errorMessage = signal<string | null>(null);
places = signal<Place[] | undefined>(undefined);
ngOnInit(): void {
this.isFetching.set(true);
const getPlaces = this.httpClient
.get<{ places: Place[] }>('http://localhost:3000/places')
.pipe(map((resData) => resData.places))
.subscribe({
next: (places) => {
this.places.set(places);
},
error: (error) => {
console.log(error);
this.errorMessage.set(
'Something went wrong fetching the available places. Please try again later.'
);
},
complete: () => this.isFetching.set(false), // 無論成功或失敗,請求完成時都會執行
});
this.destroyRef.onDestroy(() => {
getPlaces.unsubscribe();
});
}
使用 RxJS 錯誤處理運算符
更優雅的方式是使用 RxJS 的 catchError 運算符處理錯誤:
import { catchError, map, throwError } from 'rxjs';
ngOnInit(): void {
this.isFetching.set(true);
const getPlaces = this.httpClient
.get<{ places: Place[] }>('http://localhost:3000/places')
.pipe(
map((resData) => resData.places),
catchError((error) => {
console.log(error);
// 將錯誤轉換為自定義錯誤消息
return throwError(
() =>
new Error(
'Something went wrong fetching the available places. Please try again later.'
)
);
})
)
.subscribe({
next: (places) => {
this.places.set(places);
},
error: (error: Error) => {
console.log(error);
this.errorMessage.set(error.message);
},
complete: () => this.isFetching.set(false),
});
this.destroyRef.onDestroy(() => {
getPlaces.unsubscribe();
});
}
優點:使用 catchError 運算符可以統一處理錯誤邏輯,讓程式碼更乾淨,並且能夠自定義錯誤消息,提供更好的用戶體驗。
封裝 HTTP 邏輯到服務
最佳實踐是將所有與 HTTP 相關的邏輯封裝到專用的服務中:
import { Injectable, signal, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { catchError, map, throwError } from 'rxjs';
import { Place } from './place.model';
@Injectable({
providedIn: 'root',
})
export class PlacesService {
private userPlaces = signal<Place[]>([]);
private httpClient = inject(HttpClient);
loadedUserPlaces = this.userPlaces.asReadonly(); // 提供唯讀版本給外部使用
// 可用地點
loadAvailablePlaces() {
return this.fetchPlaces(
'http://localhost:3000/places',
'Something went wrong fetching the available places. Please try again later.'
);
}
// 用戶地點
loadUserPlaces() {
return this.fetchPlaces(
'http://localhost:3000/user-places',
'Something went wrong fetching your favorite places. Please try again later.'
);
}
// 添加地點到用戶收藏
addPlaceToUserPlaces(placeId: string) {
return this.httpClient.put('http://localhost:3000/user-places', {
placeId,
});
}
// 從用戶收藏中移除地點
removeUserPlace(placeId: string) {
return this.httpClient.delete(`http://localhost:3000/user-places/${placeId}`);
}
private fetchPlaces(url: string, errorMessage: string) {
return this.httpClient.get<{ places: Place[] }>(url).pipe(
map((resData) => resData.places),
catchError((error) => {
console.log(error);
return throwError(() => new Error(errorMessage));
})
);
}
}
在組件中使用服務
import { Component, signal, inject, OnInit } from '@angular/core';
import { DestroyRef } from '@angular/core';
import { PlacesService } from './places.service';
import { Place } from './place.model';
@Component({
selector: 'app-places',
templateUrl: './places.component.html',
})
export class PlacesComponent implements OnInit {
places = signal<Place[] | undefined>(undefined);
isFetching = signal<boolean>(false);
errorMessage = signal<string | null>(null);
private placesService = inject(PlacesService);
private destroyRef = inject(DestroyRef);
ngOnInit(): void {
this.isFetching.set(true);
const subscription = this.placesService.loadAvailablePlaces().subscribe({
next: (places) => {
this.places.set(places);
this.isFetching.set(false);
},
error: (error: Error) => {
this.errorMessage.set(error.message);
this.isFetching.set(false);
}
});
this.destroyRef.onDestroy(() => {
subscription.unsubscribe();
});
}
addToFavorites(placeId: string) {
this.placesService.addPlaceToUserPlaces(placeId).subscribe({
next: () => {
// 處理成功添加
},
error: (error) => {
// 處理錯誤
}
});
}
}
重要提醒
始終記住在組件銷毀時取消訂閱,避免記憶體洩漏
使用 pipe 和 RxJS 運算符來處理和轉換數據流
將 HTTP 邏輯封裝到服務中,保持組件的簡潔
適當處理載入狀態和錯誤,提升用戶體驗
考慮使用 HTTP 攔截器來統一處理請求標頭、認證和錯誤處理
Last updated