裸 dva 项目写到第二十个页面的时候,router.js 已经三百行了。每加一个页面要做四件事,在 routes/ 建组件、在 models/ 建 model、去 index.js 里补一行 app.model(require(...))、再回 router.js 加一条 <Route>。漏掉任何一步都是白屏或者 dispatch 没反应,而且报错信息还不会告诉你漏的是哪一步。
umi 干的事就是把这四步压成一步,你在 pages/ 下建个文件,路由和 model 自己就注册好了。这篇是我 2018 年从 dva 迁到 umi 时整理的笔记,约定式路由的全部规则、model 的查找顺序、配置文件的分环境写法、mock 约定,一条条列在这儿。原文的写法我一字没删,另外补了几段现在的情况,umi 后来发过好几个大版本,有些写法已经变了,哪些能抄、哪些不能抄,我在对应的位置标了出来。
在本篇文章中,我们将从浅入深,和大家一起学习以下知识:
- umi、dva、roadhog 三者到底是什么关系,为什么它们能拆开也能合起来用
- 从扁平的
models/services/routes换成按页面组织的目录,具体解决了什么问题 - 约定式路由的八条规则,动态路由、可选动态路由、嵌套路由、404、注释扩展
- 什么时候该放弃约定式改用配置式路由,两者是互斥的
- 权限路由、路由动效、面包屑、hash 路由这几个高频需求的实现方式
.umirc.js和UMI_ENV怎么配出多套环境的配置- mock 目录的约定写法,以及怎么模拟网络延迟
- model 自动注册的完整查找规则,全局 model 和页面 model 的边界
# 一、Umi简介
一个可插拔的企业级
react应用框架。umi以路由为基础的,以及各种进阶的路由功能,并以此进行功能扩展,比如支持路由级的按需加载。然后配以完善的插件体系,覆盖从源码到构建产物的每个生命周期
这段官方定义里有两个词是重点,「以路由为基础」和「插件体系」。前者决定了你的目录长什么样,后者决定了 dva、antd、PWA 这些东西怎么接进来。这篇后面的内容基本都在展开这两句。
# 1.1 特性
- 开箱即用,内置
react、react-router等 - 支持配置的路由方式
- 完善的插件体系,覆盖从源码到构建产物的每个生命周期
- 高性能,通过插件支持
PWA、以路由为单元的code splitting等 - 支持静态页面导出,适配各种环境,比如中台业务、无线业务、egg、支付宝钱包、云凤蝶等
- 开发启动快,支持一键开启
dll和hard-source-webpack-plugin等 - 一键兼容到
IE9,基于umi-plugin-polyfills - 完善的
TypeScript支持,包括d.ts定义和umi test - 与
dva数据流的深入融合,支持duck directory、model的自动加载、code splitting等等
这一串里,对老 dva 项目冲击最大的是最后一条。duck directory 和 model 自动加载合起来的效果是,你不用再维护那份 app.model(require(...)) 的清单了。第七节会把这套规则拆开讲。
# 1.2 架构
下面这张官方架构图把 umi 的分层画得挺清楚,从下往上看会更好理解。

最底下是 webpack 和 babel,中间那层才是 umi 自己的东西,也就是内核加插件机制,再往上的 dva 整合、antd 按需加载、PWA、polyfill,全都是插件。你要是嫌某个能力用不上,把插件摘掉就行,这跟 create-react-app 那种一次性 eject 的思路完全不同。
# 1.3 和 dva、roadhog关系
roadhog是基于webpack的封装工具,目的是简化webpack的配置 umi 可以简单地理解为roadhog + 路由,思路类似next.js/nuxt.js,辅以一套插件机制,目的是通过框架的方式简化React开发dva目前是纯粹的数据流,和umi以及roadhog之间并没有相互的依赖关系,可以分开使用也可以一起使用
这三者的关系我一开始也绕了半天,后来发现按「谁管什么」来记最省事。roadhog 管构建,umi 管构建加路由,dva 只管数据流。所以你能看到三种组合,光用 dva 不用 umi(老 dva 脚手架就是这样,路由自己写在 router.js 里),光用 umi 不用 dva(数据流换成别的),或者两个一起上。
也正因为这样,这篇不会重复讲 model 里 state、reducers、effects、subscriptions 怎么写,那部分我在 Dva实践总结 里从五个 API 一路写到了完整案例。这篇只讲 umi 接进来之后,dva 的写法哪里变了。
# 二、环境搭建
umi 没有独立的 cli 包要全局装,直接用 yarn create 起一个交互式脚手架就行。
$ mkdir myapp && cd myapp
$ yarn create umi
跑完会弹出一个选择器,问你要建什么类型的项目。

选 app 之后它还会接着问要不要 TypeScript、要不要 dva、要不要 antd,按需勾就行,这几个选项对应的其实就是往 .umirc.js 的 plugins 里塞不塞对应的插件。
确定后,会根据你的选择自动创建好目录和文件

生成出来的东西比 dva-cli 少得多,没有 router.js,也没有一大堆 webpack 配置文件,这正是 umi 的风格,能靠约定推导出来的就不落成文件。
# 三、目录结构
这一节是整篇最值钱的部分。umi 带来的改变不在于某个 API,而在于你的文件该往哪儿放。
dva项目之前通常都是这种扁平的组织方式
+ models
- global.js
- a1.js
- a2.js
- b.js
+ services
- a.js
- b.js
+ routes
- PageA.js
- PageB.js
这种平铺法在页面少的时候没问题,超过二十个页面就开始难受了。models/ 里躺着四十个文件,你光看文件名分不清哪个还在用;想删掉一个下线的页面,得去三个目录里各找一遍,漏删的那个 model 还会一直被注册进 store,白占内存。
用了
umi后,可以按页面维度进行组织
+ models/global.js
+ pages
+ a
- index.js
+ models
- a1.js
- a2.js
+ services
- a.js
+ b
- index.js
- model.js
- service.js
好处是更加结构更加清晰了,减少耦合,一删全删,方便 copy 和共享
「一删全删」这四个字是我用下来感受最深的。页面下线的时候 rm -rf pages/a 就完事了,它的 model、service 全跟着走,不会留下孤儿文件。反过来,要把一个功能搬到另一个项目里,也是整个目录 copy 过去。这种把相关文件放一起的组织方式在社区里叫 duck directory,最早是 Redux 圈子里为了解决同一个功能的文件散落各处提出来的。
注意 models/global.js 还留在 src 顶层,这是有意的。登录用户信息、全局菜单、权限这类跨页面共享的数据不属于任何一个页面,放全局;只有当前页面用的数据放页面自己的目录里。这条线画不清楚,页面 model 迟早会被别的页面偷偷引用,duck directory 就白分了。
自动注册 models
+ src
+ models
- g.js
+ pages
+ a
+ models
- a.js
- b.js
+ ss
- s.js
- page.js
+ c
- model.js
+ d
+ models
- d.js
- page.js
- page.js
global model为src/models/g.js/a的page model为src/pages/a/models/{a,b,ss/s}.js/c的page model为src/pages/c/model.js/c/d的page model为src/pages/c/model.js,src/pages/c/d/models/d.js
第四条要多看两眼,/c/d 拿到的是 c/model.js 加 c/d/models/d.js 两份,也就是说页面 model 会沿着目录往上找。这个设计在做多级菜单的中后台时很省事,/order 这一层的公共数据放 pages/order/model.js,下面的 /order/list、/order/detail 都能直接用,不用提到全局去。
我踩过的坑是文件名冲突。model 的 namespace 默认取文件名,如果 pages/a/models/list.js 和 pages/b/models/list.js 同时被加载,两个 namespace 都叫 list,后注册的会盖掉前面的。生产环境下页面 model 是按需加载的,你未必撞得上;但开发模式是全量载入的,所以线下正常线上异常,或者反过来,都有可能。稳妥的做法是把 namespace 显式写成带业务前缀的名字。
一个复杂应用的目录结构如下
.
├── dist/ // 默认的 build 输出目录
├── mock/ // mock 文件所在目录,基于 express
├── config/
├── config.js // umi 配置,同 .umirc.js,二选一
└── src/ // 源码目录,可选
├── layouts/index.js // 全局布局
├── pages/ // 页面目录,里面的文件即路由
├── .umi/ // dev 临时目录,需添加到 .gitignore
├── .umi-production/ // build 临时目录,会自动删除
├── document.ejs // HTML 模板
├── 404.js // 404 页面
├── page1.js // 页面 1,任意命名,导出 react 组件
├── page1.test.js // 用例文件,umi test 会匹配所有 .test.js 和 .e2e.js 结尾的文件
└── page2.js // 页面 2,任意命名
├── global.css // 约定的全局样式文件,自动引入,也可以用 global.less
├── global.js // 可以在这里加入 polyfill
├── .umirc.js // umi 配置,同 config/config.js,二选一
├── .env // 环境变量
└── package.json
这张结构图里没有 router.js,也没有 webpack 配置,能省的全省了。下面把这些约定挨个说一遍,重点是每一条约定「省掉了什么」,这样你记起来会容易得多。
1、dist
默认输出路径,可通过配置
outputPath修改
2、mock
约定
mock目录里所有的.js文件会被解析为mock文件
不用装 json-server,也不用另起一个 node 服务,umi dev 本身就是那个 mock 服务器。
比如,新建 mock/users.js,内容如下:
export default {
'/api/users': ['a', 'b'],
}
然后在浏览器里访问
http://localhost:8000/api/users 就可以看到 ['a', 'b']了
3、src
约定 src 为源码目录,但是可选,简单项目可以不加 src 这层目录