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) => {
        // 處理錯誤
      }
    });
  }
}

重要提醒

  1. 始終記住在組件銷毀時取消訂閱,避免記憶體洩漏

  2. 使用 pipe 和 RxJS 運算符來處理和轉換數據流

  3. 將 HTTP 邏輯封裝到服務中,保持組件的簡潔

  4. 適當處理載入狀態和錯誤,提升用戶體驗

  5. 考慮使用 HTTP 攔截器來統一處理請求標頭、認證和錯誤處理

Last updated