iT邦幫忙

2024 iThome 鐵人賽

0
JavaScript

Signal API in Angular系列 第 39

Day 39 - 使用 Angular 19 中的 Resource API 進行資料檢索

  • 分享至 

  • xImage
  •  

Angular 團隊在 Angular 版本 19 中發布了 resourcerxResource 函數,以方便資料檢索。
resourceloader 函數產生一個 Promise, rxResourceloader 函數產生一個Observable。 resourcerxResource 函數最終都會傳回一個 ResourceRef。 如果應用程式使用 HttpClient 傳回 Observable,工程師可以使用 rxjs-interop 套件中的rxResource 函數重構程式碼。

我有一個舊的 Angular 16 項目,它使用 HttpClient 向伺服器發出 HTTP 請求以檢索 Pokemon。 連結:https://github.com/railsstudent/ng-pokemon-signal/tree/main/projects/pokemon-signal-demo-10/ 。 我將在這篇文章中用 19.0.0-next.11 版本重寫項目,並應用這兩個函數來檢索資料。

示範1:透過 resource 函數檢索 Pokemon 資料

實作 adapter function

// pokemon.adapter.ts
 
import { Ability, DisplayPokemon, Pokemon, Statistics } from './interfaces/pokemon.interface';

export const pokemonAdapter = (pokemon: Pokemon): DisplayPokemon => {
    const { id, name, height, weight, sprites, abilities: a, stats: statistics } = pokemon;
  
    const abilities: Ability[] = a.map(({ ability, is_hidden }) => ({
      name: ability.name, isHidden: is_hidden }));
  
    const stats: Statistics[] = statistics.map(({ stat, effort, base_stat }) => ({ name: stat.name, effort, baseStat: base_stat }));
  
    return {
      id,
      name,
     … other properties
    }
}

pokemonAdapter 函數將 HTTP response 的 Pokemon 屬性變更為元件期望的形狀。

實作一個服務來定義 Pokemon 資源

// pokemon.service.ts

import { Injectable, resource, signal } from '@angular/core';
import { DisplayPokemon, Pokemon } from '../interfaces/pokemon.interface';
import { pokemonAdapter } from '../pokemon.adapter';

@Injectable({
  providedIn: 'root'
})
export class PokemonService {
  private readonly pokemonId = signal(1);

  readonly pokemonResource = resource<DisplayPokemon, number>({
    request: () => this.pokemonId(),
    loader: async ({ request: id, abortSignal }) => { 
      try {
        const response = await fetch(`https://pokeapi.co/api/v2/pokemon/${id}`, { signal: abortSignal });
        const result = await response.json() as Pokemon
        return pokemonAdapter(result);
      } catch (e) {
        console.error(e);
        throw e;
      }
    }
  });

  updatePokemonId(value: number) {
    this.pokemonId.set(value);
  }
}

PokemonService 服務使用 resource 函數透過 ID 檢索 Pokemon。 resource 選項有四個屬性:requestloaderequalinjector,對於使用 Angular Signal 的開發人員來說,這些屬性應該要很熟悉。 這個範例我只使用了 requestloaderrequest 選項是一個追蹤 pokemonId 訊號的函數。 當訊號更新時,loader 函數執行 fetch,透過 ID 檢索 Pokemon,並解析 Promise 以獲得結果。 然後,pokemonAdapter 函數在將最終結果傳回給元件之前轉換 HTTP 回應。resource 函數在 untracked 內部運行 loader 函數;因此, loader 函數中的任何訊號變化都不會導致任何計算和重新運行。

loader 函數的參數是 ResourceLoaderParams 類型。它具有以下屬性:

  • request:request 函數的結果
  • abortSignal:AbortSignal 的一個實例
  • previous:保存先前狀態的物件

如果我們想取消之前正在運行的請求,我們將把 abortSignal 傳遞給 fetch 呼叫。如果我們不使用 abortSignal,resource 函數將丟棄被後續請求取消的請求的結果。resource 的行為類似 RxJS 的 switchMap 運算子。

設計共享 Pokemon View

// pokemon.component.html

<h2>{{ title }}</h2>
<div>
    @let resource = pokemon.value();
    @let hasValue = pokemon.hasValue();
    @let isLoading = pokemon.isLoading();
    <p>Has Value: {{ hasValue }}</p>
    <p>Status: {{ pokemon.status() }}.  Status Enum: 0 - Idle, 1 - Error, 2 - Loading, 4 is Resolved.</p>
    <p>Is loading: {{ isLoading }}</p>
    <p>Error: {{ pokemon.error() }}</p>
    @if (isLoading) {
        <p>Loading the pokemon....</p>
    } @else if (resource) {
        <div class="container">
            <img [src]="resource.frontShiny" />
            <img [src]="resource.backShiny" />
        </div>
        <app-pokemon-personal [pokemon]="resource"></app-pokemon-personal>
        <app-pokemon-tab [pokemon]="resource"></app-pokemon-tab>
        <app-pokemon-controls [(search)]="pokemonId"></app-pokemon-controls>
    }
</div>

這是 PokemonComponet 和 RxPokemonCoponent 元件的共用範本。resource 函數的傳回類型是 ResourceRef,它具有以下訊號屬性:

  • value:resource 的資料
  • hasValue:resource 是否有資料
  • isLoading:狀態是否為loading/reloading
  • 狀態:資源的狀態。 0 表示 Idle,1 表示 Error,2 表示 Loading,3 表示 Reloading,4 表示 Resolved,5 表示 Local。
  • error:loader 函數拋出資源時的錯誤。

ResourceRef 擴充了 WritableResource;它公開 set()update() 來更新資源。當它發生時,狀態變成 Local。

將 Pokemon 元件中的所有內容黏合在一起

// search-input.operator.ts

import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { debounceTime, distinctUntilChanged, filter, map, Observable } from "rxjs";
import { POKEMON_MAX, POKEMON_MIN } from '../constants/pokemon.constant';

export const searchInput = (minPokemonId = POKEMON_MIN, maxPokemonId = POKEMON_MAX) => {
  return (source: Observable<number>) => source.pipe(
      debounceTime(300),
      filter((value) => value >= minPokemonId && value <= maxPokemonId),
      map((value) => Math.floor(value)),
      distinctUntilChanged(),
      takeUntilDestroyed()
    );
}
//  pokemon.component.ts

@Component({
  selector: 'app-pokemon',
  standalone: true,
  imports: [PokemonControlsComponent, PokemonPersonalComponent, PokemonTabComponent],
  templateUrl: './pokemon.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class PokemonComponent {
  private readonly pokemonService = inject(PokemonService);
  pokemonId = signal(1);
  pokemon = this.pokemonService.pokemonResource;

  constructor() {
    toObservable(this.pokemonId).pipe(searchInput())
      .subscribe((value) => this.pokemonService.updatePokemonId(value));
  }
}

pokemonId 訊號以 two-way binding 方式綁定到 PokemonControlsComponent 元件的 search model input。 pokemonId 更新時,toObservable 會向自訂 RxJS 運算子發送該值,以便在服務中設定 pokemonId 訊號之前進行 300 毫秒的 debounce。 它導致 loader 函數呼叫後端來檢索新資料並更新視圖 (view)。

示範 2:透過 rxResource 函數檢索 Pokemon 資料

實作一個服務來定義 Pokemon 資源

// rx-pokemon.service.ts

import { HttpClient } from '@angular/common/http';
import { Injectable, inject, signal } from '@angular/core';
import { rxResource } from '@angular/core/rxjs-interop';
import { catchError, delay, map, of } from 'rxjs';
import { DisplayPokemon, Pokemon } from '../interfaces/pokemon.interface';
import { pokemonAdapter } from '../pokemon.adapter';

@Injectable({
  providedIn: 'root'
})
export class RxPokemonService {
  private readonly httpClient = inject(HttpClient);
  private readonly pokemonId = signal(1);

  readonly pokemonRxResource = rxResource<DisplayPokemon | undefined, number>({
    request: () => this.pokemonId(),
    loader: ({ request: id }) =>  { 
      return this.httpClient.get<Pokemon>(`https://pokeapi.co/api/v2/pokemon/${id}`)
        .pipe(
          delay(500),
          map((pokemon) => pokemonAdapter(pokemon)),
          catchError((e) => {
            console.error(e);
            return of(undefined);
          })
        );
    }
  });

  updatePokemonId(input: number) {
    this.pokemonId.set(input); 
  }
}

此服務與 PokemonService 服務類似,不同之處在於 pokemonRxResource 成員呼叫 rxResource 函數來取得 ResourceRef。同樣, request 選項是追蹤 pokemonId 訊號的函數。 loader 函數使用 HttpClient 請求 HTTP GET 以透過 ID 檢索 Pokemon。

rxResource 函數的行為與 firstValueFrom 相同;僅考慮 Observable stream 的第一次 emission。 loader 函數將值傳送給 take(1),該值接受第一次 emission 或透過 takeUntil 取消一個 cancel Subject。

將 Pokemon 元件中的所有內容黏合在一起

//  rx-pokemon.component.ts

@Component({
  selector: 'app-rx-pokemon',
  standalone: true,
  imports: [PokemonControlsComponent, PokemonPersonalComponent, PokemonTabComponent],
  templateUrl: './pokemon.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class RxPokemonComponent {
  private readonly pokemonService = inject(RxPokemonService);
  pokemon = this.pokemonService.pokemonRxResource;  
  pokemonId = signal(1)

  constructor() {
    toObservable(this.pokemonId).pipe(searchInput())
      .subscribe((value) => this.pokemonService.updatePokemonId(value));
  }
} 

pokemonId 訊號以 two-way binding 方式綁定到 PokemonControlsComponentsearch model input。 pokemonId 更新時, toObservable 會向自訂 RxJS 運算子發送該值,以便在服務中設定 pokemonId 訊號之前進行 300 毫秒的 debounce。 它導致 loader 函數呼叫後端來檢索新資料並更新視圖 (view)。

結論:

  • resource函數監聽 request 並發出 HTTP 請求以透過 ID 檢索 Pokemon。
  • loader 函數由 request 值和 AbortSignal 實例組成。 AbortSignal 用於取消先前仍在執行的請求。如果我們不使用 AbortSignal,resource 函數會丟棄取消的請求的結果。
  • resourceloader 函數傳回一個 Promise,rxResourceloader 函數傳回一個 Observable。
  • resourcerxResource 會建立一個由許多 Signal 屬性組成的 ResourceRef
  • 如果我們使用HttpClient回傳Observables,我們可以使用 rxjs-interop 套件中的 rxResource 函數。
  • rxResource 函數在底層使用 AbortSignal;因此,它不會將 abortSignal 傳遞到 HttpClient。
  • rxResource 的行為類似 RxJS 的 firstValueFrom,傳回 Observable stream 的第一個 emission。 loader 函數完成後,rxResource 函數可以接受來自元件的新請求。

鐵人賽的第 39 天到此結束

參考:


上一篇
Day 38 - 在 Angular 19 中重置或設定 LinkedSignal 中的值
下一篇
Day 40 - 使用 Angular 原理圖從裝飾器遷移到函數
系列文
Signal API in Angular41
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言