如何使用 RxJS 處理分頁 API

這篇文章會以 node-githubgetCommits API 為例,介紹如何使用 RxJS 取得所有分頁的 commits 結果。

前言

以往在處理分頁的 API,通常都會使用遞回運算,這會讓程式碼的可讀性不佳。有鑒於最近 RxJS 正夯,想說來試著寫寫看,於是就有了這篇分享文章。

需求

首先,因為 node-github 的 getCommits API 回傳的是一個 Promise 物件,所以需要先使用 RxJS 的 fromPromise 將它轉成 Observable:

Rx.Observable
    .fromPromise(getCommits(...))

接下來,利用 node-github 提供的 hasNextPagegetNextPage,搭配 RxJS 的 expand 來處理分頁的遞回運算:

Rx.Observable
    ...
    .expand(
        (response) => hasNextPage(response)
            ? Rx.Observable.fromPromise(getNextPage(response))
            : Rx.Observable.empty()
    );

上述邏輯大概是這樣:

  1. 如果 getCommits 回傳的結果還有下一頁,就繼續 call getNextPage API
  2. 如果已經沒有下一頁,則回傳 Observable.empty() 結束 expand 運算

最後,透過 reduce 將所有分頁回傳的結果 concat 成一個 Array:

Rx.Observable
    ...
    ...
    .reduce(
        (acc, curr) => acc.concat(curr.data)
    , []);

整體程式碼大致如下:

Rx.Observable
    .fromPromise(getCommits(...))
    .expand(
        (response) => hasNextPage(response)
            ? Rx.Observable.fromPromise(getNextPage(response))
            : Rx.Observable.empty()
    )
    .reduce(
        (acc, curr) => acc.concat(curr.data)
    , []);

如果再稍微封裝一下,語法簡直優雅到無法直視 🤤:

Rx.Observable
    .fromPromise(getCommits(...))
    .expand(checkNextPage)
    .reduce(concatAllCommits);

然後看是要用 subscribe 或是 toPromise 來取得結果:

const getAllCommits$ = Rx.Observable
    .fromPromise(getCommits(...))
    .expand(checkNextPage)
    .reduce(concatAllCommits, []);

// 方法一
getAllCommits$.subscribe(
    (commits) => console.log(commits)
);

// 方法二
const commits = await getAllCommits$.toPromise();

console.log(commits);

// Output:
// [{commit}, {commit}, ...]

總結

  1. 以往的分頁 API 處理需要透過遞回運算才能完成,加上 命令式編程 的可讀性沒有 聲明式編程 的體驗佳,所以嘗試使用 RxJS 的 stream 取而代之
  2. 使用 fromPromise 將回傳物件為 Promise 的 API 轉換成 Observable
  3. 使用 expand 處理分頁的遞迴機制
  4. 使用 reduce 組合所有分頁的回傳結果
  5. 使用 toPromise 獲取結果

附件

參考