写 Redux 最烦的是一个功能要开三个文件。actionTypes 定义常量,actions 写创建函数,reducers 写 switch,异步还得再挂一层 redux-thunk 或者 redux-saga,改一个字段得在四个地方来回跳。dva 把这些东西收进了一个 model 文件,namespace、state、reducers、effects、subscriptions 五个字段写完,一个业务模块的数据流就齐了。
这篇是我 2018 年做 dva 项目时整理的实践笔记,从环境搭建、5 个 API、9 个概念一路写到用户管理页的完整落地,最后还有 axios 统一拦截的那套封装。原来的代码和写法我一字没删,另外补了一节现在再看 dva 的判断,什么项目还能继续用它,什么项目应该换掉。读完你至少能自己判断 model 里的一段逻辑该放 reducer 还是 effect。
在本篇文章中,我们将从浅入深,和大家一起学习以下知识:
- dva 到底把 React-Router、Redux、redux-saga 这三样东西粘成了什么形状
- 5 个 API 各管一件什么事,hooks 里那几个配置项分别在什么场景才会用到
- model 的五个字段怎么分工,reducer 和 effect 的边界卡在哪
- subscriptions 这个别的数据流库都没有的东西,能干什么
- 一个计数器例子把 state、action、reducer、effect、subscription 全串一遍
- 用户管理页的完整实践,从抽 model 到拆容器组件、展示组件
- 同一个需求用原生 Redux 和用 dva 分别要写多少东西
- 基于 axios 做统一错误拦截、进度条和权限跳转的一份可抄封装
- 2026 年再看 dva,什么项目还能用,什么项目该换成别的方案
# 一、环境搭建
dva 自带脚手架 dva-cli,三行命令就能起一个能跑的工程,省掉自己配 webpack、react-router、redux、redux-saga 这一整套的时间。
$ npm install dva-cli -g
# 创建应用
$ dva new dva-quickstart
# 启动
$ npm start
起完项目先看目录。dva 脚手架生成的结构本身就是一份约定,routes 放页面级组件,components 放可复用的展示组件,models 放数据,services 放接口,这套分法后面所有实践都建立在它上面。
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配置
这里有个容易被忽略的点,routes 这个名字很误导人,它装的不是路由配置,而是页面维度的容器组件,真正的路由表在 router.js 里。后面 umi 出来之后把这个目录改名叫 pages,就是为了消掉这个歧义。umi 那套约定式路由怎么改变目录组织方式,我在 使用 umi 改进 dva 项目开发 里单独写过。
使用 antd
npm i babel-plugin-import --save
babel-plugin-import是用来按需加载antd的脚本和样式的
antd 那时候整包引进来样式几百 KB,不做按需加载首屏会被拖死。babel-plugin-import 干的事是在编译期把 import { Button } from 'antd' 改写成只引 Button 那一个模块和它的样式文件。
- 编辑
.webpackrc,使babel-plugin-import插件生效
{
+ "extraBabelPlugins": [
+ ["import", { "libraryName": "antd", "libraryDirectory": "es", "style": "css" }]
+ ]
}
这段配置放到今天已经不需要了。antd 从 v5 开始换成了 cssinjs 方案,样式跟着组件按需注入,不再有一个整包的 antd.css 要你手动裁剪,babel-plugin-import 也就失去了存在的意义,官方文档里明确不再推荐配它。我把原来的写法留在这儿,是因为你接手 2018 到 2020 年之间的老项目,大概率还会在 .webpackrc 或者 .umirc.js 里看到它,知道它当年解决的是什么问题,你才敢删。
# 二、初识Dva
# 2.1 Dva的特性
dva 自己给的定义就一行等式,它不是又造了一个状态管理库,而是把三个已经成熟的东西粘在一起,再补上一层约定。
dva = React-Router + Redux + Redux-saga
这个定位很关键。你在 dva 里遇到的绝大多数疑问,答案都在 Redux 或者 redux-saga 的文档里,dva 只负责把它们的样板代码收掉。
- 仅有 5 个
API,仅有5个主要的api - 支持
HMR,支持模块的热更新 - 支持
SSR (ServerSideRender),支持服务器端渲染 - 支持
Mobile/ReactNative,支持移动手机端的代码编写 - 支持
TypeScript - 支持路由和
Model的动态加载
# 2.2 Dva的五个API
整个 dva 的对外接口就下面这张图上的几个方法,从创建实例、装插件、注册 model、注册路由到启动,是一条固定的流水线。

图里这个顺序不是随便排的,它就是每个 dva 项目 index.js 的样子。app.start() 之前的每一步都在往实例上挂东西,start 的那一刻才真正创建 redux store 并把 saga 跑起来,所以 model 和路由必须在 start 之前注册完。
# 2.2.1 app = dva(Opts)
app = dva(Opts):创建应用,返回dva实例。(注:dva 支持多实例)**
这一步是整个应用唯一的全局配置入口。下面这些 hooks 平时你可能一个都用不上,但知道它们存在很有用,因为像日志、全局错误提示、状态持久化这类需求,正确的挂载点就在这里,而不是散在各个 model 里。
在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主动抛错
这个 hook 是我认为最值得第一天就配上的。effect 里如果不写 try/catch,请求挂了默认是静默失败的,页面就一直转圈,用户看不到任何提示。有了 onError 你可以在这里统一弹一个 antd 的 message.error,所有 effect 都不用自己处理异常。
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),
})
onAction 的本质就是 Redux 的 applyMiddleware,任何一个标准 redux 中间件都能塞进来。
3、 onStateChange(fn)
state改变时触发,可用于同步 state 到localStorage,服务器端等
做「刷新页面不丢购物车」「记住上次筛选条件」这类需求就靠它。要注意这个回调触发得非常频繁,每次 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状态
这个设计我一直觉得挺聪明。onEffect 相当于给每个 effect 套了一层壳,进函数前打个标记、出来再抹掉,dva-loading 就靠这一层自动维护了全局的 loading.effects['user/query'],你再也不用在每个 model 里手写 showLoading / hideLoading 这一对 reducer。
6、 onHmr(fn)
热替换相关,目前用于
babel-plugin-dva-hmr
7、 extraReducers
指定额外的
reducer,比如redux-form需要指定额外的form reducer