UmiJS应用框架整理

介绍

整理自umi官方文档

umi,中文可发音为乌米,是一个可插拔的企业级 react 应用框架。umi 以路由为基础的,支持类 next.js 的约定式路由,以及各种进阶的路由功能,并以此进行功能扩展,比如支持路由级的按需加载。然后配以完善的插件体系,覆盖从源码到构建产物的每个生命周期,支持各种功能扩展和业务需求,目前内外部加起来已有 50+ 的插件。

umi 是蚂蚁金服的底层前端框架,已直接或间接地服务了 600+ 应用,包括 java、node、H5 无线、离线(Hybrid)应用、纯前端 assets 应用、CMS 应用等。

快速上手

环境准备

首先得有 node,并确保 node 版本是 8.10 或以上。(mac下推荐使用 nvm 来管理 node 版本)

1
2
$ node -v
8.1x

推荐使用 yarn 管理 npm 依赖,并使用国内源(阿里用户使用内网源)。

1
2
3
4
5
6
7
8
9
# 国内源
$ npm i yarn tyarn -g
# 后面文档里的 yarn 换成 tyarn
$ tyarn -v

# 阿里内网源
$ tnpm i yarn @ali/yarn -g
# 后面文档里的 yarn 换成 ayarn
$ ayarn -v

然后全局安装 umi,并确保版本是 2.0.0 或以上。

1
2
3
$ yarn global add umi
$ umi -v
2.0.0

脚手架

先找个地方建个空目录。

1
mkdir myapp && cd myapp

然后通过 umi g 创建一些页面,

1
2
$ umi g page index
$ umi g page users

umi g 是 umi generate 的别名,可用于快速生成 component、page、layout 等,并且可在插件里被扩展,比如 umi-plugin-dva 里扩展了 dva:model,然后就可以通过 umi g dva:model foo 快速 dva 的 model。

这里的 pages 目录是页面所在的目录,umi 里约定默认情况下 pages 下所有的 js 文件即路由。

然后启动本地服务器,

1
$ umi dev

约定式路由

然后我们在 index 和 users 直接加一些路由跳转逻辑。

先修改 pages/index.js,

1
2
3
4
5
6
7
8
9
10
11
+ import Link from 'umi/link';
import styles from './index.css';

export default function() {
return (
<div className={styles.normal}>
<h1>Page index</h1>
+ <Link to="/users">go to /users</Link>
</div>
);
}

再修改 pages/users.js,

1
2
3
4
5
6
7
8
9
10
11
+ import router from 'umi/router';
import styles from './index.css';

export default function() {
return (
<div className={styles.normal}>
<h1>Page index</h1>
+ <button onClick={() => { router.goBack(); }}>go back</button>
</div>
);
}

后浏览器验证,应该已经可以在 index 和 users 两个页面之间通过路由跳转了。

构建

执行 umi build,构建产物默认生成到 ./dist 下

本地验证

发布之前,可以通过 serve 做本地验证,

1
2
3
4
5
6
7
8
9
$ yarn global add serve
$ serve ./dist

Serving!

- Local: http://localhost:5000
- On Your Network: http://{Your IP}:5000

Copied local address to clipboard!

访问 http://localhost:5000,正常情况下应该是和 umi dev 一致的。

通过脚手架创建项目

介绍 create-umi

umi 通过 create-umi 提供脚手架能力,包含:
.project,通用项目脚手架,支持选择是否启用 TypeScript,以及 .umi-plugin-react 包含的功能
.ant-design-pro,仅包含 ant-design-pro 布局的脚手架,具体页面可通.过 umi block 添加
.block,区块脚手架
.plugin,插件脚手架
.library,依赖(组件)库脚手架,基于 umi-plugin-library

创建 umi 项目

你可以通过 yarn create umi 或 npm create umi 使用 create-umi。推荐使用 yarn create 命令,能确保每次使用最新的脚手架。

首先,在新目录下使用 yarn create umi,

1
2
$ mkdir myapp && cd myapp
$ yarn create umi

选择 project,

1
2
3
4
5
6
? Select the boilerplate type (Use arrow keys)
ant-design-pro - Create project with an layout-only ant-design-pro boilerplate, use together with umi block.
❯ app - Create project with a simple boilerplate, support typescript.
block - Create a umi block.
library - Create a library with umi.
plugin - Create a umi plugin.

选择是否使用 TypeScript,

1
? Do you want to use typescript? (y/N)

然后,选择你需要的功能,功能介绍详见 plugin/umi-plugin-react,

1
2
3
4
5
? What functionality do you want to enable? (Press <space> to select, <a> to toggle all, <i> to invert selection)
❯◯ antd
◯ dva
◯ code splitting
◯ dll

确定后,会根据你的选择自动创建好目录和文件,然后安装依赖,

1
$ yarn

最后通过 yarn start 启动本地开发,

1
$ yarn start

如果顺利,在浏览器打开 http://localhost:8000 可看到界面

例子

https://umijs.org/zh/guide/examples.html

目录及约定

在文件和目录的组织上,umi 更倾向于选择约定的方式。

一个复杂应用的目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.
├── 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
├── app.js // 运行时配置文件
├── .umirc.js // umi 配置,同 config/config.js,二选一
├── .env // 环境变量
└── package.json

ES6 语法

配置文件、mock 文件等都有通过 @babel/register 注册实时编译,所以可以和 src 里的文件一样,使用 ES6 的语法和 es modules 。

dist

默认输出路径,可通过配置 outputPath 修改。

mock

此目录下所有的 .js 文件(包括 _ 前缀的)都会被解析为 mock 文件。比如,新建 mock/users.js,内容如下:

1
2
3
export default {
'/api/users': ['a', 'b'],
}

然后在浏览器里访问 http://localhost:8000/api/users 就可以看到 [‘a’, ‘b’] 了。如果想忽略 mock 文件夹下的部分文件,参考 mock.exclude 配置。

src

约定 src 为源码目录,如果不存在 src 目录,则当前目录会被作为源码目录。比如:下面两种目录结构的效果是一致的。

1
2
3
4
5
6
+ src
+ pages
- index.js
+ layouts
- index.js
- .umirc.js

1
2
3
4
5
+ pages
- index.js
+ layouts
- index.js
- .umirc.js

src/layouts/index.js

注:配置式路由下无效。
全局布局,在路由外面套的一层路由。比如,你的路由是:

1
2
3
4
[
{ path: '/', component: './pages/index' },
{ path: '/users', component: './pages/users' },
]

如果有 layouts/index.js,那么路由就会变为:

1
2
3
4
5
6
[
{ path: '/', component: './layouts/index', routes: [
{ path: '/', component: './pages/index' },
{ path: '/users', component: './pages/users' },
] }
]

src/pages

注:配置式路由下无效。

约定 pages 下所有的 js、jsx、ts 和 tsx 文件即路由。关于更多关于约定式路由的介绍,请前往路由章节。

src/pages/404.js

404 页面。注意开发模式下有内置 umi 提供的 404 提示页面,所以只有显式访问 /404 才能访问到这个页面。

src/pages/document.ejs

有这个文件时,会覆盖默认的 HTML 模板。模板里需至少包含根节点的 HTML 信息,

1
<div id="root"></div>

src/pages/.umi

这是 umi dev 时生产的临时目录,默认包含 umi.js 和 router.js,有些插件也会在这里生成一些其他临时文件。可以在这里做一些验证,但请不要直接在这里修改代码,umi 重启或者 pages 下的文件修改都会重新生成这个文件夹下的文件。

src/pages/.umi-production

同 src/pages/.umi,但是是在 umi build 时生成的,umi build 执行完自动删除。

.test.(js|ts) 和 .e2e.(js|ts)

测试文件,umi test 会查找所有的 .test.js 和 .e2e.js 文件来跑测试。

src/global.(js|ts)

此文件会在入口文件的最前面被自动引入,可以在这里加载补丁,做一些初始化的操作等。

src/global.(css|less|sass|scss)
此文件不走 css modules,且会自动被引入,可以在这里写全局样式,以及做样式覆盖。

src/app.(js|ts)

运行时配置文件,可以在这里扩展运行时的能力,比如修改路由、修改 render 方法等。

.umirc.(js|ts) 和 config/config.(js|ts)

编译时配置文件,二选一,不可共存。

.env

环境变量配置文件,比如:

CLEAR_CONSOLE=none
BROWSER=none

路由

下文介绍的路由使用可以在 umi-examples/routes 和 umi-examples/routes-via-config 里找到示例代码。

umi 会根据 pages 目录自动生成路由配置。

约定式路由

基础路由

假设 pages 目录结构如下:

1
2
3
4
5
+ pages/
+ users/
- index.js
- list.js
- index.js

那么,umi 会自动生成路由配置如下:

1
2
3
4
5
[
{ path: '/', component: './pages/index.js' },
{ path: '/users/', component: './pages/users/index.js' },
{ path: '/users/list', component: './pages/users/list.js' },
]

动态路由

umi 里约定,带 $ 前缀的目录或文件为动态路由。
比如以下目录结构:

1
2
3
4
5
6
7
+ pages/
+ $post/
- index.js
- comments.js
+ users/
$id.js
- index.js

会生成路由配置如下:

1
2
3
4
5
6
[
{ path: '/', component: './pages/index.js' },
{ path: '/users/:id', component: './pages/users/$id.js' },
{ path: '/:post/', component: './pages/$post/index.js' },
{ path: '/:post/comments', component: './pages/$post/comments.js' },
]

可选的动态路由

umi 里约定动态路由如果带 $ 后缀,则为可选动态路由。
比如以下结构:

1
2
3
4
+ pages/
+ users/
- $id$.js
- index.js

会生成路由配置如下:

1
2
3
4
[
{ path: '/': component: './pages/index.js' },
{ path: '/users/:id?': component: './pages/users/$id$.js' },
]

嵌套路由

umi 里约定目录下有 _layout.js 时会生成嵌套路由,以 _layout.js 为该目录的 layout 。
比如以下目录结构:

1
2
3
4
5
+ pages/
+ users/
- _layout.js
- $id.js
- index.js

会生成路由配置如下:

1
2
3
4
5
6
7
8
[
{ path: '/users', component: './pages/users/_layout.js',
routes: [
{ path: '/users/', component: './pages/users/index.js' },
{ path: '/users/:id', component: './pages/users/$id.js' },
],
},
]

全局 layout
约定 src/layouts/index.js 为全局路由,返回一个 React 组件,通过 props.children 渲染子组件。
比如:

1
2
3
4
5
6
7
8
9
export default function(props) {
return (
<>
<Header />
{ props.children }
<Footer />
</>
);
}

不同的全局 layout

你可能需要针对不同路由输出不同的全局 layout,umi 不支持这样的配置,但你仍可以在 layouts/index.js 对 location.path 做区分,渲染不同的 layout 。

比如想要针对 /login 输出简单布局,

1
2
3
4
5
6
7
8
9
10
11
12
13
export default function(props) {
if (props.location.pathname === '/login') {
return <SimpleLayout>{ props.children }</SimpleLayout>
}

return (
<>
<Header />
{ props.children }
<Footer />
</>
);
}

404 路由

约定 pages/404.js 为 404 页面,需返回 React 组件。

比如:

1
2
3
4
5
export default () => {
return (
<div>I am a customized 404 page</div>
);
};

注意:开发模式下,umi 会添加一个默认的 404 页面来辅助开发,但你仍然可通过精确地访问 /404 来验证 404 页面。

通过注释扩展路由

约定路由文件的首个注释如果包含 yaml 格式的配置,则会被用于扩展路由。
比如:

1
2
+ pages/
- index.js

如果 pages/index.js 里包含:

1
2
3
4
5
6
/**
* title: Index Page
* Routes:
* - ./src/routes/a.js
* - ./src/routes/b.js
*/

1
2
3
4
5
6
 [
{ path: '/', component: './index.js',
title: 'Index Page',
Routes: [ './src/routes/a.js', './src/routes/b.js' ],
},
]

配置式路由

如果你倾向于使用配置式的路由,可以配置 routes ,此配置项存在时则不会对 src/pages 目录做约定式的解析。
比如:

1
2
3
4
5
6
7
8
9
10
11
12
export default {
routes: [
{ path: '/', component: './a' },
{ path: '/list', component: './b', Routes: ['./routes/PrivateRoute.js'] },
{ path: '/users', component: './users/_layout',
routes: [
{ path: '/users/detail', component: './users/detail' },
{ path: '/users/:id', component: './users/id' }
]
},
],
};

注意:component 是相对于 src/pages 目录的

权限路由

umi 的权限路由是通过配置路由的 Routes 属性来实现。约定式的通过 yaml 注释添加,配置式的直接配上即可。

比如有以下配置:

1
2
3
4
[
{ path: '/', component: './pages/index.js' },
{ path: '/list', component: './pages/list.js', Routes: ['./routes/PrivateRoute.js'] },
]

然后 umi 会用 ./routes/PrivateRoute.js 来渲染 /list。
./routes/PrivateRoute.js 文件示例:

1
2
3
4
5
6
7
8
export default (props) => {
return (
<div>
<div>PrivateRoute (routes/PrivateRoute.js)</div>
{ props.children }
</div>
);
}

路由动效
路由动效应该是有多种实现方式,这里举 react-transition-group 的例子。
先安装依赖,

1
$ yarn add react-transition-group

在 layout 组件(layouts/index.js 或者 pages 子目录下的 _layout.js)里在渲染子组件时用 TransitionGroup 和 CSSTransition 包裹一层,并以 location.pathname 为 key,

1
2
3
4
5
6
7
8
9
10
11
import withRouter from 'umi/withRouter';
import { TransitionGroup, CSSTransition } from "react-transition-group";

export default withRouter(
({ location }) =>
<TransitionGroup>
<CSSTransition key={location.pathname} classNames="fade" timeout={300}>
{ children }
</CSSTransition>
</TransitionGroup>
)

上面用到的 fade 样式,可以在 src 下的 global.css 里定义:

1
2
3
4
5
6
7
8
9
.fade-enter {
opacity: 0;
z-index: 1;
}

.fade-enter.fade-enter-active {
opacity: 1;
transition: opacity 250ms ease-in;
}

面包屑

面包屑也是有多种实现方式,这里举 react-router-breadcrumbs-hoc 的例子。
先安装依赖,

1
$ yarn add react-router-breadcrumbs-hoc

然后实现一个 Breakcrumbs.js,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import NavLink from 'umi/navlink';
import withBreadcrumbs from 'react-router-breadcrumbs-hoc';

// 更多配置请移步 https://github.com/icd2k3/react-router-breadcrumbs-hoc
const routes = [
{ path: '/', breadcrumb: '首页' },
{ path: '/list', breadcrumb: 'List Page' },
];

export default withBreadcrumbs(routes)(({ breadcrumbs }) => (
<div>
{breadcrumbs.map((breadcrumb, index) => (
<span key={breadcrumb.key}>
<NavLink to={breadcrumb.props.match.url}>
{breadcrumb}
</NavLink>
{(index < breadcrumbs.length - 1) && <i> / </i>}
</span>
))}
</div>
));

然后在需要的地方引入此 React 组件即可。

启用 Hash 路由

umi 默认是用的 Browser History,如果要用 Hash History,需配置:

1
2
3
export default {
history: 'hash',
}

Scroll to top

在 layout 组件(layouts/index.js 或者 pages 子目录下的 _layout.js)的 componentDidUpdate 里决定是否 scroll to top,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { Component } from 'react';
import withRouter from 'umi/withRouter';

class Layout extends Component {
componentDidUpdate(prevProps) {
if (this.props.location !== prevProps.location) {
window.scrollTo(0, 0);
}
}
render() {
return this.props.children;
}
}

export default withRouter(Layout);

在页面间跳转

在 umi 里,页面之间跳转有两种方式:声明式和命令式。

声明式

基于 umi/link,通常作为 React 组件使用。

1
2
3
4
5
import Link from 'umi/link';

export default () => (
<Link to="/list">Go to list page</Link>
);

命令式

基于 umi/router,通常在事件处理中被调用。

1
2
3
4
5
import router from 'umi/router';

function goToListPage() {
router.push('/list');
}

更多命令式的跳转方法,详见 api#umi/router。

配置

配置文件

umi 允许在 .umirc.js 或 config/config.js (二选一,.umirc.js 优先)中进行配置,支持 ES6 语法。

为简化说明,后续文档里只会出现 .umirc.js。

比如:

1
2
3
4
5
6
7
8
9
export default {
base: '/admin/',
publicPath: 'http://cdn.com/foo',
plugins: [
['umi-plugin-react', {
dva: true,
}],
],
};

具体配置项详见配置。

.umirc.local.js

.umirc.local.js 是本地的配置文件,不要提交到 git,所以通常需要配置到 .gitignore。如果存在,会和 .umirc.js 合并后再返回。

UMI_ENV

可以通过环境变量 UMI_ENV 区分不同环境来指定配置。
举个例子,

1
2
3
4
5
6
7
8
// .umirc.js
export default { a: 1, b: 2 };

// .umirc.cloud.js
export default { b: 'cloud', c: 'cloud' };

// .umirc.local.js
export default { c: 'local' };

不指定 UMI_ENV 时,拿到的配置是:

1
2
3
4
5
{
a: 1,
b: 2,
c: 'local',
}

指定 UMI_ENV=cloud 时,拿到的配置是:

1
2
3
4
5
{
a: 1,
b: 'cloud',
c: 'local',
}

HTML 模板

修改默认模板

新建 src/pages/document.ejs,umi 约定如果这个文件存在,会作为默认模板,内容上需要保证有

,比如:
1
2
3
4
5
6
7
8
9
10
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Your App</title>
</head>
<body>
<div id="root"></div>
</body>
</html>

配置模板

模板里可通过 context 来获取到 umi 提供的变量,context 包含:
.route,路由对象,包含 path、component 等
.config,用户配置信息
.publicPath2.1.2+,webpack 的 output.publicPath 配置
.env,环境变量,值为 development 或 production
.其他在路由上通过 context 扩展的配置信息

模板基于 ejs 渲染,可以参考 https://github.com/mde/ejs 查看具体使用。
比如输出变量,

1
<link rel="icon" type="image/x-icon" href="<%= context.publicPath %>favicon.png" />

比如条件判断,

1
2
3
4
5
<% if(context.env === 'production') { %>
<h2>生产环境</h2>
<% } else {%>
<h2>开发环境</h2>
<% } %>

针对特定页面指定模板

WARNING
此功能需开启 exportStatic 配置,否则只会输出一个 html 文件。
TIP

优先级是:路由的 document 属性 > src/pages/document.ejs > umi 内置模板

配置路由的 document 属性。比如约定式路由可通过注释扩展 document 属性,路径从项目根目录开始找,

1
2
3
/**
* document: ./src/documents/404.ejs
*/

然后这个路由就会以 ./src/documents/404.ejs 为模板输出 HTML。

Mock 数据

Mock 数据是前端开发过程中必不可少的一环,是分离前后端开发的关键链路。通过预先跟服务器端约定好的接口,模拟请求数据甚至逻辑,能够让前端开发独立自主,不会被服务端的开发所阻塞。

使用 umi 的 mock 功能

umi 里约定 mock 文件夹下的文件或者 page(s) 文件夹下的 _mock 文件即 mock 文件,文件导出接口定义,支持基于 require 动态分析的实时刷新,支持 ES6 语法,以及友好的出错提示,详情参看 mock-data。

1
2
3
4
5
6
7
8
9
10
export default {
// 支持值为 Object 和 Array
'GET /api/users': { users: [1, 2] },

// GET POST 可省略
'/api/users/1': { id: 1 },

// 支持自定义函数,API 参考 express@4
'POST /api/users/create': (req, res) => { res.end('OK'); },
};

当客户端(浏览器)发送请求,如:GET /api/users,那么本地启动的 umi dev 会跟此配置文件匹配请求路径以及方法,如果匹配到了,就会将请求通过配置处理,就可以像样例一样,你可以直接返回数据,也可以通过函数处理以及重定向到另一个服务器。

引入 Mock.js

Mock.js 是常用的辅助生成模拟数据的第三方库,当然你可以用你喜欢的任意库来结合 roadhog 构建数据模拟功能。

1
2
3
4
5
6
7
8
import mockjs from 'mockjs';

export default {
// 使用 mockjs 等三方库
'GET /api/tags': mockjs.mock({
'list|100': [{ name: '@city', 'value|1-100': 50, 'type|0-2': 1 }],
}),
};

添加跨域请求头
设置 response 的请求头即可:

1
2
3
4
5
'POST /api/users/create': (req, res) => {
...
res.setHeader('Access-Control-Allow-Origin', '*');
...
},

合理的拆分你的 mock 文件

对于整个系统来说,请求接口是复杂并且繁多的,为了处理大量模拟请求的场景,我们通常把每一个数据模型抽象成一个文件,统一放在 mock 的文件夹中,然后他们会自动被引入。

如何模拟延迟

为了更加真实的模拟网络数据请求,往往需要模拟网络延迟时间。

手动添加 setTimeout 模拟延迟

你可以在重写请求的代理方法,在其中添加模拟延迟的处理,如:

1
2
3
4
5
'POST /api/forms': (req, res) => {
setTimeout(() => {
res.send('Ok');
}, 1000);
},

使用插件模拟延迟
上面的方法虽然简便,但是当你需要添加所有的请求延迟的时候,可能就麻烦了,不过可以通过第三方插件来简化这个问题,如:roadhog-api-doc#delay。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { delay } from 'roadhog-api-doc';

const proxy = {
'GET /api/project/notice': getNotice,
'GET /api/activities': getActivities,
'GET /api/rule': getRule,
'GET /api/tags': mockjs.mock({
'list|100': [{ name: '@city', 'value|1-100': 50, 'type|0-2': 1 }]
}),
'GET /api/fake_list': getFakeList,
'GET /api/fake_chart_data': getFakeChartData,
'GET /api/profile/basic': getProfileBasicData,
'GET /api/profile/advanced': getProfileAdvancedData,
'POST /api/register': (req, res) => {
res.send({ status: 'ok' });
},
'GET /api/notices': getNotices,
};

// 调用 delay 函数,统一处理
export default delay(proxy, 1000);

动态 Mock 数据

如果你需要动态生成 Mock 数据,你应该通过函数进行处理,

1
2
3
4
5
// 静态的
'/api/random': Mock.mock({
// 只随机一次
'number|1-100': 100,
}),

1
2
3
4
5
6
7
// 动态的
'/api/random': (req, res) => {
res.send(Mock.mock({
// 每次请求均产生随机值
'number|1-100': 100,
}))
},

联调

当本地开发完毕之后,如果服务器的接口满足之前的约定,那么你只需要不开本地代理或者重定向代理到目标服务器就可以访问真实的服务端数据,非常方便。

Use umi with 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 等

使用

用 yarn 安装依赖,

1
$ yarn add umi-plugin-react

如果你用 npm,执行 npm install –save umi-plugin-react。
然后在 .umirc.js 里配置插件:

1
2
3
4
5
6
7
8
9
10
export default {
plugins: [
[
'umi-plugin-react',
{
dva: true,
},
]
],
};

推荐开启 dva-immer 以简化 reducer 编写,

1
2
3
4
5
6
7
8
9
10
11
12
export default {
plugins: [
[
'umi-plugin-react',
{
dva: {
immer: true
}
}
],
],
};

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
+ 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

配置及插件
之前在 src/dva.js 下进行配置的方式已 deprecated,下个大版本会移除支持。

在 src 目录下新建 app.js,内容如下:

1
2
3
4
5
6
7
8
9
10
11
export const dva = {
config: {
onError(e) {
e.preventDefault();
console.error(e.message);
},
},
plugins: [
require('dva-logger')(),
],
};

FAQ

url 变化了,但页面组件不刷新,是什么原因?
layouts/index.js 里如果用了 connect 传数据,需要用 umi/withRouter 高阶一下。

1
2
3
import withRouter from 'umi/withRouter';

export default withRouter(connect(mapStateToProps)(LayoutComponent));

如何访问到 store 或 dispatch 方法?

1
2
window.g_app._store
window.g_app._store.dispatch

如何禁用包括 component 和 models 的按需加载?
在 .umirc.js 里配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
export default {
plugins: [
[
'umi-plugin-react',
{
dva: {
dynamicImport: undefined // 配置在dva里
},
dynamicImport: undefined // 或者直接写在react插件的根配置,写在这里也会被继承到上面的dva配置里
}
],
],
};

全局 layout 使用 connect 后路由切换后没有刷新?

需用 withRouter 包一下导出的 react 组件,注意顺序。

1
2
import withRouter from 'umi/withRouter';
export default withRouter(connect()(Layout));

按需加载

出于性能的考虑,我们会对模块和组件进行按需加载。

按需加载组件

通过 umi/dynamic 接口实现,比如:

1
2
3
4
5
6
7
8
9
import dynamic from 'umi/dynamic';

const delay = (timeout) => new Promise(resolve => setTimeout(resolve, timeout));
const App = dynamic({
loader: async function() {
await delay(/* 1s */1000);
return () => <div>I will render after 1s</div>;
},
});

按需加载模块

通过 import() 实现,比如:

1
2
3
import('g2').then(() => {
// do something with g2
});

运行时配置

为什么有运行时配置?

我们通过 .umirc.js 做编译时的配置,这能覆盖大量场景,但有一些却是编译时很难触及的。比如:
.在出错时显示个 message 提示用户
.在加载和路由切换时显示个 loading
.页面载入完成时请求后端,根据响应动态修改路由
这些在编译时就很难处理,或者不能处理了。

配置方式

umi 约定 src 目录下的 app.js 为运行时的配置文件。

1
2
3
+ src
- app.js # 运行时配置文件
- package.json

配置列表

patchRoutes

用于运行时修改路由。
参数:
routes: Array,路由配置
e.g. 添加一个 /foo 的路由,

1
2
3
4
5
6
export function patchRoutes(routes) {
routes[0].unshift({
path: '/foo',
component: require('./routes/foo').default,
});
}

可能的场景:
. 和 render 配合使用,请求服务端根据响应动态更新路由,
. 修改全部路由,加上某个前缀

注:
1.同样适用约定式路由
2.直接修改 routes 就好,不要返回新的路由对象

render

用于改写把整个应用 render 到 dom 树里的方法。

参数:
. oldRender, Function,原始 render 方法,需至少被调用一次

1
2
3
export function render(oldRender) {
setTimeout(oldRender, 1000);
}

可能的场景:

渲染应用之前做权限校验,不通过则跳转到登录页

onRouteChange

用于在初始加载和路由切换时做一些事情。

参数:

Object
location:Object, history 提供的 location 对象
routes: Array, 路由配置
action: PUSH|POP|REPLACE|undefined,初次加载时为 undefined

1
2
3
export function onRouteChange({ location, routes, action }) {
bacon(location.pathname);
}

可能的场景:
埋点统计
注:
1.初次加载时也会执行,但 action 为 undefined

rootContainer

用于封装 root container,可以取一部分,或者外面套一层,等等。

参数:

container,ReactComponent,React 应用最上层的根组件
e.g.

1
2
3
4
export function rootContainer(container) {
const DvaContainer = require('@tmp/DvaContainer').default;
return React.createElement(DvaContainer, null, container);
}

可能的场景:
1.dva、intl 等需要在外层有个 Provider 的场景

modifyRouteProps

修改传给路由组件的 props。
参数:
props,Object,原始 props
Object
route,Object,当前路由配置

1
2
3
export function modifyRouteProps(props, { route }) {
return { ...props, foo: 'bar' };
}

注:需返回新的 props

区块

在 umi 中,区块是指页面级别的可复用的代码。umi 定义了一个区块的规范,你可以通过 umi 能够快速简单的在你的项目中添加区块,用于快速的开始一个页面的开发。

使用区块

在项目根目录使用如下命令可以添加一个区块到到你的项目中:

1
$ umi block add [block url]

其中 [block url] 可以是一个 Github 或者 Gitlab 地址,也可以是一个 Git 仓库地址,也可以是一个本地相对或者绝对路径。只要对应的路径下是一个区块的代码,满足 umi 区块的规范,那么 umi 就可以通过该命令将区块的代码下载到你的项目中。
比如,你可以运行

1
$ umi block add https://github.com/umijs/umi-blocks/tree/master/blank

来将官方的区块仓库中的 blank 区块下载到你的项目本地。对于官方区块仓库下的区块你可以使用更加简洁的命令,比如 umi block add blank 来下载区块。

如果你的项目正在本地调试,那么区块下载到项目中后你就可以访问相应的路径来查看效果了。区块代码会被默认下载到 pages/[name] 下面,其中 name 是默认取区块中的 package.json 中的 name字段(会去掉/前的无效片段)。对于配置式路由,我们也会默认添加路由配置到你的配置中,所以也一样可以直接访问。

你可以通过 umi help block 来查看支持的更多配置。

需要注意的是,区块只是用于开发时新建页面时的提效工具,一般来说区块要实际应用都需要针对项目需求去修改最后的代码,之后的维护都将和普通页面一样由开发者来维护,不存在区块更新的说法。

区块开发

初始化区块

你可以通过 create-umi 快速创建一个区块的模板:

1
$ yarn create umi --block

区块的目录结构如下:

1
2
3
4
5
6
7
8
- root
- src // 区块的代码
- index.js // 区块入口,需要默认导出一个 React 组件
- _mock.js // 约定的 mock 文件
- @ // 区块依赖的一些需要放到项目 src 下的内容(通常不推荐采用)
- package.json // 区块依赖等信息
- .umirc.js // 基于 umi 开发区块时的配置
- thumb.[png|jpg] // 物料的缩略图

其中 package.json 文件相关内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
name: '@umi-blocks/blank',
description: '区块描述',
// ... 更多其他 npm 包的相关定义
dependencies: {
// dependencies 里面是区块运行时阶段的依赖,比如 antd g2 这些包的依赖
antd: '^3.0.0',
},
devDependencies: {
// 用户调试区块时候的依赖,和区块没有直接关系,可以提供基于 umi 的开发方案
},
scripts: {
// 开发区块调试时的命令,和区块没有直接关系
}
}

区块添加逻辑

当执行 umi block add [block url] 的时候实际上是执行的如下步骤:

通过 git clone 下载区块代码(如果已经存在则会通过 git pull 更新)
检测区块的 package.json 中的依赖并自动安装到项目中
将区块代码复制到对应的页面目录,复制过程中会做一些宏替换
如果是配置式路由,那么会自动添加路由
另外,如果在项目中配置了 singular 为 true,那么这个处理过程也会将对应的复数目录改为单数。

宏替换

为了避免区块添加到应用中出现冲突,umi 提供了一些宏,当区块被添加到项目中时,区块代码中的宏也会按照区块对应的信息被替换。通过这个能力可以避免诸如 dva model 的 namespace 冲突等问题。

具体的宏如下,基于 –path=/Test_Hello/hello-Block 示例。

ROUTE_PATH /test_hello/hello-block
BLOCK_NAME test_hello-hello-block
PAGE_NAME hello-block
PAGE_NAME_UPPER_CAMEL_CASE HelloBlock
BLOCK_NAME_CAMEL_CASE testHelloHelloBlock

区块调试

区块调试基于 umi-plugin-block-dev 这个插件,基于该插件你就可以把区块当做一个普通的 umi 项目来调试了。如下所示,在区块的根目录下添加 .umirc.js 文件(通过 create-umi 创建的区块脚手架会自带该文件)。
export default {
plugins: [
[‘umi-plugin-block-dev’, {
layout: ‘ant-design-pro’,
}],
],
}
该插件会将区块的 src 目录作为 umi 的 pages 目录,这样你就可以在区块根目录下通过 umi dev 来开发调试区块了。

如果你觉得的区块质量较好,你可以通过提交 PR 来把它添加到 官方区块仓库 中。

部署

默认方案
umi@2 默认对新手友好,所以默认不做按需加载处理,umi build 后输出 index.html、umi.js 和 umi.css 三个文件。

不输出 html 文件

某些场景 html 文件交给后端输出,前端构建并不需要输出 html 文件,可配置环境变量 HTML=none 实现。

1
$ HTML=none umi build

部署 html 到非根目录

经常有同学问这个问题:

为什么我本地开发是好的,部署后就没反应了,而且没有报错?

没有报错! 这是应用部署在非根路径的典型现象。为啥会有这个问题?因为路由没有匹配上,比如你把应用部署在 /xxx/ 下,然后访问 /xxx/hello,而代码里匹配的是 /hello,那就匹配不上了,而又没有定义 fallback 的路由,比如 404,那就会显示空白页。
怎么解决?可通过配置 base 解决。

1
2
3
export default {
base: '/path/to/your/app/root',
};

使用 hashHistory
可通过配置 history 为 hash 为解决。

1
2
3
export default {
history: 'hash',
};

按需加载
要实现按需加载,需装载 umi-plugin-react 插件并配置 dynamicImport。

1
2
3
4
5
6
7
export default {
plugins: [
['umi-plugin-react', {
dynamicImport: true,
}],
],
};

参数详见:umi-plugin-react#dynamicImport。

静态资源在非根目录或 cdn

这时,就需要配置 publicPath。至于 publicPath 是啥?具体看 webpack 文档,把他指向静态资源(js、css、图片、字体等)所在的路径。

1
2
3
export default {
publicPath: "http://yourcdn/path/to/static/"
}

使用 runtime 的 publicPath
对于需要在 html 里管理 publicPath 的场景,比如在 html 里判断环境做不同的输出,可通过配置 runtimePublicPath 为解决。

1
2
3
export default {
runtimePublicPath: true,
};

然后在 html 里输出:

1
2
3
<script>
window.publicPath = <%= YOUR PUBLIC_PATH %>
</script>

静态化
在一些场景中,无法做服务端的 html fallback,即让每个路由都输出 index.html 的内容,那么就要做静态化。
比如上面的例子,我们在 .umirc.js 里配置:

1
2
3
export default {
exportStatic: {},
}

然后执行 umi build,会为每个路由输出一个 html 文件。

1
2
3
4
5
6
7
8
9
./dist
├── index.html
├── list
│ └── index.html
└── static
├── pages__index.5c0f5f51.async.js
├── pages__list.f940b099.async.js
├── umi.2eaebd79.js
└── umi.f4cb51da.css

注意:静态化暂不支持有变量路由的场景。

HTML 后缀

有些静态化的场景里,是不会自动读索引文件的,比如支付宝的容器环境,那么就不能生成这种 html 文件,

1
2
3
├── index.html
├── list
│ └── index.html

而是

1
2
├── index.html
└── list.html

配置方式是在 .umirc.js 里,

1
2
3
4
5
export default {
exportStatic: {
htmlSuffix: true,
},
}

umi build 会生成,

1
2
3
4
5
6
7
8
./dist
├── index.html
├── list.html
└── static
├── pages__index.5c0f5f51.async.js
├── pages__list.f940b099.async.js
├── umi.2924fdb7.js
└── umi.cfe3ffab.css

静态化后输出到任意路径

1
2
3
4
5
6
export default {
exportStatic: {
htmlSuffix: true,
dynamicRoot: true,
},
}
0%