dva是什麼
dva 首先是一個基於 redux和 redux-saga的數據流方案,然後爲了簡化開發體驗,dva 還額外內置了 react-router和 fetch,所以也可以理解爲一個輕量級的應用框架。
dva 是基於現有應用架構 (redux + react-router + redux-saga 等)的一層輕量封裝。dva 是 react 和 redux 的最佳實踐。最核心的是提供了 app.model 方法,用於把 reducer, initialState, action, saga 封裝到一起。
特性
- 易學易用,僅有 6 個 api,對 redux 用戶尤其友好,配合 umi 使用更是降低爲 0 API
- elm 概念,通過 reducers, effects 和 subscriptions 組織 model
- 插件機制,比如 dva-loading可以自動處理 loading 狀態,不用一遍遍地寫 showLoading 和 hideLoading
- 支持 HMR,基於 babel-plugin-dva-hmr實現 components、routes 和 models 的 HMR
安裝
創建一個dva項目很簡單,dva爲我們提供了dva-cli,用來快速創建dva項目。
通過 npm 安裝 dva-cli 並確保版本是 0.9.1 或以上。
$ npm install dva-cli -g
$ dva -v
dva-cli version 0.10.1
dva version 2.4.1
roadhog version 2.5.0-beta.4
創建應用
$ dva new dva-quickstart
運行
$ cd dva-quickstart
$ npm start
Compiled successfully!
You can now view Your App in the browser.
Local: http://localhost:8000/
On Your Network: http://192.168.43.29:8000/
Note that the development build is not optimized.
To create a production build, use npm run build.
運行成功後,會自動打開瀏覽器,默認爲4000端口,可以看到下面的界面
目錄結構
│ .editorconfig # 編輯器配置
│ .eslintrc # eslint配置
│ .gitignore # 配置git忽略目錄或文件
│ .roadhogrc.mock.js# mock數據配置
│ .webpackrc # webpack配置文件
│ package.json # 項目配置文件
├─mock # 模擬數據文件
├─public # 公共資源
└─src
│ index.css # 主樣式
│ index.js # 入口文件
│ router.js # 路由配置文件
├─assets # 放置靜態資源
├─components # 組件
├─models # dva最核心的內容
├─routes # 路由頁面
├─services # 請求後臺數據,調用後臺API接口
└─utils # 工具類,默認放置了封裝fetch的方法
Dva的核心概念
數據的改變發生通常是通過用戶交互行爲或者瀏覽器行爲(如路由跳轉等)觸發的,當此類行爲會改變數據的時候可以通過 dispatch 發起一個 action,如果是同步行爲會直接通過 Reducers 改變 State ,如果是異步行爲(副作用)會先觸發 Effects 然後流向 Reducers 最終改變 State,所以在 dva 中,數據流向非常清晰簡明,並且思路基本跟開源社區保持一致(也是來自於開源社區)。
Dva解決了react沒有解決的問題
- 通信:組件之間如何通信?
- 數據流:數據如何和視圖串聯起來?路由和數據如何綁定?如何編寫異步邏輯?等等
通信問題
組件會發生三種通信。
- 向子組件發消息
- 向父組件發消息
- 向其他組件發消息
React 只提供了一種通信手段:傳參。對於大應用,很不方便。
數據流問題
目前流行的數據流方案有:
- Flux,單向數據流方案,以 Redux 爲代表
- Reactive,響應式數據流方案,以 Mobx 爲代表
- 其他,比如 rxjs 等
最流行的社區 React 應用架構方案如下:
- 路由:React-Router
- 架構:Redux
- 異步操作:Redux-saga
這種方案雖然說可以達到所需要的目的,但是缺點也顯而易見,要引入多個庫,項目結構複雜。
那麼dva所做的事情就是把他們進行了整合與封裝,只暴露出幾個簡單的API,使得react項目的開發變得方便快捷。
在index.js中,可以看到dva執行的步驟
import dva from 'dva';
import './index.css';
// 1. Initialize
const app = dva({});
// 2. Plugins
// app.use({});
// 3. Model
app.model(require('./models/example').default);
// 4. Router
app.router(require('./router').default);
// 5. Start
app.start('#root');
Model
首先看一下Model,這是dva的核心模塊,組件之前的數據狀態管理全都要靠它,通過Model可以解決react各組件間的傳值問題。
一個Model就是一個對象,它包含以下屬性:
- namespace: 當前 Model 的名稱。整個應用的 State,由多個小的 Model 的 State 以 namespace 爲 key 合成
- state: 該 Model 當前的狀態。數據保存在這裏,直接決定了視圖層的輸出
- reducers: Action 處理器,處理同步動作,用來算出最新的 State
- effects:Action 處理器,處理異步動作
reducers中包含多個Action 處理器,它接受兩個參數,一個是當前的state,另一個是傳入的參數,返回處理後的state來更新當前的state。
reducers: {
delete(state, { payload: id }) {
return state.filter(item => item.id !== id);
},
},
Effect 是一個generator函數,內部使用yield關鍵字,標識每一步的操作(不管是同步還是異步),dva 提供多個 effect 函數內部的處理函數,比較常用的是 call 和 put。 call 用來執行異步操作, put用來發出一個Action,類似於 dispatch。就像下面這樣:
effects:{
*deleteItem(action, { call, put}){
yield new Promise(resolve => {
setTimeout(() => {
resolve()
}, 1000);
})
yield put({
type: 'delete',
payload: action.payload
})
}
}
State只是一個存儲數據的地方,而數據的表現還是要在UI層進行呈現,還要可以實現用戶操作UI層改變State的目的,State改變後也會引起View的改變。這就需要將State與View關聯起來。
dva提供了一個叫connect的方法,connect 方法傳入的第一個參數是 mapStateToProps 函數,mapStateToProps 函數會返回一個對象,用於建立 State 到 Props 的映射關係。它返回的也是一個 React 組件,通常稱爲容器組件。
const Products = ({ dispatch, products }) => {
function handleDelete(id) {
dispatch({
type: 'products/deleteItem',
payload: id,
});
}
return (
<div>
<h2>List of Products</h2>
<ProductList onDelete={handleDelete} products={products} />
</div>
);
};
export default connect(({ products }) => ({
products,
}))(Products);
在handleDelete中有一個dispatch方法,被 connect 的 Component 會自動在 props 中擁有 dispatch 方法,它就是用來改變state的方法。其中的type的值是以“namespace/effect或reducer”的形式,payload爲傳入的額外參數。
dva的其他輸出文件
dva/router
定義路由,默認輸出 react-router接口, react-router-redux的接口通過屬性 routerRedux 輸出。
import { Router, Route, routerRedux } from 'dva/router';
dva/fetch
異步請求庫,輸出 isomorphic-fetch的接口。不和 dva 強綁定,可以選擇任意的請求庫。
import fetch from 'dva/fetch';
function parseJSON(response) {
return response.json();
}
function checkStatus(response) {
if (response.status >= 200 && response.status < 300) {
return response;
}
const error = new Error(response.statusText);
error.response = response;
throw error;
}
export default function request(url, options) {
return fetch(url, options)
.then(checkStatus)
.then(parseJSON)
.then(data => ({ data }))
.catch(err => ({ err }));
}
dva/saga
輸出 redux-saga的接口,主要用於用例的編寫。(用例中需要用到 effects)
dva/dynamic
解決組件動態加載問題的 util 方法。
在實際項目開發中,我們的model數量是很多的,每個model一般都只是在對應的幾個模塊中會用到,但在dva的初始化過程中,我們就必須把所有要用的model加載進來,這顯然對性能方面是很不友好的。如果能在對應的路由下只加載所用到的model,這將會得到很大的提升。
import dynamic from 'dva/dynamic';
const UserPageComponent = dynamic({
app,
models: () => [
import('./models/users'),
],
component: () => import('./routes/UserPage'),
});
- app: dva 實例,加載 models 時需要
- models: 返回 Promise 數組的函數,Promise 返回 dva model
- component:返回 Promise 的函數,Promise 返回 React Component
插件機制
dva中可以靈活的配置 hooks 或者註冊插件,如dva-loading 可以自動處理 loading 狀態,不用一遍遍地寫 showLoading 和 hideLoading
import createLoading from 'dva-loading';
...
app.use(createLoading(opts));
app = dva(opts)
app = dva(opts)
opts中可以設置 history 和初始化 state 數據,這裏的 state 優先級要高於model中的state。
const app = dva({
history: createHistory(),
initialState: { //初始數據
products: [
{ name: 'dva', id: 1 },
{ name: 'antd', id: 2 },
],
},
});
還可以在這裏面設置以下hooks:
onError((err, dispatch) => {})
effect 執行錯誤或 subscription 通過 done 主動拋錯時觸發,可用於管理全局出錯狀態。如果我們用 antd,那麼最簡單的全局錯誤處理通常會這麼做:
import { message } from 'antd';
const app = dva({
onError(e) {
message.error(e.message, /* duration */3);
},
});
onAction(fn | fn[])
在 action 被 dispatch 時觸發,用於註冊 redux 中間件。支持函數或函數數組格式。例如我們要通過 redux-logger打印日誌:
import createLogger from 'redux-logger';
const app = dva({
onAction: createLogger(opts),
});
onStateChange(fn)
state 改變時觸發,可用於同步 state 到 localStorage,服務器端等。
onReducer(fn)
封裝 reducer 執行。比如藉助 redux-undo實現 redo/undo :
import undoable from 'redux-undo';
const app = dva({
onReducer: reducer => {
return (state, action) => {
const undoOpts = {};
const newState = undoable(reducer, undoOpts)(state, action);
// 由於 dva 同步了 routing 數據,所以需要把這部分還原
return { ...newState, routing: newState.present.routing };
},
},
});
onEffect(fn)
封裝 effect 執行。比如 dva-loading基於此實現了自動處理 loading 狀態。
onHmr(fn)
熱替換相關,目前用於 babel-plugin-dva-hmr。
extraReducers
指定額外的 reducer,比如 redux-form需要指定額外的 form reducer:
import { reducer as formReducer } from 'redux-form'
const app = dva({
extraReducers: {
form: formReducer,
},
});
extraEnhancers
指定額外的 StoreEnhancer,比如結合 redux-persist的使用:
import { persistStore, autoRehydrate } from 'redux-persist';
const app = dva({
extraEnhancers: [autoRehydrate()],
});
persistStore(app._store);
歡迎訪問我的個人網站:www.dengzhanyong.com
喜歡的話可以關注我的公衆號:【前端筱園】
一起交流,共同成長