Hello, umi

什么是 umi

umi 是由 dva 的开发者 云谦 编写的一个新的 React 开发框架。umi 既是一个框架也是一个工具,你可以将它简单的理解为一个专注性能的类 next.js 前端框架,并通过约定、自动生成和解析代码等方式来辅助开发,减少开发者的代码量。

umi 是通用方案,适用于现在几乎所有的 web 环境。

umi 的优势

umi 是一个专注性能的类 next.js 端框架,它的优势是:

  • 内置大量的性能优化
  • 多端,无缝支持容器和浏览器访问
  • 类 webpack 的插件机制
  • 针对 antd 和 dva 有友好的支持

umi 最显著的特点就是「文件即路由」——在 pages 文件夹下新建文件,umi 将自动生成与文件路径对应的路由。在大部分其他前端框架中,路由配置一直是一个很麻烦的事情,而对于多人协作开发的项目,公共的配置文件则可能面临着更多的冲突。

umi 的可扩展性

作者称“umi 有着类 webpack 般灵活的插件机制,他就是一个架子”。 主要的 umi 项目不到 700 行代码,umi 负责搭好骨架,把框架的生命周期钩子暴露出来,然后通过插件来丰富功能。

你可以用高达玩具类比 umi 的可扩展性:刚入手的玩家可以根据说明书,一步一步地组装出自己心爱的玩具;对于高玩来说,官方提供了一个骨架,保证了高达的可动性,然后你自己可以随意 DIY、任意地使用材料和设计方式。

刚接触前端的同学可以很好的完成公司的业务需求;对前端有一定了解的同学可以随意地修改,包括配置、编译、开发、模板、请求方式、数据流等等,几乎所有能想到的前端工程化的内容,都允许自定义。在一步步接触这些可配置项的时候,你也会一步步对前端工程化更多的认识和理解。

umi 的性能

在项目性能方面 umi 已经帮你做了很多优化,包括构建产物的大小、执行效率、首屏加载、用户体验等方面,但这些优化对于开发者是无感知的,有时候你升级了一下插件版本,整个项目可能就跟着优化了,而不需要你进行其他调整。作者称“你只管写业务代码,我会负责性能,并且随着 umi 的迭代,我保证你的应用会越来越快”。

简单的说,umi 做到了开箱即用,对于开发者和前端初学者是非常友好的。

Why umi

不知道你在开发中有没有遇到这些问题:

  • 我希望我的项目既可以跑在支付宝(淘宝)容器里(多页),又可以跑在普通浏览器里(单页),有啥办法吗?
  • 随着项目越来越大,开发调试的启动和热更新时间越来越长。。
  • 我所有的文件都打包在一起发布了,用户反馈说网站打开很慢,有没有办法基于路由做按需加载?
  • 连 iOS 都支持 PWA 了,我能否一键开启让我的项目更快?
  • 据说 preact 又小又快,我如何一键开启?
  • 开发完之后部署又遇到问题,publicPath 和 basename 是啥?又如何解决?
  • 我要部署到静态服务器或 cdn 上,能否帮我把 HTML 也生成出来,部署后就能跑?
  • antd{,-mobile} 还要配 babel-plugin-import?那个 es 文件夹又是啥?
  • ts、jest、babel 的配置好麻烦,而且配了这个又和另一个冲突,怎么办?
  • 据说 webpack 的 tree-shake、scope-hoist、side effect 等能进一步减少文件大小,我如何最大化地利用?
  • dva 的 model 一个个手写载入好麻烦,还有 dva@2 之后 history 的 query 怎么没有了?

以上的问题来自 umi 作者平时开发中收录。 如果你遇到了同样或者类似的疑问和烦恼,不妨试试 umi 吧。

快速上手

环境准备

在开始之前,请确保你的开发环境已经安装了 Node.js,umi 需要 Node.js 10.13 版本以上:

  • 检测 Node.js 版本,可以在终端/控制台窗口中运行 node -v 命令。

第一步 安装Umi

后续需要使用 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

第二步 新建一个简单的 umi 项目

在你的工作空间或者任意目录新建一个名为 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 页面:
在这里插入图片描述

第四步 编辑页面

  1. 打开 pages/home.js
  2. 编辑文件
<div className={styles.normal}>
- <h1>Page home</h1>
+ <h1>Welcome to Umi</h1>
</div>
  1. 保存文件,回到浏览器查看修改后的页面:
    在这里插入图片描述
    umi 默认开启热更新功能,修改文件并保存之后就能直接在页面中看到变化。

使用CLI创建初始化项目

第一步:使用 yarn 初始化项目

$ yarn global add @umijs/create-umi-app
...
success Installed "@umijs/create-umi-app@3.0.16" with binaries:
      - create-umi-app
✨  Done in 36.07s.

@umijs/create-umi-app 主要是用来使用命令行创建 umi 相关的库或者项目。命令中打印 success 说明安装成功,如果你还需要进一步确认,可以执行 create-umi-app -v 来查看 @umijs/create-umi-app 的版本号。

第二步:使用 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/
src/.umi/
/.umi-production/
src/.umi-production/
src/**/*.test.js & *.e2e.js
src/**/*.test.js & *.e2e.js

src/global.(j|t)sx
src/global.(j|t)sx
src/global.(css|less|sass|scss)
src/global.(css|less|sass|scss)
.umirc.js 和 config/config.js
.umirc.js 和 config/config.js
.env
.env

页面跳转

Umi提供了两种跳转方式
命令式:history.push('/list')
声明式:<Link to="/list">Go to list page</Link>

声明式跳转
声明式跳转
命令式跳转
命令式跳转

使用dva models

在这里插入图片描述
教程在前面使用 create-umi-app 初始化项目时,依赖了 @umijs/preset-react ,这是一个插件集,你无需再而外安装 plugin-dva ,只需要再配置中开启即可。打开 umi 的配置文件:

./umirc.js

import { defineConfig } from 'umi';

export default defineConfig({
  dva: {},
  antd: {}
});

新增 model 文件

新增 model 文件
新建 ./src/models/hero.ts

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 文件,文件名不同。

在页面中使用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 };
    },
  },

http 请求 umi-request

在这里插入图片描述

新建 src/app.ts

umi的运行时配置,都在 src/app.ts 中,我们在这里配置 umi-request 的配置。

export const request = {
  prefix: 'https://pvp.qq.com',
};

这里我们配置了所有请求的 prefix

在 model 中发起请求

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 请求,虽然他没有正确响应,页面中我们也没有取得网络上的数据,但是,它确实是发起了,如果请求的接口不存在跨域问题的话,那么这里就能取到数据了。

proxy 请求代理

在这里插入图片描述
之所以会出现跨域访问问题,是因为浏览器的安全策略。所以我们预想是不是有一种方式,能够绕过浏览器的安全策略?

那就是先请求一个同源服务器,再由服务器去请求其他的服务器。比如:
• 我们本来是要请求 https://pvp.qq.com 服务器,但是它存在跨域。
• 所以我们先请求了 http://localhost:3000 (假设的),它不存在跨域问题,所以它受理了我们的请求,并且我们可以取得它返回的数据。
• 而由 http://localhost:3000 返回的数据,又是从真实的 https://pvp.qq.com 获取来的,因为服务端不是在浏览器环境,所以就没有浏览器的安全策略问题。
• 因为 http://localhost:3000 (假设的)这个服务器,它只是把我们请求的参数,转发到真实服务端,又把真实服务端下发的数据,转发给我们,所以我们称它为代理。

umi 提供了 proxy 来处理这个问题。

配置 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

这里指的是后续和服务端对接的时候,如何优雅的设置 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

Mock 数据

预想的问题

  1. 在实际项目开发中,我们经常会遇到和服务端同步开发的情况。
    这时候我们就可以要求服务端先输出接口文档,前后端都根据接口文档输出。
    在项目交付之前一段时间,再进行前后端连调。
  2. 在demo开发学习的过程中,我们也常常遇到需要静态数据的情况
  3. 公司网络限制了只能访问部分外网,是的,墙里墙,还是满多的
  4. 没有网络的情况,呵呵,想到咖啡店敲个代码,悠哉悠哉,没有公用Wi-Fi,接口都挂了,页面渲染不出来

umi 中使用 mock

在 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请求携带参数

比如我们需要取得单个英雄的数据,我们就需要在请求里面携带参数。
./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,但是,到后端取不到数据。

mock post 取不到参数

这里需要注意,我们需要为请求增加请求头,还有 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,
        }),
      });
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐