[Angular 大師之路] 在 Angular 中應用 RxJS 的 operators (1) - 基礎篇

在前兩天介紹 AsyncPipe 時,我們不難發現當進入 RxJS 思維時,搭配 AsyncPipe 可以帶來非常多的好處!雖然透過這種不使用 subscribe() 訂閱的方式,對於許多剛進入 Angular 或 RxJS 世界的朋友會感到不適應;尤其是當有更多複雜資料要處理時,不使用 subscribe() 訂閱後處理資料還真的不知道該如何處理是好。

其實只要透過 RxJS 提供的運算子(operators),便能夠幫助我們減少大量的程式,並寫出更加好維護的程式碼!這兩天就讓我們來看看一些我個人在開發 Angular 時常用的 RxJS 應用技巧吧!

類型:觀念/技巧

難度:4 顆星

實用度:5 顆星

map

RxJS 提供的 map() operator 跟我們在 JavaScript 使用的 map() 非常類似,不同的是 JavaScript 的 map()將一個陣列換成另外一個陣列,而 RxJS 則是將一個訂閱可以得到的資料轉換成另外一筆資料,例如:

title$ = this.httpClient.get('...').pipe(
  map(data => data.title)
);

以上程式使用 HttpClientget() 方法取的資料,並透過 map() operator 將原來的資料轉換成另外一個資料,此時 title$ 就不會得到 Http Request 取得的資料,而是轉換後的結果!

另外一種常用的情境將是:有一個元件需要帶入一個陣列,當作選單資料來源,而在從網路取得資料後,希望能加入一筆預設值,此時也是使用 map() 的絕佳時機:

menuItems$ = this.httpClient.get('...').pipe(
  map(items => [{label: 'Please Select', value: null}, ...items])
);

map() 的使用頻率非常高,只要當資料需要進行轉換時,都是使用 map() 的最好時機!

tap

tap() 不會幫我們做認資料的變換,也不會影響整個 RxJS 資料流方向,某種程度的意思是「什麼都不影響」,然而在 tap() 內是可以寫一些程式的,只是跟 RxJS 整個流向無關而已,因此最常使用的就是在 tap() 內加個 console.log 就能夠方便我們進行一些除錯,如下:

title$ = this.httpClient.get('...').pipe(
  tap(data => console.log(data)), // 在 map() 前先印一次資料
  map(data => data.title),
  tap(data => console.log(data)) // 在 map() 後再次印一次,觀察 map 內程式的結果
);

透過 tap() ,搭配 console.log 就能夠確保我們使用的 operators 邏輯是否正常啦!

另外,當我們嚐到 AsyncPipe 的甜頭後,在程式中幾乎會將 subscribe() 全部移除,但偶爾真的遇到需要把資料暫存的情境呢?這時候也能夠使用 tap() ,暫時將資料存下來,做額外的處理!

data$: Observable<any>;
data: any;
  
ngOnInit() {
  this.data$ = this.httpClient.get('...').pipe(
    tap(data => this.data = data)
  );
}

buttonClick() {
  this.data.title = 'Hello';
}

switchMap

switchMap() 可以在收到 observable 資料時,轉換成另外一個 observable,不使用 switchMap() 時,什麼時候有可能會把 observable 資料換成另一個 observable 呢?比較常見的是路由內容(observable)變更時,再去 API 請求(observable)資料,當使用 subscribe() 時,看起來會如此:

constructor(private route: ActivatedRoute, private httpClient: HttpClient) { }

ngOnInit() {
  this.route.params.subscribe(params => {
    this.httpClient.get(`.../post/${params['id']}`).subscribe(post => {
      this.post = post;
    })
  });
}

這種巢狀的 subscribe() 可以說是 RxJS 中最常見的一種 anti pattern,就算沒有 Angular 的 AsyncPipe,也應該只要有一層 subscribe() 好!而當有一系列有前後順序必要的 observable 時,該怎麼處理呢?這時候就可以使用 switchMap()!如下:

this.route.params.pipe(
  switchMap(params => this.httpClient.get(`.../post/${params['id']}`))
)

如果有一系列的轉換,且資料都要保存起來呢?可以再額外透過 map() 最終組成一個大物件:

this.postData$ = this.route.params.pipe(
  switchMap(params => this.httpClient.get(`.../post/${params['id']}`).pipe(
    map(post => ({ id: params['id'], post: post }))
  )),
  switchMap(post => this.httpClient.get(`.../comments/${post.id}`).pipe(
    map(comments => Object.assign(post, { comments: comments }))
  ))
)

除了 switchMap 外,另外還有常見的 concatMapmergeMapexhauseMap,都是用來把 observable 資料轉換成另外一個 observable,只是在資料流中有不同的處理方式,有興趣就自行上官方文件看看囉!

combineLatest

在剛剛討論 switchMap 時,我們的呼叫是有順序的,而當沒有順序時,我們可能會希望平行的處理 observable,並將所有 observable 有資料後才進行後續處理,這時候就可以使用 combineLatest 來同時取得資料,不會有順序問題!

const posts$ = this.httpClient.get('.../posts');
const tags$ = this.httpClient.get('.../tags');

this.data$ = combineLatest(posts$, tags$).pipe(
  map(([posts, tags]) => ({posts: posts, tags: tags}))
)

我們也可以整合畫面上各種事件最終得到結果,例如一個包含搜尋、排序和分頁的資料,我們可以將搜尋、排序和分頁都設計成單一個 observable,在使用 combineLatest 產生搜尋結果,如下:

this.products$ = combineLatest(
  this.filterChange$,
  this.sortChange$,
  this.pageChange$
)
.pipe(
  exhaustMap(([keyword, sort, page]) =>
    this.httpClient.get(`.../products/?keyword=${keyword}&sort=${sort}&page=${page}`))
);

startWith

在使用 combineLatest 時,會在 combineLatest 內每個 observable 都有資料時才會最終取得新的結果,若是以剛才討論的搜尋程式,希望在程式一開始就給空的資料來產生搜尋結果時,就可以使用 startWith 來確保 observable 可以有起始的資料:

this.products$ = combineLatest(
  this.filterChange$.pipe(startWith('')),
  this.sortChange$.pipe(startWith({})),
  this.pageChange$.pipe(startWith({}))
)
.pipe(
  exhaustMap(([keyword, sort, page]) =>
    this.httpClient.post(`.../products`, { keyword: keyword, sort: sort, page: page}))
);

forkJoin

forkJoincombineLatest 類似,差別在於 combineLatest 在 RxJS 整個資料流有資料變更時都會發生,而 forkJoin 會在所有 observable 都完成(complete)後,才會取得最終的結果,所以對於 Http Request 的整合,我們可以直接使用 forkJoin 因為 Http Request 只會發生一次,然後就完成了!

const posts$ = this.httpClient.get('.../posts');
const tags$ = this.httpClient.get('.../tags');

this.data$ = forkJoin(posts$, tags$).pipe(
  map(([posts, tags]) => ({posts: posts, tags: tags}))
)

本日小結

今天我們把一些個人在開發 Angular 時常用的單一個 RxJS operator,當有了更多的實際案例後,就能夠產生越來越多有創意的變化囉!明天我們再來討論一些比較複雜的 RxJS operators 應用吧!

相關資源

如果您覺得我的文章有幫助,歡迎免費成為 LikeCoin 會員,幫我的文章拍手 5 次表示支持!