下文說說我理解的支付寶前端應用架構發展史,從 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 的概念。
1 2 3 4 5 6 7 8 9 |
// 定義 state createRootContainer({ user: { name: 'chris', age: 30 } })(App); // 繫結 state createContainer({ myUser: 'user', })(UserInfo); |
這在一定程度上迎合了 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 裡的介面了,一定程度上做了解耦。
1 2 3 4 5 6 |
createActionContainer({ myUser: 'user', }, { // 繫結 actions userActions, })(UserInfo); |
這讓 Roof 越來越像 redux,但由於沒有引入 dispatch
,在實際專案中遇到了不少坑。比較典型的是 action 之間的互相呼叫。
1 2 3 4 |
function actionA() { actionB(); } function actionB() {} |
還有 action 裡更新資料之前必須重新從 state 里拉最新的進行更新之類的問題,記得當時還寫過 issue 來記錄踩過的坑。這是想引入 redux,但卻只引入一半的結果。
Roof 0.5.6@beta
然後是 Roof 0.5.6@beta,這個版本的核心已經換成了 redux,引入 reducer
和 dispatch
來解決上個版本遇到的問題。所以本質上他等同於 react-redux,看下 import
語句應該就能明白。
1 2 |
import { createStore, combineReducers } from 'redux'; import { createDispatchContainer, createRootContainer } from 'roof'; |
大家可能注意到這個版本有個 @beta
,這也是目前 Roof 的最終版本。因為大家意識到既然已經這樣了,為啥不用 redux 呢?
Redux
然後就有不少專案開始用 redux,但是 redux 是一個 library,要在團隊中使用,就需要有最佳實踐。那麼最佳實踐是什麼呢?
理解 Redux
Redux 本身是一個很輕的庫,解決 component -> action -> reducer -> state 的單向資料流轉問題。
按我理解,他有兩個非常突出的特點是:
- predictable,可預測性
- 可擴充套件性
可預測性是由於他大量使用 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 這套雖然已經比較不錯,但仍避免不了在專案中暴露自己的問題。
- 檔案切換問題redux 的專案通常要分 reducer, action, saga, component 等等,我們需要在這些檔案之間來回切換。並且這些檔案通常是分目錄存放的:
1234567+ src+ sagas- user.js+ reducers- user.js+ actions- user.js
所以通常我們需要在這三個 user.js 中來回切換。(真實專案中通常還有services/user.js
等) 不知大家是否有感覺,這樣的頻繁切換很容易打斷編碼思路? - saga 建立麻煩我們在 saga 裡監聽一個 action 通常需要這樣寫:
1234567891011function *userCreate() {try {// Your logic here} catch(e) {}}function *userCreateWatcher() {takeEvery('user/create', userCreate);}function *rootSaga() {yield fork(userCreateWatcher);}
對於 redux-saga 來說,這樣設計可以讓實現更靈活,但對於我們的專案而言,大部分場景只需要用到 takeEvery 和 takeLatest 就足夠,每個 action 的監聽都需要這麼寫就顯得非常冗餘。 - 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)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
import React from 'react'; import dva, { connect } from 'dva'; import { Route } from 'dva/router'; // 1. Initialize const app = dva(); // 2. Model app.model({ namespace: 'count', state: 0, reducers: { ['count/add' ](count) { return count + 1 }, ['count/minus'](count) { return count - 1 }, }, }); // 3. View const App = connect(({ count }) => ({ count }))(function(props) { return ( <div> <h2>{ props.count }</h2> <button key="add" onClick={() => { props.dispatch({type: 'count/add'})}}>+</button> <button key="minus" onClick={() => { props.dispatch({type: 'count/minus'})}}>-</button> </div> ); }); // 4. Router app.router( <Route path="/" component={App} /> ); // 5. Start app.start(document.getElementById('root')); |
5 步 4 個介面完成單頁應用的編碼,不需要配 middleware,不需要初始化 saga runner,不需要 fork, watch saga,不需要建立 store,不需要寫 createStore,然後和 Provider 繫結,等等。但卻能擁有 redux + redux-saga + … 的所有功能。
更多 dva 的詳解,後面會逐步補充。
最後
從 Roof 到 Redux 再到 dva 一路走來,每個方案都有自己的優點和缺陷,後一個總是為了解決前一個方案的問題而生,感覺上是在逐步變好的過程中,這讓我覺得踏實。
另外,感嘆堅持走自己的路是件很困難的事情,尤其是積累了一定使用者量之後。在害怕失去使用者和保留本心之間需要有個權衡和堅守。