iT邦幫忙

2024 iThome 鐵人賽

0
JavaScript

Signal API in Angular系列 第 32

Day 32 - 在 Angular 中從後端檢索資料的不同方法

  • 分享至 

  • xImage
  •  

我想整合不同的方法來使用 ObservableSignalHttpClient 在 Angular 中檢索資料。根據我的觀察,我發現了六種資料檢索模式,每種模式都有其優點和缺點。在這篇文章分析了 Angular 中的資料檢索之後,我希望讀者能夠選擇自己喜歡的選擇並將其應用到他們的 Angular 專案中。

此示範將 signal 傳遞給 Pokemon API 以檢索 Pikachu 並使用不同的模式來顯示結果。資料檢索模式是:

  • Observable + HttpClient + AsyncPipe
  • Effect + HttpClient
  • HttpClient + toObservable + toSignal + SwitchMap
  • derivedAsync
  • derivedAsync + requiredSync 發出同步數據
  • derivedFrom 處理多個 signals 和Oservables

安裝 ngxtension 以將derivedAsync匯入到示範中

npm i -save-exact ngxtension
import { ApplicationConfig, provideExperimentalZonelessChangeDetection } from '@angular/core';
import { provideHttpClient } from "@angular/common/http";

export const appConfig: ApplicationConfig = {
 providers: [
   provideHttpClient(),
   provideExperimentalZonelessChangeDetection()
 ]
}

為示範提供 HttpClient 和 experimental zoneless。

import { appConfig } from './app.config';

bootstrapApplication(App, appConfig);

App 元件和 appConfig 引導到應用程式。

檢索 Pokemon 的實用函數

// pokemon.interface.ts

export interface Pokemon {
 id: number;
 name: string;
 sprites: {
   front_shiny: string
 };
}

export interface DisplayPokemon {
 id: number;
 name: string;
 img: string;
}
```typscript

```typescript
import { HttpClient } from "@angular/common/http";
import { inject } from "@angular/core";
import { map, Observable } from "rxjs";
import { DisplayPokemon, Pokemon } from "./pokemon.interface";

export const getPokemonFn = (): (id: number) => Observable<DisplayPokemon> => {
 const httpClient = inject(HttpClient);

 return (id: number) => {
   return httpClient.get<Pokemon>(`https://pokeapi.co/api/v2/pokemon/${id}`)
     .pipe(
       map((p) => ({
         id: p.id,
         name: p.name,
         img: p.sprites.front_shiny
       }))
     );
 }
}

getPokemonFn 函數是一個高階函數,它會傳回一個檢索 Pokemon 的函數。匿名函數接受 Pokemon Id 並呼叫伺服器來檢索 DisplayPokemon Observable。

建立 Pokemon 元件

import { TitleCasePipe } from "@angular/common";
import { ChangeDetectionStrategy, Component, input } from "@angular/core";
import { DisplayPokemon } from "../pokemon.interface";

@Component({
 selector: 'app-pokemon',
 standalone: true,
 imports: [TitleCasePipe],
 template: `
   <div class="outer">
     <div>
       <img [attr.alt]="pokemon().name" [src]="pokemon().img" />
     </div>
     <p>
       <span style="font-weight: bold;">Id:</span> <span>{{ pokemon().id }}</span>
       <span style="font-weight: bold;">Name:</span>
       <span>{{ pokemon().name | titlecase }}</span>
     </p>
   </div>
 `,
})
export class PokemonComponent {
 pokemon = input.required<DisplayPokemon>();
}

PokemonComponent 元件包含所需的訊號輸入 (signal input) pokemon。它呈現皮卡丘的圖像、id 和名稱。

import { ChangeDetectionStrategy, Component, HostAttributeToken, inject, input } from "@angular/core";
import { DisplayPokemon } from "../pokemon.interface";
import { PokemonComponent } from "../app-pokemon/app-pokemon.compont";

@Component({
 selector: 'app-pokemon-container',
 standalone: true,
 imports: [PokemonComponent],
 template: `
   <h3 style="padding: 0.5rem;">{{ pattern }}</h3>
   @if (pokemon(); as pokemon) {
     <app-pokemon [pokemon]="pokemon" />
   }
 `,
})
export class PokemonContainerComponent {
 pokemon = input.required<DisplayPokemon | null | undefined>();

 pattern = inject(new HostAttributeToken('pattern'), { optional: true }) || 'Signal Default Value';
}

PokemonContainerComponent 元件顯示模式名稱並呈現 PokemonComponent 元件。

現在,我可以一一描述每種模式。

Observable + AsyncPipe + HttpClient

<app-pokemon-container [pokemon]="pokemon$ | async" pattern="Observable + AsyncPipe + HttpClient" />
export class App {
 id = signal(25);
 getPokemon = getPokemonFn();

 // old way. RxJS way before there is Signal
 pokemon$ = this.getPokemon(this.id());
}

id signal 的值為 25。範本使用 AsyncPipie 解析 pokemon$ 並將結果指派給 AppPokemonContainerComponent 元件的 pokemon input。

我的拙見:開發人員可以使用 RxJSHttpClient 來檢索 Observable 並在範本中解析它以顯示結果。 如果我們想避免 AsyncPipeObservable,我們可以嘗試其他模式。

Effect 和 HttpClient

<app-pokemon-container [pokemon]="pokemon2()" pattern="Effect + HttpClient" />
pokemon2 = signal<DisplayPokemon | undefined>(undefined);

constructor() {
   effect((OnCleanUp) => {
     const subscription = this.getPokemon(this.id())
       .subscribe((p) => this.pokemon2.set(p))
  
     OnCleanUp(() => subscription.unsubscribe());
   });
 }

我聲明了一個 pokemon2 訊號 (signal) 並提供了一個 undefined 的初始值。 此 effect 追蹤 id 訊號 (signal) 並在訊號值 (signal value) 變化時執行邏輯。 effect 邏輯呼叫 API 來擷取 pokemon、訂閱 Observable 並覆寫 pokemon2 訊號 (signal) 的值。在 OnCleanUp 回呼函數中,在銷毀 effect 之前取消訂閱 (subscription)。

我的拙見:Angular 團隊領導和幾位專家建議盡量減少該 effect 的使用。此 effect 在每個查詢中建立一個新的 Observablesubscription。此外,開發人員必須使用 OnCleanUp 回呼函數來清理 subscription 以避免 memory leaks。開發人員很容易忘記取消 subscription。 該 effect 適用於高級工程師,他們可以做出正確的決定來使用 effect 或替代方案。

HttpClient + toObservable + toSignal + SwitchMap

<app-pokemon-container [pokemon]="pokemon3()" pattern="HttpClient + toObservable + toSignal + SwitchMap" />
createToObservable(pokemonId: Signal<number>) {
   return toObservable(pokemonId).pipe(
     tap((id) => console.log(id)),
     switchMap((id) => this.getPokemon(id))
   );
}

pokemon3$ = this.createToObservable(this.id);
pokemon3 = toSignal<DisplayPokemon>(this.pokemon3$, { initialValue: undefined });

toObservable(this.id) 建立一個 Observable 並將 id 傳送給 switchMap 運算子以取得 pokemon。然後,toSignalObservable 建立一個 signal 並將結果分配給 pokemon3。然後,範本 pokemon3 的 signal函數來顯示資料。

我的拙見:這種模式很好,因為 HttpClient 總是完成並取消訂閱 (subscription)。此外,switchMap 在發出新請求之前會取消先前的請求。然而,toSignaltoObservable 在元件上加入了樣板程式碼,當這種模式重複出現時,元件將變得難以維護。當 root service 建立一個不取消訂閱 (subscription) 的 Observable 時,toSignal 可能會導致 memory leaks。工程師應該在元件中使用 toSignal,這樣當元件被銷毀時,Observable 也會被銷毀。

derivedAsync

<app-pokemon-container [pokemon]="pokemon4()" pattern="derivedAsync" />
pokemon4 = derivedAsync(() => this.getPokemon(this.id()));

使用 ngxtensionderivedAsync 函數來檢索 Pokemon。derivedAsync 的回傳類型是 Signal<T> | undefined

我的拙見:實用函數支援 PromiseObservable,並回傳 Signal<T> | undefined。函數使用Subject 發出一個值,並在 DestroyRef 的回呼中執行清理。預設行為是 switchAll,它會取消先前的請求。它具有早期模式的所有優點,而沒有缺點。 Angular 開發人員不必擔心 AsyncPipe、需要清理的訂閱 (subscription) 以及 toSignal(toObservable(signal)) 樣板程式碼。

<app-pokemon-container [pokemon]="pokemon5()" pattern="derivedAsync + requireSync" />
pokemon5$ = this.getPokemon(this.id()).pipe(startWith(DEFAULT_POKEMON));
 pokemon5 = derivedAsync(() => this.pokemon5$,
   {
     requireSync: true,
   });

此模式也使用 ngxtensionderivedAsync 函數來檢索 Pokemon。 requiredSync 為 true 確保 Observable 在訂閱時發出一個值;因此,傳回類型為 Signal<T>

我的拙見:透過在 startWith 運算子中指定初始值,derivedAsync 函數的輸出是 Signal<T>。因此,在 HTML 範本中顯示資料之前,我不需要使用 @if 來測試 undefined

derivedFrom - 執行多個 signals 和 Observables

@for (p of pokemons(); track p.id) {
   <app-pokemon-container [pokemon]="p" pattern="derivedFrom" />
}
pokemons = derivedFrom([
   this.createToObservable(this.id),
   this.createToObservable(this.nextPokemon),
 ], { initialValue: [] as DisplayPokemon[] });

當我必須在示範中檢索多個 Pokemon 時,我會使用 derivedFrom 來獲取結果。如果 Observable 沒有立即發出值(例如:使用 startWith RxJS 運算子),則函數會拋出錯誤。修復方法是在函數的第二個參數中包含 initialValue 選項。 derivedFrom 的結果是 DisplayedPokemon 陣列的 Signal。 範本中的 @for 將每個 pokemon 分配給 AppPokemonContainerComponent 元件的 signal input。

這些是我觀察到的所有可以使用 HttpClient 檢索資料並將結果儲存在 signal 中的模式。 我的首選是derivedAsync,因為

  • 沒有 toSignal 和 toObservable 樣板程式碼
  • 沒有使用 AsynPipe 來解析 HTML 範本中的 Observable
  • 訊號效果和額外聲明中沒有非同步計算
  • 可以透過 requireSync 選項來發出同步數據

鐵人賽的第 32 天到此結束。

參考:


上一篇
Day 31 - 請不要使用 effect
下一篇
Day 33 - 將 manual injector 傳遞給 toSignal 函數
系列文
Signal API in Angular36
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言