支付寶前端應用架構的發展和選擇

發表於2016-12-28

下文說說我理解的支付寶前端應用架構發展史,從 roof 到 redux,再到 dva

Roof 應該是從 0.4 開始在專案裡大範圍推廣的。

Roof 0.4

Roof 0.4 接觸不多,時間久了已經沒有太多印象了,記憶中很多概念是從 baobab 裡來的,通過 cursor 訂閱資料,並基於此設計了很多針對複雜場景的解決方案。

這種方式靈活且強大,現在想想如果這條路一走到底,或許比現在要好一些。但由於概念比較多,當時大家都比較難理解 cursor 這類的概念。並且 redux 越來越流行。。

Roof 0.5

然後有了 Roof 0.5,提供 createRootContainer 和 createContainer,實現類似 react-redux 裡 Provider 和 connect 的功能,並隱藏了 cursor 的概念。

這在一定程度上迎合了 redux 使用者的習慣。但 redux 使用者卻並不滿足,就算不能用 redux,也希望能在 roof 上使用上更多 redux 相關的特性。

還有個在這一階段討論較多的另一個問題是沒有最佳實踐,大家針對同一個問題通常有不同的解法。最典型的是非同步請求的處理,有些人直接寫從 Component 生命週期裡,有些好一點的提取成 service/api,但還是在 Component 裡調,還有些提取成 Controller 。

這是 library 相對於 framework 的略勢,Roof 本質上是一個 library,要求他去解決所有開發中能想到的問題其實是不公平的。那麼如何做的? 目前看起來有兩種方案,1) boilerplate 2) framework 。這在之後會繼續探討。

Roof 0.5.5

在經歷了幾個 bugfix 版本之後,Roof 0.5.5 卻是個有新 feature 的更新。感覺從這個版本起已經不是原作者的本意了,而是對於使用者的妥協。

這個版本引入了一個新的概念:action

這也是從 redux (或者說 flux) 裡而來的,所有使用者操作都可以被理解成是一個 action,這樣在 Component 裡就不用直接調 Controller 或者 api/service 裡的介面了,一定程度上做了解耦。

這讓 Roof 越來越像 redux,但由於沒有引入 dispatch,在實際專案中遇到了不少坑。比較典型的是 action 之間的互相呼叫。

還有 action 裡更新資料之前必須重新從 state 里拉最新的進行更新之類的問題,記得當時還寫過 issue 來記錄踩過的坑。這是想引入 redux,但卻只引入一半的結果。

Roof 0.5.6@beta

然後是 Roof 0.5.6@beta,這個版本的核心已經換成了 redux,引入 reducerdispatch 來解決上個版本遇到的問題。所以本質上他等同於 react-redux,看下 import 語句應該就能明白。

大家可能注意到這個版本有個 @beta,這也是目前 Roof 的最終版本。因為大家意識到既然已經這樣了,為啥不用 redux 呢?

Redux

然後就有不少專案開始用 redux,但是 redux 是一個 library,要在團隊中使用,就需要有最佳實踐。那麼最佳實踐是什麼呢?

理解 Redux

Redux 本身是一個很輕的庫,解決 component -> action -> reducer -> state 的單向資料流轉問題。

按我理解,他有兩個非常突出的特點是:

  1. predictable,可預測性
  2. 可擴充套件性

可預測性是由於他大量使用 pure function 和 plain object 等概念(reducer 和 action creator 是 pure function,state 和 action 是 plain object),並且 state 是 immutable 的。這對於專案的穩定性會是非常好的保證。

可擴充套件性則讓我們可以通過 middleware 定製 action 的處理,通過 reducer enhancer 擴充套件 reducer 等等。從而有了豐富的社群擴充套件和支援,比如非同步處理、Form、router 同步、redu/undo、效能問題(selector)、工具支援。

Library 選擇

但是那麼多的社群擴充套件,我們應該如何選才能組成我們的最佳實踐? 以非同步處理為例。(這也是我覺得最重要的一個問題)

用地比較多的通用解決方案有這些:

redux-thunk 是支援函式形式的 action,這樣在 action 裡就可以 dispatch 其他的 action 了。這是最簡單應該也是用地最廣的方案吧,對於簡單專案應該是夠的。

redux-promise 和上面的類似,支援 promise 形式的 action,這樣 action 裡就可以通過看似同步的方式來組織程式碼。

但 thunk 和 promise 都有的問題是,他們改變了 action 的含義,使得 action 變得不那麼純粹了。

然後出現的 redux-saga 讓我眼前一亮,具體不多說了,可以看他的文件。總之給我的感覺是優雅而強大,通過他可以把所有的業務邏輯都放到 saga 裡,這樣可以讓 reducer, action 和 component 都很純粹,幹他們原本需要乾的事情。

所以在非同步處理這一環節,我們選擇了 redux-saga

最終通過一系列的選擇,我們形成了基於 redux 的最佳實踐

新的問題

但就像之前所有的 Roof 版本一樣,每個時代的應用架構都有自己的問題。Redux 這套雖然已經比較不錯,但仍避免不了在專案中暴露自己的問題。

  1. 檔案切換問題redux 的專案通常要分 reducer, action, saga, component 等等,我們需要在這些檔案之間來回切換。並且這些檔案通常是分目錄存放的:

    所以通常我們需要在這三個 user.js 中來回切換。(真實專案中通常還有 services/user.js 等) 不知大家是否有感覺,這樣的頻繁切換很容易打斷編碼思路?
  2. saga 建立麻煩我們在 saga 裡監聽一個 action 通常需要這樣寫:

    對於 redux-saga 來說,這樣設計可以讓實現更靈活,但對於我們的專案而言,大部分場景只需要用到 takeEvery 和 takeLatest 就足夠,每個 action 的監聽都需要這麼寫就顯得非常冗餘。
  3. entry 建立麻煩可以看下這個 redux entry 的例子,除了 redux store 的建立,中介軟體的配置,路由的初始化,Provider 的 store 的繫結,saga 的初始化,還要處理 reducer, component, saga 的 HMR 。這就是真實的專案應用 redux 的例子,看起來比較複雜。

dva

基於上面的這些問題,我們封裝了 dva 。dva 是基於 redux 最佳實踐 實現的 framework,api 參考了 choo,概念來自於 elm 。詳見 dva 簡介

並且除了上面這些問題,dva 還能解決 domain model 組織和團隊協作的問題。

來看個簡單的例子:(這個例子沒有非同步邏輯,所以並沒有包含 effects 和 subscriptions 的使用,感興趣的可以看 Popular Products 的 Demo)

5 步 4 個介面完成單頁應用的編碼,不需要配 middleware,不需要初始化 saga runner,不需要 fork, watch saga,不需要建立 store,不需要寫 createStore,然後和 Provider 繫結,等等。但卻能擁有 redux + redux-saga + … 的所有功能。

更多 dva 的詳解,後面會逐步補充。

最後

從 Roof 到 Redux 再到 dva 一路走來,每個方案都有自己的優點和缺陷,後一個總是為了解決前一個方案的問題而生,感覺上是在逐步變好的過程中,這讓我覺得踏實。

另外,感嘆堅持走自己的路是件很困難的事情,尤其是積累了一定使用者量之後。在害怕失去使用者和保留本心之間需要有個權衡和堅守。

相關文章