在前段时间,咱们也学习讲解过Redux框架的基本使用,可是有不少同窗在交流群里给个人反馈信息说,redux框架理解上有难度,看了以后仍是一脸懵逼不知道如何下手,不少同窗就转向选择使用dva框架。其实dva框架就是一个redux框架与redux-saga等框架的一个集大成者,把几个经常使用的数据处理框架进行了再次封装,在使用方式上给使用者带来了便利,下面咱们就来简单的介绍下dva框架的基本API和基本使用html
这里和讲解Redux框架同样,做者任然是提供了两个经典的Demo示例,CounterApp 和 TodoList 来帮助初学者更好的理解和使用node
https://github.com/guangqiang-liu/react-dva-counterreact
https://github.com/guangqiang-liu/react-dva-todoListgit
D.Va拥有一部强大的机甲,它具备两台全自动的近距离聚变机炮、可使机甲飞跃敌人或障碍物的推动器、 还有能够抵御来自正面的远程攻击的防护矩阵。—— 来自 守望先锋 。
https://github.com/dvajs/dva/blob/master/README_zh-CN.mdgithub
建立应用,返回 dva 实例(注:dva 支持多实例)web
opts
包含以下配置:redux
history
:指定给路由用的 history,默认是 hashHistoryinitialState
:指定初始数据,优先级高于 model 中的 state,默认是 {}若是配置history
为 browserHistory,则建立dva对象能够写成以下写法react-native
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, })
配置 hooks 或者注册插件。(插件最终返回的是 hooks )服务器
好比注册 dva-loading
插件的例子:
import createLoading from 'dva-loading' ... app.use(createLoading(opts))
hooks
包含以下配置项:
一、 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) }, })
二、 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, }, })
注册model,这个操做时dva中核心操做,下面单独作详解
取消 model 注册,清理 reducers, effects 和 subscriptions。subscription 若是没有返回 unlisten 函数,使用 app.unmodel 会给予警告⚠️
注册路由表,这一操做步骤在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
启动应用,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)
下面是简单常规的 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个重要的属性:
model 的命名空间,同时也是他在全局 state 上的属性,只能用字符串,不支持经过.
的方式建立多层命名空间
reducer的初始值,优先级低于传给dva()的 opts.initialState
例如:
const app = dva({ initialState: { count: 1 }, }); app.model({ namespace: 'count', state: 0, })
此时,在 app.start()
后 state.count 为 1
以 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 } } }
以 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 类型有有以下四种:
一、takeEvery
二、takeLatest
三、throttle
四、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 } }) } }
以 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} }) } }) } }
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') )
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();
咱们经过上面两种不一样方式来实现一个异步的计数器的代码结构发现:
action
模块和reducer
模块action
和reducer
封装到model
中,异步流程采用Generator处理本篇文章主要讲解了dva框架中开发经常使用API和一些使用技巧,若是想查看更多更全面的API,请参照dva官方文档:https://github.com/dvajs/dva
若是同窗们看完教程仍是不知道如何使用dva框架,建议运行做者提供的Demo示例结合学习
做者建议:同窗们能够将做者以前讲解的redux框架和dva框架对比来学习理解,这样更清楚他们之间的区别和联系。