使用umi改进dva项目开发
条评论一、Umi简介
一个可插拔的企业级
react
应用框架。umi
以路由为基础的,以及各种进阶的路由功能,并以此进行功能扩展,比如支持路由级的按需加载。然后配以完善的插件体系,覆盖从源码到构建产物的每个生命周期
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
等等
1.2 架构
1.3 和 dva、roadhog关系
roadhog
是基于webpack
的封装工具,目的是简化webpack
的配置
umi 可以简单地理解为roadhog + 路由
,思路类似next.js/nuxt.js
,辅以一套插件机制,目的是通过框架的方式简化React
开发dva
目前是纯粹的数据流,和umi
以及roadhog
之间并没有相互的依赖关系,可以分开使用也可以一起使用
二、环境搭建
1 | $ mkdir myapp && cd myapp |
确定后,会根据你的选择自动创建好目录和文件
三、目录结构
dva
项目之前通常都是这种扁平的组织方式
1 | + models |
用了
umi
后,可以按页面维度进行组织
1 | + models/global.js |
好处是更加结构更加清晰了,减少耦合,一删全删,方便 copy 和共享
自动注册 models
1 | + src |
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
一个复杂应用的目录结构如下
1 | . |
1、dist
默认输出路径,可通过配置
outputPath
修改
2、mock
约定
mock
目录里所有的.js
文件会被解析为mock
文件
比如,新建mock/users.js
,内容如下:
1 | export default { |
然后在浏览器里访问
http://localhost:8000/api/users 就可以看到 ['a', 'b']
了
3、src
约定 src 为源码目录,但是可选,简单项目可以不加 src 这层目录
比如:下面两种目录结构的效果是一致的。
1 | + src |
1 | + pages |
4、src/layouts/index.js
全局布局,实际上是在路由外面套了一层
比如,你的路由是:
1 | [ |
如果有 layouts/index.js
,那么路由则变为:
1 |
|
5、src/pages
约定
pages
下所有的(j|t)sx?
文件即路由
6、src/pages/404.js
404
页面。注意开发模式下有内置 umi 提供的 404 提示页面,所以只有显式访问/404
才能访问到这个页面
7、src/pages/document.ejs
有这个文件时,会覆盖默认的 HTML 模板。需至少包含以下代码,
1 | <div id="root"></div> |
8、src/pages/.umi
这是
umi dev
时生产的临时目录,默认包含umi.js
和router.js
,有些插件也会在这里生成一些其他临时文件。可以在这里做一些验证,但请不要直接在这里修改代码,umi
重启或者pages
下的文件修改都会重新生成这个文件夹下的文件
9、src/pages/.umi-production
同
src/pagers/.umi
,但是是在umi build
时生成的,会在umi build
执行完自动删除
10、src/global.(j|t)sx?
在入口文件最前面被自动引入,可以考虑在此加入
polyfill
11、src/global.(css|less|sass|scss)
这个文件不走
css modules
,自动被引入,可以写一些全局样式,或者做一些样式覆盖
12、.umirc.js 和 config/config.js
umi
的配置文件,二选一
13、.env
环境变量,比如:
1 | CLEAR_CONSOLE=none |
四、路由配置
4.1 约定式路由
4.1.1 基础路由
假设 pages
目录结构如下:
1 | + pages/ |
那么,umi 会自动生成路由配置如下:
1 | [ |
4.1.2 动态路由
umi
里约定,带$
前缀的目录或文件为动态路由。
比如以下目录结构:
1 | + pages/ |
会生成路由配置如下:
1 | [ |
4.1.3 可选的动态路由
umi
里约定动态路由如果带$
后缀,则为可选动态路由。
比如以下结构:
1 | + pages/ |
会生成路由配置如下:
1 | [ |
4.1.4 嵌套路由
umi
里约定目录下有_layout.js
时会生成嵌套路由,以_layout.js
为该目录的layout
。
比如以下目录结构:
1 | + pages/ |
会生成路由配置如下:
1 | [ |
4.1.5 全局 layout
约定
src/layouts/index.js
为全局路由,返回一个 React 组件,通过props.children
渲染子组件。
比如:
1 | export default function(props) { |
4.1.6 不同的全局 layout
你可能需要针对不同路由输出不同的全局
layout
,umi
不支持这样的配置,但你仍可以在layouts/index.js
对location.path
做区分,渲染不同的layout
。
- 比如想要针对
/login
输出简单布局,
1 | export default function(props) { |
4.1.7 404 路由
约定
pages/404.js
为404
页面,需返回React
组件。
比如:
1 | export default () => { |
注意:开发模式下,umi 会添加一个默认的
404
页面来辅助开发,但你仍然可通过精确地访问/404
来验证 404 页面。
4.1.8 通过注释扩展路由
约定路由文件的首个注释如果包含
yaml
格式的配置,则会被用于扩展路由。
比如:
1 | + pages/ |
如果
pages/index.js
里包含:
1 | /** |
则会生成路由配置:
1 | [ |
4.2 配置式路由
如果你倾向于使用配置式的路由,可以配置
routes
,此配置项存在时则不会对src/pages
目录做约定式的解析。
比如:
1 | export default { |
注意:
component
是相对于src/pages
目录的
4.3 权限路由
umi
的权限路由是通过配置路由的Routes
属性来实现。约定式的通过yaml
注释添加,配置式的直接配上即可。
比如有以下配置:
1 | [ |
然后 umi 会用
./routes/PrivateRoute.js
来渲染/list
。
./routes/PrivateRoute.js
文件示例:
1 | export default (props) => { |
4.4 路由动效
路由动效应该是有多种实现方式,这里举 react-transition-group 的例子。
先安装依赖,
1 | $ yarn add react-transition-group |
在
layout
组件(layouts/index.js
或者pages
子目录下的 _layout.js)里在渲染子组件时用TransitionGroup
和CSSTransition
包裹一层,并以location.key
为key
,
1 | import withRouter from 'umi/withRouter'; |
上面用到的
fade
样式,可以在src
下的global.css
里定义:
1 | .fade-enter { |
4.5 面包屑
面包屑也是有多种实现方式,这里举
react-router-breadcrumbs-hoc
的例子。
先安装依赖,
1 | $ yarn add react-router-breadcrumbs-hoc |
然后实现一个 Breakcrumbs.js
,比如:
1 | import NavLink from 'umi/navlink'; |
然后在需要的地方引入此 React
组件即可。
4.6 启用 Hash 路由
umi
默认是用的Browser History
,如果要用Hash History
,需配置:
1 | export default { |
4.7 页面间跳转
在 umi 里,页面之间跳转有两种方式:声明式和命令式
声明式
基于
umi/link
,通常作为React
组件使用。
1 | import Link from 'umi/link'; |
命令式
基于
umi/router
,通常在事件处理中被调用。
1 | import router from 'umi/router'; |
五、配置
配置文件
umi
允许在.umirc.js
或config/config.js
(二选一,.umirc.js
优先)中进行配置,支持ES6
语法。
比如:
1 | export default { |
.umirc.local.js
.umirc.local.js
是本地的配置文件,不要提交到git
,所以通常需要配置到.gitignore
。如果存在,会和.umirc.js
合并后再返回。
UMI_ENV
可以通过环境变量
UMI_ENV
区分不同环境来指定配置。
举个例子,
1 | // .umirc.js |
不指定 UMI_ENV
时,拿到的配置是:
1 | { |
指定
UMI_ENV=cloud
时,拿到的配置是:
1 | { |
六、Mock 数据
使用 umi 的 mock 功能
umi
里约定mock
文件夹下的文件即mock
文件,文件导出接口定义,支持基于require
动态分析的实时刷新,支持 ES6 语法,以及友好的出错提示
1 | export default { |
当客户端(浏览器)发送请求,如:
GET /api/users
,那么本地启动的umi dev
会跟此配置文件匹配请求路径以及方法,如果匹配到了,就会将请求通过配置处理
引入 Mock.js
Mock.js
是常用的辅助生成模拟数据的第三方库,当然你可以用你喜欢的任意库来结合 roadhog 构建数据模拟功能
1 | import mockjs from 'mockjs'; |
添加跨域请求头
设置
response
的请求头即可:
1 | 'POST /api/users/create': (req, res) => { |
合理的拆分你的 mock 文件
对于整个系统来说,请求接口是复杂并且繁多的,为了处理大量模拟请求的场景,我们通常把每一个数据模型抽象成一个文件,统一放在 mock 的文件夹中,然后他们会自动被引入
模拟延迟
为了更加真实的模拟网络数据请求,往往需要模拟网络延迟时间
- 手动添加
setTimeout
模拟延迟
你可以在重写请求的代理方法,在其中添加模拟延迟的处理,如:
1 | 'POST /api/forms': (req, res) => { |
使用插件模拟延迟
上面的方法虽然简便,但是当你需要添加所有的请求延迟的时候,可能就麻烦了,不过可以通过第三方插件来简化这个问题,如:
roadhog-api-doc#delay
。
1 | import { delay } from 'roadhog-api-doc'; |
联调
当本地开发完毕之后,如果服务器的接口满足之前的约定,那么你只需要不开本地代理或者重定向代理到目标服务器就可以访问真实的服务端数据,非常方便
七、结合dva实践
自
>= umi@2
起,dva
的整合可以直接通过umi-plugin-react
来配置
特性
- 按目录约定注册
model
,无需手动app.model
- 文件名即
namespace
,可以省去 model 导出的namespace key
- 无需手写
router.js
,交给umi
处理,支持model
和component
的按需加载 - 内置
query-string
处理,无需再手动解码和编码 - 内置
dva-loading
和dva-immer
,其中dva-immer
需通过配置开启 - 开箱即用,无需安装额外依赖,比如
dva
、dva-loading
、dva-immer
、path-to-regexp
、object-assign
、react
、react-dom
等`
使用
1 | $ yarn add umi-plugin-react |
然后在 .umirc.js
里配置插件:
1 | export default { |
推荐开启 dva-immer
以简化 reducer
编写,
1 | export default { |
model 注册
model
分两类,一是全局model
,二是页面model
。全局model
存于/src/models/
目录,所有页面都可引用;页面 model 不能被其他页面所引用。
规则如下:
src/models/**/*.js
为global model
src/pages/**/models/**/*.js
为page model
global model
全量载入,page model
在production
时按需载入,在development
时全量载入page model
为page js
所在路径下models/**/*.js
的文件page model
会向上查找,比如page js
为pages/a/b.js
,他的page model
为pages/a/b/models/**/*.js
+pages/a/models/**/*.js
,依次类推- 约定
model.js
为单文件 model,解决只有一个model
时不需要建 models 目录的问题,有model.js
则不去找models/**/*.js
1 | + src |
如上目录:
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
八、问题汇总
1、如何配置 onError、initialState 等 hook?
新建
src/dva.js
,通过导出的config
方法来返回额外配置项,比如:
1 | import { message } from 'antd'; |
2、url 变化了,但页面组件也刷新,是什么原因?
layouts/index.js
里如果用了connect
传数据,需要用umi/withRouter
高阶一下
1 | import withRouter from 'umi/withRouter'; |
3、如何访问到 store 或 dispatch 方法?
1 | window.g_app._store |
4、如何禁用包括 component 和 models 的按需加载?
在
.umirc.js
里配置:
1 | export default { |
如果不用page.js的命名,倒是能生成路由,但是model、service、components就全部变路由了
不用 page.js,然后通过 umi-plugin-routes 过滤掉不需要的路由,参考 https://github.com/zuiidea/antd-admin/blob/develop/.umirc.js#L4-L16
.umirc.mock.js 这个文件怎么配置呢?
可以不用配置,在 mock/ 下建文件写 mock 代码即可。