iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 27
1
Modern Web

Angular 大師之路系列 第 27

[Angular 大師之路] Day 27 - 認識 AsyncPipe (1) - 基本使用技巧

在撰寫前端程式時,我們很難避免會遇到非同步的程式處理,在大量使用 RxJS 的 Angular 中更是如此,而今天我們要介紹的 AsyncPipe 在樣板上要處理非同步可以說是非常方便的工具!

類型:觀念

難度:4 顆星

實用度:5 顆星

基本用法

先來看看一個常見的非同步狀況 - Http Request!

@Component({
  selector: 'myㄦapp',
  template: `
  <ul>
    <li *ngFor="let todo of todos">{{ todo.title }}</li>
  </ul>
  `
})
export class AppComponent {
  todos: any[];

  constructor(private httpClient: HttpClient) { }

  ngOnInit() {
    this.httpClient.get('https://jsonplaceholder.typicode.com/todos/')
      .subscribe((data: any[]) => {
        this.todos = data;
      });
  }
}

在上面程式中,我們使用 HttpClient 去呼叫一個 API ,由於 HttpClient 的 get 方法會回傳一個 observable,因此我們可以透過 subscribe 方法,去訂閱結果,並放在類別的一個參數內。

由於 Http Request 是非同步的程式, observable 也是,因此我們可以先把這個非同步的物件保留起來,而不是立刻呼叫 subscribe 方法:

import { Observable } from 'rxjs';

export class AppComponent {
  todos$: Observable<any[]>;

  constructor(private httpClient: HttpClient) { }

  ngOnInit() {
    this.todos$ = this.httpClient.get<any[]>('https://jsonplaceholder.typicode.com/todos/');
  }
}

在這裡我們改用宣告一個型別為 Observable<any[]> 的變數 todos$,並直接把 HttpClient 取得的物件指派給這個變數,而不再做訂閱的行為,因此目前不會有任何的 Http Request 發生,程式看起來也比較清爽。

在開發習慣中,我們會在變數後面加上一個 $ 符號,代表他是一個 observable。

那麼到底什麼時候才會執行訂閱動作呢?我們可以在樣板上針對 todos$ 變數後面加上一個 async 的 pipe,讓這個 AsyncPipe 來幫助我們訂閱!

<li *ngFor="let todo of todos$ | async">{{ todo.title }}</li>

如此一來就可以在樣板上由 AsyncPipe 來幫我們訂閱啦!接下來我們看看還有沒有什麼其他的技巧吧!

AsyncPipe 也可以幫助我們自動處理 Promise,不過在 Angular 中還是使用 RxJS 居多。

類延遲載入

由於我們現在把訂閱的工作交給樣板上的程式了,因此資料不會在元件開始的時候就載入,而是在樣板中有需要顯示的時候才載入,因此我們可以做出一個類似延遲載入的效果:

@Component({
  selector: 'my-app',
  template: `
  <button (click)="loadTodos()">Load Todos</button>
  <ul *ngIf="load">
    <li *ngFor="let todo of todos$ | async">{{ todo.title }}</li>
  </ul>
  `
})
export class AppComponent {
  load: boolean;
  todos$: Observable<any[]>;

  constructor(private httpClient: HttpClient) { }

  ngOnInit() {
    this.todos$ = this.httpClient.get<any[]>('https://jsonplaceholder.typicode.com/todos/');
  }

  loadTodos() {
    this.load = !this.load;
  }
}

在這段程式中,畫面一開始不會載入資料,因為 todos$ | async 的部分並沒有被載入,當按下按鈕把 this.load 設為 true 時,才會進行載入的動作,透過這種方式,我們就可以在真正需要資料時才進行載入的動作,避免不必要的 API 呼叫啦!

使用 shareReplay 避免重複載入

在上一段程式中,我們可以透過設定 this.load 來決定資料是否要被載入,而當每次 this.load 被設定為 true 時,都會再次呼叫 API,若希望只有第一次要顯示時呼叫就好,可以使用 RxJS 的 shareReplay operator,這個 operator 會保留最近 N 次(看設定) 的內容,當 observable 被訂閱時,預設會先「重播」最新 N 次的紀錄!

因此上述呼叫的程式就可以改為:

// import { shareReplay } from 'rxjs/operators'; 
ngOnInit() {
  this.todos$ = this.httpClient
    .get<any[]>('https://jsonplaceholder.typicode.com/todos/')
    // 重播最近一次的資料
    .pipe(shareReplay(1));
}

這時候可以打開 F12 的 network 頁籤看看,再切換 this.load 時,就不會重複一直載入囉!

搭配 ngIf 使用

另外一種常見的情境是搭配 *ngIf 使用,也就是在真正呼叫 Http Request 時,我們的 observable 是沒資料的,因此 *ngIf 的宿主標籤不會被顯示,搭配 AsyncPipe 訂閱後,真正得到資料時才顯示內容,如下:

<ng-container *ngIf="todos$ | async; else loading">>
  <ul>
    <li *ngFor="let todo of todos$ | async">{{ todo.title }}</li>
  </ul>
</ng-container>
<ng-template #loading>Loading...</ng-template>

這段程式可以運作得非常好,在 Http Request 還沒完成的期間顯示 Loading... 內容,直到有資料後才顯示內容,但需要使用兩次 AsyncPipe,雖然我們已經知道可以搭配 shareReplay 來避免重複呼叫,但是套用多個 AsyncPipe 還是感覺醜醜的,這時候可以加上 as ,把訂閱的結果存到另一個區域變數中,如下:

<ng-container *ngIf="todos$ | async as todos; else loading">
  <ul>
    <!-- 在內部就不需要使用 todos$ | async,而是使用 as 後面的 todos 區域變數 -->
    <li *ngFor="let todo of todos">{{ todo.title }}</li>
  </ul>
</ng-container>

這樣看起來就清爽多啦!

今天就先介紹幾種常用的情境,明天再來看看更複雜的應用情境吧!

相關資源


上一篇
[Angular 大師之路] Day 26 - 認識元件的變更偵測策略
下一篇
[Angular 大師之路] Day 28 - 認識 AsyncPipe (2) - 進階技巧
系列文
Angular 大師之路30

1 則留言

0
mattchen0512
iT邦新手 5 級 ‧ 2019-01-10 13:54:29

標題 [Angular 大師之路] Day 27 - 開場閒聊
請問標題是不是有誤?
/images/emoticon/emoticon56.gif

感謝提醒, 沒想到標題出現這種錯誤 Orz

我要留言

立即登入留言