umi 是由 dva 的开发者 云谦 编写的一个新的 React 开发框架。umi 既是一个框架也是一个工具,你可以将它简单的理解为一个专注性能的类 next.js 前端框架,并通过约定、自动生成和解析代码等方式来辅助开发,减少开发者的代码量。
umi 是通用方案,适用于现在几乎所有的 web 环境。
umi 是一个专注性能的类 next.js 端框架,它的优势是:
umi 最显著的特点就是「文件即路由」——在 pages 文件夹下新建文件,umi 将自动生成与文件路径对应的路由。在大部分其他前端框架中,路由配置一直是一个很麻烦的事情,而对于多人协作开发的项目,公共的配置文件则可能面临着更多的冲突。
作者称“umi 有着类 webpack 般灵活的插件机制,他就是一个架子”。 主要的 umi 项目不到 700 行代码,umi 负责搭好骨架,把框架的生命周期钩子暴露出来,然后通过插件来丰富功能。
你可以用高达玩具类比 umi 的可扩展性:刚入手的玩家可以根据说明书,一步一步地组装出自己心爱的玩具;对于高玩来说,官方提供了一个骨架,保证了高达的可动性,然后你自己可以随意 DIY、任意地使用材料和设计方式。
刚接触前端的同学可以很好的完成公司的业务需求;对前端有一定了解的同学可以随意地修改,包括配置、编译、开发、模板、请求方式、数据流等等,几乎所有能想到的前端工程化的内容,都允许自定义。在一步步接触这些可配置项的时候,你也会一步步对前端工程化更多的认识和理解。
在项目性能方面 umi 已经帮你做了很多优化,包括构建产物的大小、执行效率、首屏加载、用户体验等方面,但这些优化对于开发者是无感知的,有时候你升级了一下插件版本,整个项目可能就跟着优化了,而不需要你进行其他调整。作者称“你只管写业务代码,我会负责性能,并且随着 umi 的迭代,我保证你的应用会越来越快”。
简单的说,umi 做到了开箱即用,对于开发者和前端初学者是非常友好的。
不知道你在开发中有没有遇到这些问题:
以上的问题来自 umi 作者平时开发中收录。 如果你遇到了同样或者类似的疑问和烦恼,不妨试试 umi 吧。
在开始之前,请确保你的开发环境已经安装了 Node.js,umi 需要 Node.js 10.13 版本以上:
node -v
命令。后续需要使用 umi 来创建页面 umi g
,并执行多种任务,比如测试 umi test
、打包 umi build
和开发 umi dev
等。为了能直接在命令行中运行这些命令,你需要打开终端/控制台窗口,输入以下命令来全局安装 umi :
npm install -g umi
推荐使用 yarn 代替 npm 来安装 umi , yarn 会针对部分场景做一些缓存以节省时间,你可以输入以下命令来全局安装 yarn :
npm install -g yarn
命令行执行结束后,判断 yarn 是否安装成功:
$ yarn -v
1.9.4
然后使用 yarn 安装 umi :
$ yarn global add umi
命令行执行结束后,判断 umi 是否安装成功:
$ umi -v
3.0.16
在你的工作空间或者任意目录新建一个名为 my-app 的文件夹:
$ mkdir my-app
在webstrom或vscode终端通过 umi g page 来创建页面:
$ umi g page home
create pages/home.js
create pages/home.css
✔ success
现在,你应该已经得到了以下的目录结构:
└── pages
├── home.css
├── home.js
└── list.js
这里的 pages 目录是页面所在的目录,umi 约定默认情况下 pages 下所有的 js 文件即路由
$ umi dev
Compiling
✔ success webpack compiled in 3s 49ms
DONE Compiled successfully in 3056ms 22:37:57
App running at:
- Local: http://localhost:8000/ (copied to clipboard)
- Network: http://192.168.199.195:8000/
umi 在启动完成后将自动打开浏览器,并访问 http://localhost:8000/ ,你将看到以下页面:
这是开发环境下的 404 页面,因为目前并没有在 pages 下面创建 index.js 。不过没有关系,我们可以通过访问 http://localhost:8000/home 来访问我们创建的 home 页面:
<div className={styles.normal}>
- <h1>Page home</h1>
+ <h1>Welcome to Umi</h1>
</div>
$ yarn global add @umijs/create-umi-app
...
success Installed "@umijs/[email protected]" with binaries:
- create-umi-app
✨ Done in 36.07s.
@umijs/create-umi-app 主要是用来使用命令行创建 umi 相关的库或者项目。命令中打印 success 说明安装成功,如果你还需要进一步确认,可以执行 create-umi-app -v
来查看 @umijs/create-umi-app 的版本号。
先找个地方建个空目录
$ mkdir myapp && cd myapp
$ create-umi-app
Copy: .editorconfig
Write: .gitignore
Copy: .prettierignore
Copy: .prettierrc
Write: .umirc.ts
Copy: mock/.gitkeep
Write: package.json
Copy: README.md
Copy: src/pages/index.less
Copy: src/pages/index.tsx
Copy: tsconfig.json
Copy: typings.d.ts
如果你的命令行打印的日志如上,说明新建项目完成了,如果有其他的错误,可以确认一下当前目录下是否为空。
以上两部也可以合并成一步,在一个空文件夹下面,执行 yarn create @umijs/umi-app
$ yarn
...这个过程需要一些时间
success Saved lockfile.
✨ Done in 170.43s.
看到命令行打印 success,一般就是安装成功了,但是有时候因为一些网络问题,会出现丢包的情况,需要你重新运行 yarn 验证是否全部安装成功。
$ yarn start
$ umi dev
Starting the development server...
✔ Webpack
Compiled successfully in 7.21s
DONE Compiled successfully in 7216ms 14:51:34
App running at:
- Local: http://localhost:8000 (copied to clipboard)
- Network: http://192.168.10.6:8000
你可以通过浏览器访问 http://localhost:8000/ 来查看页面:
src/.umi/
/.umi-production/
src/**/*.test.js & *.e2e.js
src/global.(j|t)sx
src/global.(css|less|sass|scss)
.umirc.js 和 config/config.js
.env
Umi提供了两种跳转方式
命令式:history.push('/list')
声明式:Go to list page
教程在前面使用 create-umi-app
初始化项目时,依赖了 @umijs/preset-react ,这是一个插件集,你无需再而外安装 plugin-dva ,只需要再配置中开启即可。打开 umi 的配置文件:
import { defineConfig } from 'umi';
export default defineConfig({
dva: {},
antd: {}
});
import { Effect, Reducer } from 'umi';
export interface HeroModelState {
name: string;
}
export interface HeroModelType {
namespace: 'hero';
state: HeroModelState;
effects: {
query: Effect;
};
reducers: {
save: Reducer<HeroModelState>;
};
}
const HeroModel: HeroModelType = {
namespace: 'hero',
state: {
name: 'hero',
},
effects: {
*query({ payload }, { call, put }) {
},
},
reducers: {
save(state, action) {
return {
...state,
...action.payload,
};
},
},
};
export default HeroModel;
关于这个文件的详细说明,可以查看导读的《五分钟掌握最小知识体系》。这里需要说明的是,如果文件中的 namespace 未写明,umi 会使用文件名作为 model 的 namespace。为了减少错误的出现,最好保持所有的 model 文件,文件名不同。
在这里我们需要引入 dva 的 connect 将页面和 model 绑定在一起,我们稍微改造一下页面的结构:
./src/pages/hero.tsx
import React, { FC } from 'react';
import styles from './hero.css';
import { connect, HeroModelState, ConnectProps } from 'umi'; ---step1
interface PageProps extends ConnectProps {
hero: HeroModelState;
}
const Hero: FC<PageProps> = (props) => { ---step2
console.log(props.hero); ---step4
return (
<div>
<h1 className={styles.title}>Page hero</h1>
<h2>This is {props.hero.name}</h2>
</div>
);
}
export default connect(({ hero }: { hero: HeroModelState }) => ({ hero }))(Hero);
--- step3
这里有一个很常见的需求,我们需要在进入页面的时候,发起请求页面初始化数据。这里我们通过 dva model 的 subscriptions 实现。
src/models/hero.ts subscriptions
import { Effect, Reducer, Subscription } from 'umi';
export interface HeroModelType {
namespace: 'hero';
state: HeroModelState;
effects: {
query: Effect;
};
reducers: {
save: Reducer<HeroModelState>;
};
+ subscriptions: { setup: Subscription };
}
HeroModel 中增加 subscriptions
subscriptions: {
setup({ dispatch, history }) {
return history.listen(({ pathname, query }) => {
if (pathname === '/hero') {
dispatch({
type: 'fetch'
})
}
});
}
},
这里需要注意的是,subscriptions 是一个全局的监听,就是说,当设定触发条件满足时,所有的 subscriptions 都会响应,所以我们在这里判断了路由为当前路由时,发起一个 effects 事件。
src/models/hero.ts effects
然后在 effects 里面,响应这个事件。
effects: {
*fetch({ type, payload }, { put, call, select }) {
const data = [
{
ename: 105,
cname: '廉颇',
title: '正义爆轰',
new_type: 0,
hero_type: 3,
skin_name: '正义爆轰|地狱岩魂',
},
{
ename: 106,
cname: '小乔',
title: '恋之微风',
new_type: 0,
hero_type: 2,
skin_name: '恋之微风|万圣前夜|天鹅之梦|纯白花嫁|缤纷独角兽',
},
];
yield put({
type: 'save',
payload: {
heros: data,
},
});
},
},
这里的 *fetch 前面的 * 表示它是一个异步函数,你可以在里面使用 yield 进行等待操作(什么是 Effect ?)。这里的 put 方法和 dispatch 方法可以理解为同一个方法,只是在不同的地方,用不同的方法名表示而已。这里我们写了一个静态数据,然后又发起了一个叫做 save 的事件。
别忘了在类型定义里面增加属性定义哦
export interface HeroModelType {
namespace: 'hero';
state: HeroModelState;
effects: {
query: Effect;
fetch: Effect;
};
reducers: {
save: Reducer<HeroModelState>;
};
subscriptions: { setup: Subscription };
}
src/models/hero.js reducers
最终我们在 reducers 中响应了这个 save 事件,用于更新页面数据,触发页面更新。
reducers: {
save(state, action) {
return { ...state, ...action.payload };
},
},
umi的运行时配置,都在 src/app.ts 中,我们在这里配置 umi-request 的配置。
export const request = {
prefix: 'https://pvp.qq.com',
};
这里我们配置了所有请求的 prefix
src/models/hero.ts
import { Effect, Reducer, Subscription,request } from 'umi';
...
*fetch({ type, payload }, { put, call, select }) {
const data = yield request('/web201605/js/herolist.json');
const localData = [
{
ename: 105,
cname: '廉颇',
title: '正义爆轰',
new_type: 0,
hero_type: 3,
skin_name: '正义爆轰|地狱岩魂',
},
{
ename: 106,
cname: '小乔',
title: '恋之微风',
new_type: 0,
hero_type: 2,
skin_name: '恋之微风|万圣前夜|天鹅之梦|纯白花嫁|缤纷独角兽',
},
];
yield put({
type: 'save',
payload: {
heros: data||localData,
},
});
},
这时候我们发现页面中并没有取得数据,在我们的代码逻辑中,就算取不到网络数据,也会使用本地数据。这时候我们打开控制台,查看一下网络请求情况。
src/app.ts
+ import { ResponseError } from 'umi-request';
export const request = {
prefix: '',
+ errorHandler: (error: ResponseError) => {
+ // 集中处理错误
+ console.log(error);
+ },
};
到这里,我们已经正确发起了一个 http 请求,虽然他没有正确响应,页面中我们也没有取得网络上的数据,但是,它确实是发起了,如果请求的接口不存在跨域问题的话,那么这里就能取到数据了。
之所以会出现跨域访问问题,是因为浏览器的安全策略。所以我们预想是不是有一种方式,能够绕过浏览器的安全策略?
那就是先请求一个同源服务器,再由服务器去请求其他的服务器。比如:
• 我们本来是要请求 https://pvp.qq.com 服务器,但是它存在跨域。
• 所以我们先请求了 http://localhost:3000 (假设的),它不存在跨域问题,所以它受理了我们的请求,并且我们可以取得它返回的数据。
• 而由 http://localhost:3000 返回的数据,又是从真实的 https://pvp.qq.com 获取来的,因为服务端不是在浏览器环境,所以就没有浏览器的安全策略问题。
• 因为 http://localhost:3000 (假设的)这个服务器,它只是把我们请求的参数,转发到真实服务端,又把真实服务端下发的数据,转发给我们,所以我们称它为代理。
umi 提供了 proxy 来处理这个问题。
要在 umi 中使用 proxy 非常简单,只要在配置文件中配置就可以了。
./.umirc.js
export default {
plugins: [
...
],
"proxy": {
"/api": { ---step1
"target": "https://pvp.qq.com", ---step2
"changeOrigin": true, ---step3
"pathRewrite": { "^/api" : "" } ---step4
}
}
}
// 注意层级,proxy在最外层,不要写到插件plugins里面
• step1 设置了需要代理的请求头,比如这里定义了 /api ,当你访问如 /api/abc 这样子的请求,就会触发代理
• step2 设置代理的目标,即真实的服务器地址
• changeOrigin 设置是否跨域请求资源
• pathRewrite 表示是否重写请求地址,比如这里的配置,就是把 /api 替换成空字符
src/app.ts
import { ResponseError } from 'umi-request';
export const request = {
- prefix: 'https://pvp.qq.com',
+ prefix: '/api',
errorHandler: (error: ResponseError) => {
// 集中处理错误
console.log(error);
},
};
修改请求地址,前缀改成 /api ,其实通过代理,最后真实访问的地址还是 https://pvp.qq.com/web201605/js/herolist.json
我们打开控制台,可以看到我们的请求地址是
http://localhost:8000/api/web201605/js/herolist.json ,响应200,并返回了真实数据。
你不会在浏览器的控制台中查看到我们真实代理的地址,这里需要注意,代理只是将请求服务做了中转,设置proxy不会修改请求地址。
这里指的是后续和服务端对接的时候,如何优雅的设置 proxy 。
step1
向服务端要一个,不需要授权,不需要登录,get请求的接口。
如:https://api.douban.com/v2/movie/in_theaters
step2
将服务端给我们的地址,直接在浏览器中访问,如果能正确返回数据,那说明服务端给的接口没有问题。
step3
查看接口文档,设置正确的代理前缀。
比如接口是 /v1/abc /v2/sss /v2/xxx 这样的,那target应该是 https://api.douban.com/
比如接口是 /v2/abc /v2/sss /v2/xxx 这样的,那target应该是 https://api.douban.com/v2/
step 4
设置 proxy
比如接口是 /v1/abc /v2/sss /v2/xxx 这样的,那target应该是 https://api.douban.com/
"proxy": {
"/api": {
"target": "https://api.douban.com/",
"changeOrigin": true,
"pathRewrite": { "^/api" : "" }
}
}
请求地址 /api/v1/abc /api/v2/sss /api/v2/xxx
比如接口是 /v2/abc /v2/sss /v2/xxx 这样的,那target应该是 https://api.douban.com/v2/
"proxy": {
"/api": {
"target": "https://api.douban.com/v2/",
"changeOrigin": true,
"pathRewrite": { "^/api" : "" }
}
}
请求地址 /api/abc /api/sss /api/xxx
在 umi 中使用 mock 还是蛮简单的,任何配置都不需要,只要在 ./mock/ 文件夹下,新建 js 文件,然后按照规范编写文档,就可以使用 mock 功能了。
在page下,定义_mock.js也可以使用mock功能。如./src/pages/index/_mock.js
这里有一个最简单的实现:
mock/heros.ts
export default {
'/api/web201605/js/herolist.json': [
{
ename: 106,
cname: '小乔',
title: '恋之微风',
new_type: 0,
hero_type: 2,
skin_name: '恋之微风|万圣前夜|天鹅之梦|纯白花嫁|缤纷独角兽',
},
],
};
只要请求路径匹配,那么就会直接返回数组。我们先关闭 .umirc.js 中的 proxy 配置,先注释掉就好。
// "proxy": {
// "/api": {
// "target": "https://pvp.qq.com",
// "changeOrigin": true,
// "pathRewrite": { "^/api" : "" }
// }
// }
我们把 https://pvp.qq.com/web201605/js/herolist.json 请求的数据,保存下来,作为我们的本地数据,存放到 ./mock/herolist.json 其实你可以放到任意的地方,放在这里面也不会被解析成mock服务,只是就近放在一起而已,因为这个数据很大,放到 ./mock/heros.js 里面的话会让文件变得很长,不利于我们看代码。
然后在 ./mock/heros.js 中引入herolist.json ,修改一下请求的返回值,这样我们的mock数据就和官方接口返回值保持一致啦。
import herolist from './herolist.json';
export default {
'/api/herolist.json': herolist
};
在开发过程中,服务端启用临时服务器,返回出参,可以通过这种不规范不推荐的方式保留下来。在服务器不可以的情况下,我们还可以进行前端的开发工作
比如我们需要取得单个英雄的数据,我们就需要在请求里面携带参数。
./mock/heros.ts
'POST /api/herodetails.json': (req, res) => {
const { ename } = req.body;
const hero = herolist.filter(item => item.ename === parseInt(ename, 10))[0];
res.send(hero);
},
定义了这个请求是 post 请求,并从请求参数中取出来 ename 对原数组做了过滤。
然后在 ./src/models/hero.ts 中修改请求。
const data = yield request('/api/herodetails.json', {
method: 'POST',
body: {
ename: 110,
},
});
请求携带了参数 body,但是,到后端取不到数据。
这里需要注意,我们需要为请求增加请求头,还有 body ,需要转成字符串。
最后我们发起的请求就是
const data = yield request('/api/herodetails.json', {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json; charset=utf-8',
},
body: JSON.stringify({
ename: 110,
}),
});