dva框架的使用详解及Demo教程
在前段时间,我们也学习讲解过Redux框架的基本使用,但是有很多同学在交流群里给我的反馈信息说,redux框架理解上有难度,看了之后还是一脸懵逼不知道如何下手,很多同学就转向选择使用dva框架。其实dva框架就是一个redux框架与redux-saga等框架的一个集大成者,把几个常用的数据处理框架进行了再次封装,在使用方式上给使用者带来了便利,下面我们就来简单的介绍下dva框架的基本API和基本使用
Demo运行效果图
这里和讲解Redux框架一样,作者任然是提供了两个经典的Demo示例,CounterApp 和 TodoList 来帮助初学者更好的理解和使用
http://ovyjkveav.bkt.clouddn.com/17-12-22/25369015.jpg
http://ovyjkveav.bkt.clouddn.com/17-12-22/35513550.jpg
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,默认是 hashHistory
initialState:指定初始数据,优先级高于 model 中的 state,默认是 {}
如果配置history为 browserHistory,则创建dva对象可以写成如下写法
importcreateHistoryfrom'history/createBrowserHistory';constapp = 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插件的例子:
importcreateLoadingfrom'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'constapp = dva({ onError(e) { message.error(e.message,3) },})
2、onAction(fn | fn[])
在action被dispatch时触发,用于注册 redux 中间件。支持函数或函数数组格式
例如我们要通过 redux-logger 打印日志:
importcreateLoggerfrom'redux-logger';constapp = dva({onAction: createLogger(opts),})
3、onStateChange(fn)
state改变时触发,可用于同步 state 到 localStorage,服务器端等
4、onReducer(fn)
封装 reducer 执行,比如借助 redux-undo 实现 redo/undo :
importundoablefrom'redux-undo';constapp = dva({onReducer:reducer=>{return(state, action) =>{constundoOpts = {};constnewState = 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{ reducerasformReducer }from'redux-form'constapp = 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';importIndexPagefrom'./routes/IndexPage'importTodoListfrom'./routes/TodoList'functionRouterConfig({ history }){return( )}export default RouterConfig
当然,如果我们想解决组件动态加载问题,我们的路由文件也可以按照下面的写法来写
import{ Router, Switch, Route }from'dva/router'importdynamicfrom'dva/dynamic'functionRouterConfig({ history, app }){constIndexPage = dynamic({ app,component:()=>import('./routes/IndexPage'), })constUsers = dynamic({ app,models:()=>[import('./models/users')],component:()=>import('./routes/Users'), })return( )}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(, htmlElement)
dva框架中的核心层:Model
下面是简单常规的model文件的写法
/** Created by guangqiang on 2017/12/17. */importqueryStringfrom'query-string'import*astodoServicefrom'../services/todo'exportdefault{namespace:'todo',state: {list: [] },reducers: { save(state, {payload: { list } }) {return{ ...state, list } } },effects: { *addTodo({payload: value }, { call, put, select }) {// 模拟网络请求constdata =yieldcall(todoService.query, value)console.log(data)lettempList =yieldselect(state=>state.todo.list)letlist = [] list = list.concat(tempList)consttempObj = {} tempObj.title = value tempObj.id = list.length tempObj.finished =falselist.push(tempObj)yieldput({type:'save',payload: { list }}) }, *toggle({payload: index }, { call, put, select }) {// 模拟网络请求constdata =yieldcall(todoService.query, index)lettempList =yieldselect(state=>state.todo.list)letlist = [] list = list.concat(tempList)letobj = list[index] obj.finished = !obj.finishedyieldput({type:'save',payload: { list } }) }, *delete({payload: index }, { call, put, select }) {constdata =yieldcall(todoService.query, index)lettempList =yieldselect(state=>state.todo.list)letlist = [] list = list.concat(tempList) list.splice(index,1)yieldput({type:'save',payload: { list } }) }, *modify({payload: { value, index } }, { call, put, select }) {constdata =yieldcall(todoService.query, value)lettempList =yieldselect(state=>state.todo.list)letlist = [] list = list.concat(tempList)letobj = list[index] obj.title = valueyieldput({type:'save',payload: { list } }) } },subscriptions: { setup({ dispatch, history }) {// 监听路由的变化,请求页面数据returnhistory.listen(({ pathname, search }) =>{constquery = queryString.parse(search)letlist = []if(pathname ==='todoList') { dispatch({type:'save',payload: {list} }) } }) } }}
model对象中包含5个重要的属性:
namespace
model 的命名空间,同时也是他在全局 state 上的属性,只能用字符串,不支持通过.的方式创建多层命名空间
state
reducer的初始值,优先级低于传给dva()的opts.initialState
例如:
constapp = 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 }) {// 模拟网络请求constdata =yieldcall(todoService.query, value) console.log(data) let tempList =yieldselect(state => state.todo.list) letlist= []list=list.concat(tempList)consttempObj = {} tempObj.title = value tempObj.id =list.length tempObj.finished =falselist.push(tempObj)yieldput({ type:'save', payload: {list}}) }, *toggle({ payload: index }, { call, put, select }) {// 模拟网络请求constdata =yieldcall(todoService.query, index) let tempList =yieldselect(state => state.todo.list) letlist= []list=list.concat(tempList) let obj =list[index] obj.finished = !obj.finishedyieldput({ type:'save', payload: {list} }) }, *delete({ payload: index }, { call, put, select }) {constdata =yieldcall(todoService.query, index) let tempList =yieldselect(state => state.todo.list) letlist= []list=list.concat(tempList)list.splice(index,1)yieldput({ type:'save', payload: {list} }) }, *modify({ payload: { value, index } }, { call, put, select }) {constdata =yieldcall(todoService.query, value) let tempList =yieldselect(state => state.todo.list) letlist= []list=list.concat(tempList) let obj =list[index] obj.title = valueyieldput({ 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 }) {// 监听路由的变化,请求页面数据returnhistory.listen(({ pathname, search }) =>{constquery = queryString.parse(search)letlist = []if(pathname ==='todoList') { dispatch({type:'save',payload: {list} }) } }) } }
使用dva框架和直接使用redux写法的区别
使用 redux
actions.js 文件
exportconstREQUEST_TODO ='REQUEST_TODO';exportconstRESPONSE_TODO ='RESPONSE_TODO';constrequest =count=>({type: REQUEST_TODO,payload: {loading:true, count}});constresponse =count=>({type: RESPONSE_TODO,payload: {loading:false, count}});exportconstfetch =count=>{return(dispatch) =>{ dispatch(request(count));returnnewPromise(resolve=>{ setTimeout(()=>{ resolve(count +1); },1000) }).then(data=>{ dispatch(response(data)) }) }}
reducer.js 文件
import{ REQUEST_TODO, RESPONSE_TODO }from'./actions';exportdefault(state = {loading:false,count:0}, action) => {switch(action.type) {caseREQUEST_TODO:return{...state, ...action.payload};caseRESPONSE_TODO:return{...state, ...action.payload};default:returnstate; }}
app.js 文件
importReactfrom'react';import{ bindActionCreators }from'redux';import{ connect }from'react-redux';import*asactionsfrom'./actions';constApp =({fetch, count, loading}) =>{return(
{loading ?
)}functionmapStateToProps(state){returnstate;}functionmapDispatchToProps(dispatch){returnbindActionCreators(actions, dispatch)}exportdefaultconnect(mapStateToProps, mapDispatchToProps)(App)
index.js 文件
import{ render }from'react-dom';import{ createStore, applyMiddleware }from'redux';import{ Provider }from'react-redux'importthunkMiddlewarefrom'redux-thunk';importreducerfrom'./app/reducer';importAppfrom'./app/app';conststore = createStore(reducer, applyMiddleware(thunkMiddleware));render(,document.getElementById('app'))
使用dva
model.js 文件
exportdefault{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}) {yieldput({type:'request',loading:true});letcount =yieldcall((count) =>{returnnewPromise(resolve=>{ setTimeout(()=>{ resolve(count +1); },1000); }); }, action.count);yieldput({type:'response',loading:false, count }); } }}
app.js 文件
import React from 'react'
import { connect } from 'dva';
const App =({fetch, count, loading}) =>{return(
{loading ?
)}functionmapStateToProps(state){returnstate.demo;}functionmapDispatchToProps(dispatch){return{ fetch(count){ dispatch({type:'demo/fetch', count}); } }}exportdefaultconnect(mapStateToProps, mapDispatchToProps)(App)
index.js 文件
importdvafrom'dva';importmodelfrom'./model';importAppfrom'./app';constapp = dva();app.use({});app.model(model);app.router(()=>);app.start();
我们通过上面两种不同方式来实现一个异步的计数器的代码结构发现:
使用 redux 需要拆分出action模块和reducer模块
dva将action和reducer封装到model中,异步流程采用Generator处理
总结
本篇文章主要讲解了dva框架中开发常用API和一些使用技巧,如果想查看更多更全面的API,请参照dva官方文档:https://github.com/dvajs/dva
如果同学们看完教程还是不知道如何使用dva框架,建议运行作者提供的Demo示例结合学习