dva框架的使用詳解及Demo教程
在前段時間,我們也學習講解過Redux框架的基本使用,但是有很多同學在交流群里給我的反饋信息說,redux框架理解上有難度,看了之后還是一臉懵逼不知道如何下手,很多同學就轉向選擇使用dva框架。其實dva框架就是一個redux框架與redux-saga等框架的一個集大成者,把幾個常用的數據處理框架進行了再次封裝,在使用方式上給使用者帶來了便利,下面我們就來簡單的介紹下dva框架的基本API和基本使用
Demo運行效果圖
這里和講解Redux框架一樣,作者任然是提供了兩個經典的Demo示例,CounterApp 和 TodoList 來幫助初學者更好的理解和使用


Demo地址
- CounterApp
https://github.com/guangqiang-liu/react-dva-counter
- TodoList
https://github.com/guangqiang-liu/react-dva-todoList
dva的由來
D.Va擁有一部強大的機甲,它具有兩台全自動的近距離聚變機炮、可以使機甲飛躍敵人或障礙物的推進器、 還有可以抵御來自正面的遠程攻擊的防御矩陣。—— 來自 守望先鋒 。
dva 官方地址
https://github.com/dvajs/dva/blob/master/README_zh-CN.md
dva核心API
- app = dva(opts)
創建應用,返回 dva 實例(注:dva 支持多實例)
opts
包含如下配置:
history
:指定給路由用的 history,默認是 hashHistoryinitialState
:指定初始數據,優先級高於 model 中的 state,默認是 {}
如果配置history
為 browserHistory,則創建dva對象可以寫成如下寫法
import createHistory from 'history/createBrowserHistory'; const app = dva({ history: createHistory(), })
另外,出於易用性的考慮,opts 里也可以配所有的 hooks ,下面包含全部的可配屬性:
const app = dva({
history, initialState, onError, onAction, onStateChange, onReducer, onEffect, onHmr, extraReducers, extraEnhancers, })
- app.use(hooks)
配置 hooks 或者注冊插件。(插件最終返回的是 hooks )
比如注冊 dva-loading
插件的例子:
import createLoading from 'dva-loading' ... app.use(createLoading(opts))
hooks
包含如下配置項:
1、 onError((err, dispatch) => {})
effect 執行錯誤或 subscription 通過 done 主動拋錯時觸發,可用於管理全局出錯狀態
注意:subscription 並沒有加 try...catch,所以有錯誤時需通過第二個參數 done 主動拋錯
例子:
app.model({ subscriptions: { setup({ dispatch }, done) { done(e) }, }, })
如果我們使用antd組件,那么最簡單的全局錯誤處理通常會這么做:
import { message } from 'antd' const app = dva({ onError(e) { message.error(e.message, 3) }, })
2、 onAction(fn | fn[])
在action被dispatch時觸發,用於注冊 redux 中間件。支持函數或函數數組格式
例如我們要通過 redux-logger 打印日志:
import createLogger from 'redux-logger'; const app = dva({ onAction: createLogger(opts), })
3、 onStateChange(fn)
state
改變時觸發,可用於同步 state 到 localStorage,服務器端等
4、 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 }; }, }, })
5、 onEffect(fn)
封裝 effect 執行。比如 dva-loading
基於此實現了自動處理 loading 狀態
6、 onHmr(fn)
熱替換相關,目前用於 babel-plugin-dva-hmr
7、 extraReducers
指定額外的 reducer,比如 redux-form
需要指定額外的 form reducer:
import { reducer as formReducer } from 'redux-form' const app = dva({ extraReducers: { form: formReducer, }, })
- app.model(model)
注冊model,這個操作時dva中核心操作,下面單獨做詳解
- app.unmodel(namespace)
取消 model 注冊,清理 reducers, effects 和 subscriptions。subscription 如果沒有返回 unlisten 函數,使用 app.unmodel 會給予警告⚠️
- app.router(({ history, app }) => RouterConfig)
注冊路由表,這一操作步驟在dva中也很重要
// 注冊路由 app.router(require('./router'))
// 路由文件 import { Router, Route } from 'dva/router'; import IndexPage from './routes/IndexPage' import TodoList from './routes/TodoList' function RouterConfig({ history }) { return ( <Router history={history}> <Route path="/" component={IndexPage} /> <Route path='/todoList' components={TodoList}/> </Router> ) } export default RouterConfig
當然,如果我們想解決組件動態加載問題,我們的路由文件也可以按照下面的寫法來寫
import { Router, Switch, Route } from 'dva/router' import dynamic from 'dva/dynamic' function RouterConfig({ history, app }) { const IndexPage = dynamic({ app, component: () => import('./routes/IndexPage'), }) const Users = dynamic({ app, models: () => [import('./models/users')], component: () => import('./routes/Users'), }) return ( <Router history={history}> <Switch> <Route exact path="/" component={IndexPage} /> <Route exact path="/users" component={Users} /> </Switch> </Router> ) } export default RouterConfig
其中dynamic(opts)
中opt包含三個配置項:
-
opts
- app: dva 實例,加載 models 時需要
- models: 返回 Promise 數組的函數,Promise 返回 dva model
- component:返回 Promise 的函數,Promise 返回 React Component
-
app.start(selector?)
啟動應用,selector 可選,如果沒有 selector 參數,會返回一個返回 JSX 元素的函數
app.start('#root')
那么什么時候不加 selector?常見場景有測試、node端、react-native 和 i18n 國際化支持
比如通過 react-intl 支持國際化的例子:
import { IntlProvider } from 'react-intl' ... const App = app.start() ReactDOM.render(<IntlProvider><App /></IntlProvider>, htmlElement)
dva框架中的核心層:Model
下面是簡單常規的 model
文件的寫法
/** Created by guangqiang on 2017/12/17. */ import queryString from 'query-string' import * as todoService from '../services/todo' export default { namespace: 'todo', state: { list: [] }, reducers: { save(state, { payload: { list } }) { return { ...state, list } } }, effects: { *addTodo({ payload: value }, { call, put, select }) { // 模擬網絡請求 const data = yield call(todoService.query, value) console.log(data) let tempList = yield select(state => state.todo.list) let list = [] list = list.concat(tempList) const tempObj = {} tempObj.title = value tempObj.id = list.length tempObj.finished = false list.push(tempObj) yield put({ type: 'save', payload: { list }}) }, *toggle({ payload: index }, { call, put, select }) { // 模擬網絡請求 const data = yield call(todoService.query, index) let tempList = yield select(state => state.todo.list) let list = [] list = list.concat(tempList) let obj = list[index] obj.finished = !obj.finished yield put({ type: 'save', payload: { list } }) }, *delete({ payload: index }, { call, put, select }) { const data = yield call(todoService.query, index) let tempList = yield select(state => state.todo.list) let list = [] list = list.concat(tempList) list.splice(index, 1) yield put({ type: 'save', payload: { list } }) }, *modify({ payload: { value, index } }, { call, put, select }) { const data = yield call(todoService.query, value) let tempList = yield select(state => state.todo.list) let list = [] list = list.concat(tempList) let obj = list[index] obj.title = value yield put({ type: 'save', payload: { list } }) } }, subscriptions: { setup({ dispatch, history }) { // 監聽路由的變化,請求頁面數據 return history.listen(({ pathname, search }) => { const query = queryString.parse(search) let list = [] if (pathname === 'todoList') { dispatch({ type: 'save', payload: {list} }) } }) } } }
model對象中包含5個重要的屬性:
- namespace
model 的命名空間,同時也是他在全局 state 上的屬性,只能用字符串,不支持通過.
的方式創建多層命名空間
- state
reducer的初始值,優先級低於傳給dva()的 opts.initialState
例如:
const app = dva({ initialState: { count: 1 }, }); app.model({ namespace: 'count', state: 0, })
此時,在 app.start()
后 state.count 為 1
- reducers
以 key/value 格式定義reducer,用於處理同步操作,唯一可以修改 state 的地方,由 action 觸發
格式為 (state, action) => newState
或 [(state, action) => newState, enhancer]
namespace: 'todo', state: { list: [] }, // reducers 寫法 reducers: { save(state, { payload: { list } }) { return { ...state, list } } }
- effects
以 key/value 格式定義 effect。用於處理異步操作和業務邏輯,不直接修改 state。由action 觸發,可以觸發action,可以和服務器交互,可以獲取全局 state 的數據等等
注意: dva框架中的effects 模塊的設計思想來源於 redux-saga
框架,如果同學們對 redux-saga
框架不熟悉,可以查看作者對 redux-saga的講解:https://www.jianshu.com/p/7cac18e8d870
格式為 *(action, effects) => void
或 [*(action, effects) => void, { type }]
type 類型有有如下四種:
1、takeEvery
2、takeLatest
3、throttle
4、watcher
// effects 寫法 effects: { *addTodo({ payload: value }, { call, put, select }) { // 模擬網絡請求 const data = yield call(todoService.query, value) console.log(data) let tempList = yield select(state => state.todo.list) let list = [] list = list.concat(tempList) const tempObj = {} tempObj.title = value tempObj.id = list.length tempObj.finished = false list.push(tempObj) yield put({ type: 'save', payload: { list }}) }, *toggle({ payload: index }, { call, put, select }) { // 模擬網絡請求 const data = yield call(todoService.query, index) let tempList = yield select(state => state.todo.list) let list = [] list = list.concat(tempList) let obj = list[index] obj.finished = !obj.finished yield put({ type: 'save', payload: { list } }) }, *delete({ payload: index }, { call, put, select }) { const data = yield call(todoService.query, index) let tempList = yield select(state => state.todo.list) let list = [] list = list.concat(tempList) list.splice(index, 1) yield put({ type: 'save', payload: { list } }) }, *modify({ payload: { value, index } }, { call, put, select }) { const data = yield call(todoService.query, value) let tempList = yield select(state => state.todo.list) let list = [] list = list.concat(tempList) let obj = list[index] obj.title = value yield put({ type: 'save', payload: { list } }) } }
- subscriptions
以 key/value 格式定義 subscription,subscription 是訂閱,用於訂閱一個數據源,然后根據需要 dispatch 相應的 action
在 app.start() 時被執行,數據源可以是當前的時間、服務器的 websocket 連接、keyboard 輸入、geolocation 變化、history 路由變化等等
格式為 ({ dispatch, history }, done) => unlistenFunction
注意:如果要使用 app.unmodel(),subscription 必須返回 unlisten 方法,用於取消數據訂閱
// subscriptions 寫法 subscriptions: { setup({ dispatch, history }) { // 監聽路由的變化,請求頁面數據 return history.listen(({ pathname, search }) => { const query = queryString.parse(search) let list = [] if (pathname === 'todoList') { dispatch({ type: 'save', payload: {list} }) } }) } }
使用dva框架和直接使用redux寫法的區別
- 使用 redux
actions.js 文件
export const REQUEST_TODO = 'REQUEST_TODO'; export const RESPONSE_TODO = 'RESPONSE_TODO'; const request = count => ({type: REQUEST_TODO, payload: {loading: true, count}}); const response = count => ({type: RESPONSE_TODO, payload: {loading: false, count}}); export const fetch = count => { return (dispatch) => { dispatch(request(count)); return new Promise(resolve => { setTimeout(() => { resolve(count + 1); }, 1000) }).then(data => { dispatch(response(data)) }) } }
reducer.js 文件
import { REQUEST_TODO, RESPONSE_TODO } from './actions'; export default (state = { loading: false, count: 0 }, action) => { switch (action.type) { case REQUEST_TODO: return {...state, ...action.payload}; case RESPONSE_TODO: return {...state, ...action.payload}; default: return state; } }
app.js 文件
import React from 'react'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import * as actions from './actions'; const App = ({fetch, count, loading}) => { return ( <div> {loading ? <div>loading...</div> : <div>{count}</div>} <button onClick={() => fetch(count)}>add</button> </div> ) } function mapStateToProps(state) { return state; } function mapDispatchToProps(dispatch) { return bindActionCreators(actions, dispatch) } export default connect(mapStateToProps, mapDispatchToProps)(App)
index.js 文件
import { render } from 'react-dom'; import { createStore, applyMiddleware } from 'redux'; import { Provider } from 'react-redux' import thunkMiddleware from 'redux-thunk'; import reducer from './app/reducer'; import App from './app/app'; const store = createStore(reducer, applyMiddleware(thunkMiddleware)); render( <Provider store={store}> <App/> </Provider> , document.getElementById('app') )
- 使用dva
model.js 文件
export default { namespace: 'demo', state: { loading: false, count: 0 }, reducers: { request(state, payload) { return {...state, ...payload}; }, response(state, payload) { return {...state, ...payload}; } }, effects: { *'fetch'(action, {put, call}) { yield put({type: 'request', loading: true}); let count = yield call((count) => { return new Promise(resolve => { setTimeout(() => { resolve(count + 1); }, 1000); }); }, action.count); yield put({ type: 'response', loading: false, count }); } } }
app.js 文件
import React from 'react' import { connect } from 'dva'; const App = ({fetch, count, loading}) => { return ( <div> {loading ? <div>loading...</div> : <div>{count}</div>} <button onClick={() => fetch(count)}>add</button> </div> ) } function mapStateToProps(state) { return state.demo; } function mapDispatchToProps(dispatch) { return { fetch(count){ dispatch({type: 'demo/fetch', count}); } } } export default connect(mapStateToProps, mapDispatchToProps)(App)
index.js 文件
import dva from 'dva'; import model from './model'; import App from './app'; const app = dva(); app.use({}); app.model(model); app.router(() => <App />); app.start();
我們通過上面兩種不同方式來實現一個異步的計數器的代碼結構發現:
-
使用 redux 需要拆分出
action
模塊和reducer
模塊 -
dva將
action
和reducer
封裝到model
中,異步流程采用Generator處理
總結
本篇文章主要講解了dva框架中開發常用API和一些使用技巧,如果想查看更多更全面的API,請參照dva官方文檔:https://github.com/dvajs/dva
如果同學們看完教程還是不知道如何使用dva框架,建議運行作者提供的Demo示例結合學習
作者建議:同學們可以將作者之前講解的redux框架和dva框架對比來學習理解,這樣更清楚他們之間的區別和聯系。
更多文章
- 作者React Native開源項目OneM【500+ star】地址(按照企業開發標准搭建框架完成開發的):https://github.com/guangqiang-liu/OneM:歡迎小伙伴們 star
- 作者簡書主頁:包含60多篇RN開發相關的技術文章http://www.jianshu.com/u/023338566ca5歡迎小伙伴們:多多關注,多多點贊
- 作者React Native QQ技術交流群:620792950 歡迎小伙伴進群交流學習
- 友情提示:在開發中有遇到RN相關的技術問題,歡迎小伙伴加入交流群(620792950),在群里提問、互相交流學習。交流群也定期更新最新的RN學習資料給大家,謝謝大家支持!
小伙伴們掃下方二維碼加入RN技術交流QQ群
