經過前面四篇的鋪墊,終於輪到我們的主角dva了,就是下面這個美女:
先擦一擦哈喇子,我們來介紹一下,dva出自於暴雪出品的一款遊戲《守望先鋒》,援引官方的角色介紹:
D.Va擁有一部強大的機甲,它具有兩臺全自動的近距離聚變機炮、可以使機甲飛躍敵人或障礙物的推進器、 還有可以抵禦來自正面的遠端攻擊的防禦矩陣。
然後呢,螞蟻金服的一位架構師sorrycc很迷這位美女,正巧剛開發了一款前端框架沒有名字,作為一個向女神獻禮的專案,dva框架就此誕生。
實際上,dva只是基於現有開源框架的一層輕量封裝,並沒有引入任何新概念:
- React:管理View
- react-router:管理路由
- Redux:管理Model
- redux-saga:管理非同步呼叫(副作用)
再來看一下框架圖,是不是都是熟悉的配方,熟悉的味道?
當然,也不是完全沒有新東西,其中有一個Subscription好像之前沒有見過,這是一種資料來源訂閱機制,資料來源可以是鍵盤輸入事件、路由變化、伺服器的 websocket 連線等等。你可以在資料發生變化時收到通知,並派發必要的action。
實際上,dva是一個整合者,它的目標是解決“Code is everywhere”問題。當我們同時使用上面這些框架時,一般會呈現下面這種型別的檔案結構:
+ src
+ sagas
- user.js
+ reducers
- user.js
+ actions
- user.js
複製程式碼
然後,當我們需要實現一個功能時,就需要在這幾個檔案之間來回切換。。。
另一方面,dva還試圖隱藏一些經常重複書寫的routine程式碼,讓開發者能夠更加專注於業務邏輯。比如我們寫一個應用的入口檔案,需要做下面這麼多事情:
- 配置middleware
- 建立store
- 新增<Provider>繫結
- 建立watcher saga和root saga
- 啟動saga
- 。。。
實際上,可能95%以上的專案中這些程式碼都是一模一樣的,我們不需要每次都花費時間來重新寫一遍這些程式碼。
下面開始正式介紹dva 1.0相關的內容,dva 2.0做了一些優化升級,後面專門有一節介紹具體變化。
8個概念
其實基本都是前面幾篇文章裡介紹過的概念:
- state:也就是全域性唯一的Store
- action:即Redux中的action
- model:這是dva抽象出來的一個概念,為了把下面這些東西放到一個統一的檔案裡
- reducer:即Redux中的reducer
- effect:即redux-saga中的worker saga
- subscription:前面介紹過,用於訂閱資料來源
- router:即react-router中的<Router>
- route:即react-router中的<Route/>
最終寫出來的model.js會類似下面這個樣子,可以發現所有相關程式碼都放到一起了,不需要在多個檔案之間來回切換了(這裡的namespace就是以前Redux中的reducer的名字):
export default {
namespace: 'transactions',
state: {
txs: []
},
subscriptions: {
setup({ dispatch, history }) {
history.listen(location => {
if (location.pathname === '/transactions/list') {
dispatch({type: 'fetch'});
}
});
},
},
effects: {
*fetch({ payload }, {call, select, put}) {
const { result } = yield call(apis.fetchTxs)
yield put({type:'addTx', payload: result})
},
},
reducers: {
addTx(state, { payload }) {
return { ...state, txs: payload };
},
},
}
複製程式碼
7個API
dva只有7個API,所以上手基本上沒有什麼難度:
- app = dva(opts):建立dva物件
- app.use(hooks):使用外掛(後面介紹)
- app.model(model):註冊model
- app.unmodel(namespace):取消model註冊
- app.replaceModel(model):替換model
- app.router(({ history, app }) => RouterConfig):配置路由
- app.start(selector?):啟動應用(引數是根元件id)
下面這個連結展示了5步建立單頁應用的例子:github.com/sorrycc/blo…
4大模組
這個主要是從程式碼結構上來劃分的,一般分為下面4類模組:
- service:執行非同步任務的API
- model:包含effect, reducer, subscription
- component:無狀態元件
- 傳送action觸發狀態更新
- action = 'namespace/effect'或者'namespace/reducer'
- route:各種container(這個名字取得不太好)
- 通過connect()連線到model
另外,由於dva 1.0使用的是react-router v3,所以最外層還有一個router.js用於配置靜態路由。所以一般的目錄結構如下所示:
+ src/
+ services/
- users.js
+ models/
- users.js
+ components/
+ users/
- users.js
- users.css
+ routes/
- users.js
- router.js
複製程式碼
dva 2.0中採用了react-router v4,就不需要router.js了。另外,現在官方推薦搭配使用umi(烏米,sorrycc最新的開源專案),可以自動幫你註冊model、根據目錄結構生成路由配置,目錄結構會變成下面這個樣子:
+ src/
+ models/
- global.js
+ pages/
+ users/
+ index.js
+ services
- users.js
+ models/
- users.js
+ components/
+ users/
- users.js
- users.css
複製程式碼
可以發現,把route以及相關聯的model都放到pages目錄中了,可以達到減少耦合,一刪全刪的功能。
外掛(Plugin)
前面提到過一個API可以註冊“外掛”,所謂外掛就是一些生命週期“鉤子(hooks)”,可以在事件發生時做一些額外處理。包括下面這些型別的鉤子:
- onError
- onStateChange
- onAction
- onHmr:熱替換(Hot Module Replacement)
- onReducer:reducer被呼叫
- onEffect:effect被呼叫
- extraReducers:額外的reducer被呼叫(比如搭配合redux-form)
- extraEnhancers:額外的store enhancer被呼叫(比如搭配redux-persist)
一個比較典型的例子是頁面載入資料時轉圈圈,載入完畢後隱藏,這是一個很多地方都需要用到的場景。官方提供了一個dva-loading外掛,可以自動在非同步請求資料時把loading設定為true,獲得資料後把loading設定為false。這樣你就可以在元件中繫結loading實現轉圈圈的自動控制了:
// index.js
import createLoading from 'dva-loading';
app.use(createLoading());
// components
function mapStateToProps(state) {
const { list, total, page } = state.users;
return {
loading: state.loading.models.users,
list,
total,
page,
};
}
複製程式碼
和redux+saga的對應關係
雖然dva只是一層輕量級封裝,但是做了一些特殊的命名約定,剛開始寫程式碼的時候會有點迷糊,搞不清楚跟之前直接使用redux+saga的時候的對應關係,這裡也幫大家梳理一下。
直接使用redux+saga的流程如下所示:
- component傳送請求action
- saga呼叫service,然後put一個結果action
- reducer從action中獲取type和payload,修改state(可以使用combinedReducer,會依次遍歷)
- component在mapStateToProps()中通過state.<reducer名字>獲取更新
使用dva時的流程如下所示:(觸發effect為例)
- component傳送請求action,type約定為namespace/effect
- effect呼叫service,然後put一個結果action,type約定為namespace/reducer(同一個model中可以省略namespace)
- reducer從action中獲取type和payload,修改state(每個reducer只處理一種型別的action,type約定為reducer的名字)
- component在mapStateToProps()中通過state.<namespace>獲取更新
dva2.0的變動
目前dva已經進化到2.0版本,除了採用了react-router v4以外,還有一些細節上的變動:
-
dispatch一個effect 型別的action時返回一個Promise,方便檢視層回撥
-
新增 dynamic 介面,配合 react-router@4 處理元件的按需載入
-
take 自動補全 namespace 字首
-
effect 前後會額外觸發
/@@start
和/@@end
的 action,可利用此約定實現 put 的同步執行 -
同名 reducer 和 effect 不會 fallthrough(即兩者都執行),而是僅執行 effect
具體細節可以參見:github.com/sorrycc/blo…
今天就說到這裡,老規矩,上一張思維導圖結束本篇文章:
參考: