為什麼我應該學習RxJs?

前言

只要學習到Angular、Nest.Js,就一定會聽過RxJs ── 這個令人又愛又恨的套件
恨的是,框架本身就夠多東西要學了,還多一個RxJs來插花
而且這還不是個簡單的小套件!
光是學會使用RxJs,可能就需要近1個月專心學習、使用後,才能轉換思路、掌握他

愛的是,當你熟悉他之後,你會發現他有多麼強大、好用
原本需要再安裝lodash等util類輔助套件來做一些商業邏輯的資料整理、計算
但透過RxJs竟然可以很輕易的做好資料分組、運算,組合出想要的資料集,連額外輔助套件都不必安裝了!

而做為Node.Js界裡響應式開發(Reactive Programming)的代表RxJs
除了已內置在主流的後端Nest.Js、(相對沒那麼有人氣)前端Angular之外
在任何的專案裡,皆可以視需求融入使用

學會了RxJs,更是學會了響應式開發(Reactive Programming)
不論在何種語言,都有機會使用到相關觀念

簡單敘述響應式開發(Reactive Programming)

等等,學個RxJs,怎麼又多了個新名詞?!
響應式開發(Reactive Programming),簡稱ReactiveX,縮寫Rx
RxJs就是響應式開發在JavaScript裡的實現
以此類推,其他語言也有RxJava、RxGO、RxPy….

相比於我們所熟悉的指令式且帶狀態的開發習慣
響應式開發則是更高階的抽象
把指令式的程式流,轉換成事件流

--a---b-c---d---X---|->

`a, b, c, d` => 事件觸發
`X` => 遇到錯誤,即exception、error
`|` => 事件流完全結束,即completed
`--->` => 時間流

每一次的事件觸發(即上圖的a, b, c, d),都會發送出資料
再將資料丟進水管(pipe)裡,每經過一條管路,就做一次運算或整理,最後回傳結果

範例

而所謂的水管
若熟悉java,就是java 8 裡的stream功能
而在JavaScript裡,就是陣列裡會出現的寫法(又稱functional programming)

const ARRAY_DATA = [1, 2, 3, 4, 5];
// 一般的指令式寫法
const result = [];
for (const num of ARRAY_DATA) {
    const temp = num * 2;
    if (temp < 5) {
        result.push(temp);
    }
}

console.log(result); // [2, 4]

// functional programming 寫法
result = ARRAY_DATA
    .map(num => num * 2) // [2, 4, 6, 8, 10]
    .filter(num => num < 5); // [2, 4]
console.log(result) // 輸出 [2, 4]

光是語言原生提供的functional programming寫法就明顯感受到程式碼精簡、易讀性

而RxJs的水管(pipe)更加強大,可以同時處理同步、非同步函式
在前端使用,甚至還可以直接監聽click等相關事件!

可以直接想成functional programming寫法,不在侷限於陣列資料,而是任何事件!
再由各種小邏輯函式組裝而成工廠流水線

以上述例子來看,若用RxJs書寫,則程式碼看起來像這個樣子

from([1, 2, 3, 4, 5]).pipe( // from,把陣列資料逐一送出
    map(num => num * 2), // 1 * 2 = 2,繼續往後送出資料
    filter(num => num < 5), // 2 < 5,,符合條件,繼續往後。當num >= 5時,不符條件,不後送
    toArray(), // 當所有資料都處理完成後,組裝回陣列
).subscribe(console.log); // 輸出結果 [2, 4]

當然因為這例子直接用原生語法最簡單
並沒有體現出RxJs的強大之處,硬要使用RxJs則略顯冗長

旨在說明「指令式的程式流」 -> 「資料工廠流水線」的概念

可以想像更為複雜的業務情景:
工廠流水線,不在是簡單暱名函式(工人)足以負擔
其商業邏輯冗長,拆數段後封裝在許多子function裡(更多的工人!)
甚至有些是非同步函式向其他api取資料回來後才能繼續計算(比較高階的工人)
其帶來的管道寫法有多麼優雅、易讀!
可見後面例子

一個使用RxJs產報表的例子

假設有一報表需求,需經過一連串的整理、彙總後再回傳資料給前端
只需要公開genSaleReport()
其餘複雜邏輯則語意化命名藏在底下(以下範例故意將private function命名成大寫便於閱讀)

當有bug需要追查程式碼時,亦可以從入口處迅速理解這張報表的資料流大至上長什麼樣子
再往細部function深追問題

每個private function亦可視需要補上註解(假設無法很精確的用函式命名表達功能)
當滑鼠懸浮在function上時,可在IDE更直白的理解函式功能,加速問題處理!

export class SaleReportService {
    
    private logger = new Logger();

    genSaleReport(DATA: any[]) {
       return from(DATA).pipe( // 可能由DB、其他系統API取得資料
                map(datum => this.ORGANIZE_DATA(datum)), // 同步函式,將資料整理成一漂亮的object
                tap(datum => this.logger.log(`saleReport-整理成object: ${datum}`)), // LOG出組裝後的資料
                map(async datum => await this.ASYNC_CALC_DATA(datum)), // 一些複雜的計算,甚至須需用非同步函式,封裝在`ASYNC_CALC_DATA`
                tap(datum => this.logger.log(`saleReport-複雜計算後的結果: ${datum}`)), // LOG出計算後的資料
                map(datum => this.ORGANIZE_FINAL_DATA(`saleReport-最終資料: ${datum}`)), // 同步函式2,組裝出最終版的資料結構
                filter(datum => datum.price > 1000), // 上線運作一段時間後,應主管要求,只須呈現 price > 1000 的資料。簡單的再加上filter,不必費力細追邏輯後再決定該加在何處較適當
                toArray(), // 當所有資料都處理完成後,組裝成陣列,一次回傳
            );
    }
    
    /**
     * step 1: 整理資料成一漂亮的object
     */
    private ORGANIZE_DATA(datum) { ... }
    
    /**
     * step 2: 複雜的業務邏輯,還有非同步運算
     */
    private async ASYNC_CALC_DATA(datum) { ... } 
    
    /**
     * step 3: 都算完了!將資料做最後梳理,回傳完整資料集
     */
    private ORGANIZE_FINAL_DATA(datum) { ... }
}

由上述片段程式碼可以發現
不論註解書寫情況是下列哪種

  1. 寫在function上方
  2. 直接寫在每一運算子後面
  3. 不寫,function命名夠語意化可以直接理解函式功能

都可以讓維護者很迅速的進入資料流,使得程式碼更易於維護!

我是因應框架(Angular 或 Nest.Js)才來學RxJs,應該學到什麼程度?

Angular

前端使用,只需要熟悉他的觀察者模式Observer、Observable

管道指令裡知道fromoffilter等幾個基本語法就足夠使用了
因為複雜的商業邏輯在後端處理完成了,前端只會拿到整理好的結果並著重在畫面渲染

比較重要的是須熟悉Subject的使用方式。因Component層層嵌套下,不可能靠深層傳遞資料來觸發變化畫面內容
這樣的程式碼相當難維護,就連原作者自己可能都維護不下去…

應透過RxJs的Subject以很輕鬆、有效的方式跨越層層Component,依據資料觸發直接變化相關畫面

在單純的Http打後端API,Angular則是框架直接封裝了RxJs寫法,因此在各式教學文都可以看到下列寫法

this.http.get('<YOUR_DOMAIN>/api/user')
    .subscribe(res => this.data = res);

透過RxJs須訂閱才會觸發動作的特性,更有效的結合了Angular 依賴注入,將API抽象至Service裡
只有當頁面需要使用時,再觸發打API取資料,達到更佳的邏輯分離,讓程式碼更具結構化

Nest.Js

就算完全不會RxJs,也不影響你學習、開發Nest.Js!
可以把RxJs當作是錦上添花的工具,熟悉他,可以把商業邏輯封裝的更加漂亮(如前述報表例子)

不熟悉他,頂多是死背Interceptor等框架提供的寫法
反正底層會自動處理好相關事務,您只需要知道該怎麼墊邏輯即可

而商業邏輯使用熟悉的指令式寫法或Promise也不會影響需求開發

但仍建議好好掌握RxJs!
因為在複雜的商業邏輯下,RxJs可以很漂亮地像工廠流水線般處理同步、非同步函式
較不會看到第一個例子的指令式寫法,塞滿各種for, if來整理資料

亦助於更深層理解框架底層的運作方式

複雜的商業邏輯,就會使用到相當多的Operator
尤其pairwise(), groupBy(), toArray(), partition() …等
更著重在熟悉各種Operator的應用

所幸常用的就那幾個Operator,其他則有需要時再查閱官方文件就好

Subject的應用就相當的少,甚至整個專案都用不到

也就更加要求您熟悉響應式開發,轉換大腦寫程式的思路
相比於前端,在起步時比較痛苦
熟悉後,則會感受到程式碼更加的原子化(更易於寫test case)、封裝性、可讀性,帶來的優點相當多!

推薦資料

huli大大 / 希望是最淺顯易懂的 RxJS 教學

寫的非常清晰,尤其在開發思路的轉換,很明確的提供例子感受差異

JSDC 2017 / 封裝程式的藝術

很可惜演講時仍是 RxJs 5 的版本
隔年的6版就是重大的Breaking change,全面改成pipe的管道寫法
對於 Angular ,帶來的最大優點就是搖樹優化可以把未使用的運算子搖掉,大幅瘦身前端打包後的結果
但仍值得觀看,用淺顯易懂的例子講解,有助於更快速理解RxJs帶來的優勢!

系列文章

RxJs教學系列文章