Dva总结|原理篇
30 秒速记
- 核心判断:状态管理的核心是把状态变化约束成可追踪的数据流,中间件在派发边界扩展异步、日志和副作用
- 原理主线:围绕 「一、环境搭建」、「二、初识Dva」、「三、计数器例子」 建立输入、状态变化与输出之间的因果关系
- 文章范围:深入解析Dva的原理、架构设计、数据流、与React集成方式,帮助前端开发者全面理解Dva在实际项目中的应用
- 边界与代价:集中式状态不是越多越好;可变性、订阅粒度和异步取消决定性能与可维护性
- 工程落地:选型要从状态归属、更新频率、调试需求和团队约束出发,而不是只比较 API 数量
Dva 本质上是把 React Router、Redux 和 Redux-saga 整合成一套以领域模型为中心的数据流方案。 每个 model 用 namespace 隔离状态,reducer 负责同步更新,effect 处理异步逻辑并派发动作。应用还能通过 hooks 扩展日志、错误处理和 loading 等能力,并支持动态加载路由与模型。需要注意,旧项目中的入口和路由写法可能受版本影响,迁移时要先核对当前依赖。
这篇文章不要按 API 清单来背。先用上面的 Mind Map 建立全局结构,再通过交互 DEMO 观察正常路径和边界路径如何改变状态;阅读正文时重点核对每一步的输入、负责执行的参与者、产生的中间状态以及最终可观察结果。遇到版本敏感结论,要把“历史实现”“当前行为”和“工程兼容策略”分开说明;遇到性能或架构取舍,则用实际指标、失败现象和验证手段支撑判断。
版本校准: 本文若分析 ReactDOM.render、旧生命周期或栈调和,应把它视为理解架构演进的历史路径。React 19 已移除 ReactDOM.render,当前客户端入口使用 createRoot;并发渲染也必须区分可中断的渲染阶段与同步提交阶段。迁移前对照 React 19 官方升级指南。
# 一、环境搭建
$ npm install dva-cli -g
# 创建应用
$ dva new dva-quickstart
# 启动
$ npm start
react项目的推荐目录结构(如果使用dva脚手架创建,则自动生成如下)
|── /mock/ # 数据mock的接口文件
|── /src/ # 项目源码目录(我们开发的主要工作区域)
| |── /components/ # 项目组件(用于路由组件内引用的可复用组件)
| |── /routes/ # 路由组件(页面维度)
| | |── route1.js
| | |── route2.js # 根据router.js中的映射,在不同的url下,挂载不同的路由组件
| | └── route3.js
| |── /models/ # 数据模型(可以理解为store,用于存储数据与方法)
| | |── model1.js
| | |── model2.js # 选择分离为多个model模型,是根据业务实体进行划分
| | └── model3.js
| |── /services/ # 数据接口(处理前台页面的ajax请求,转发到后台)
| |── /utils/ # 工具函数(工具库,存储通用函数与配置参数)
| |── router.js # 路由配置(定义路由与对应的路由组件)
| |── index.js # 入口文件
| |── index.less
| └── index.html
|── package.json # 项目信息
└── proxy.config.js # 数据mock配置
使用 antd
npm i babel-plugin-import --save
babel-plugin-import是用来按需加载antd的脚本和样式的
- 编辑
.webpackrc,使babel-plugin-import插件生效
{
+ "extraBabelPlugins": [
+ ["import", { "libraryName": "antd", "libraryDirectory": "es", "style": "css" }]
+ ]
}
# 二、初识Dva
# 2.1 Dva的特性
dva = React-Router + Redux + Redux-saga
- 仅有 5 个
API,仅有5个主要的api - 支持
HMR,支持模块的热更新 - 支持
SSR (ServerSideRender),支持服务器端渲染 - 支持
Mobile/ReactNative,支持移动手机端的代码编写 - 支持
TypeScript - 支持路由和
Model的动态加载
# 2.2 Dva的五个API

# 2.2.1 app = dva(Opts)
app = dva(Opts):创建应用,返回dva实例。(注:dva 支持多实例)**
在opts可以配置所有的hooks
const app = dva({
history,
initialState,
onError,
onAction,
onStateChange,
onReducer,
onEffect,
onHmr,
extraReducers,
extraEnhancers,
});
hooks包含如下配置项
1、 onError((err, dispatch) => {})
effect执行错误或subscription通过done主动抛错时触发,可用于管理全局出错状态- 注意:
subscription并没有加try...catch,所以有错误时需通过第二个参数done主动抛错
app.model({
subscriptions: {
setup({ dispatch }, done) {
done(e)
},
},
})
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,
},
})
这里比较常用的是,
history的配置,一般默认的是hashHistory,如果要配置history为browserHistory,可以这样
import createHistory from 'history/createBrowserHistory';
const app = dva({
history: createHistory(),
});
initialState`:指定初始数据,优先级高于 `model` 中的 `state`,默认是 `{}`,但是基本上都在`modal`里面设置相应的`state@前端进阶之旅: 代码已经复制到剪贴板
# 2.2.2 app.use(Hooks)
app.use(Hooks):配置 hooks 或者注册插件
这里最常见的就是dva-loading插件的配置
import createLoading from 'dva-loading';
...
app.use(createLoading(opts));
但是一般对于全局的
loading我们会根据业务的不同来显示相应不同的loading图标,我们可以根据自己的需要来选择注册相应的插件
# 2.2.4 app.unmodel(namespace)
取消
model注册,清理reducers,effects和subscriptions。subscription如果没有返回unlisten函数,使用app.unmodel会给予警告
# 2.2.5 app.router(Function)
注册路由表,这一操作步骤在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
如果我们想解决组件动态加载问题,我们的路由文件也可以按照下面的写法来写

