前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版

Dva实践总结,从五个 API 到 model 数据流落地

首页2018-09-05 16:00:43Front-End
DvaReactReduxredux-saga状态管理

写 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 的五个核心 API:dva()、app.use()、app.model()、app.router()、app.start() 的调用顺序示意

图里这个顺序不是随便排的,它就是每个 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

fe
  • 一、环境搭建
  • 二、初识Dva
    • 2.1 Dva的特性
    • 2.2 Dva的五个API
      • 2.2.1 app = dva(Opts)
      • 2.2.2 app.use(Hooks)
      • 2.2.3 app.model(ModelObject)
      • 2.2.4 app.unmodel(namespace)
      • 2.2.5 app.router(Function)
      • 2.2.6 app.start
    • 2.3 Dva九个概念
      • 2.3.1 State
      • 2.3.2 Action
      • 2.3.3 Model
      • 2.3.4 Router
    • 2.4 整体架构
    • 2.5 Dva图解
  • 三、计数器例子
  • 四、Dva实践
    • 4.1 抽离Model
    • 4.2 设计组件
      • 4.2.1 容器组件
      • 4.2.2 展示组件
      • 4.2.3 设置路由
      • 4.2.4 设计容器组件
    • 4.3 添加Reducer
      • 4.3.1 第一步:实现reducer函数
      • 4.3.2 第二步:关联Model中的数据源
      • 4.3.3 第三步:通过发起Action,在组件中获取Model中的数据
      • 4.3.4 第四步: 在index.js中添加models
    • 4.4 添加Effects
    • 4.5 把请求处理分离到service中
  • 五、使用dva框架和直接使用redux写法的区别
    • 5.1 使用 redux
    • 5.2 使用dva
  • 六、使用axios统一处理
    • 6.1 示例代码
    • 6.2 明确响应体
    • 6.3 依赖包分析
    • 6.4 axios 全局配置
    • 6.5 加载 NProgress 过渡组件
    • 6.6 网络请求成功处理
    • 6.7 网络请求失败处理
  • 七、2026 年再看 dva
  • 总结
  • 参考

← 使用umi改进dva项目开发,约定式路由与插件体系实践浅谈PWA(Progressive Web App) →