简述

Umi,中文可发音为乌米,是可扩展的企业级前端应用框架。Umi 以路由为基础的,同时支持配置式路由和约定式路由,保证路由的功能完备,并以此进行功能扩展。然后配以生命周期完善的插件体系,覆盖从源码到构建产物的每个生命周期,支持各种功能扩展和业务需求。

  • UmiJS 是一个类 Next.js 的react开发框架。
  • 他基于一个约定,即 pages 目录下的文件即路由,而文件则导出 react 组件。
  • 然后打通从源码到产物的每个阶段,并配以完善的插件体系,让我们能把 umi 的产物部署到各种场景里。

68747470733a2f2f7368697075736572636f6e74656e742e636f6d2f33616138333237306131363061333263313434366263346134323366613330332f506173746564253230496d616765253230322e706e67.png

快速上手

  1. mkdir myapp && cd myapp
  2. # 通过官方工具创建项目
  3. yarn create @umijs/umi-app # 或 npx @umijs/create-umi-app
  4. yarn install # 安装所有依赖
  5. yarn start # 启动
  6. yarn build # 打包。构建产物默认生成到 ./dist 下,然后通过 “tree ./dist” 命令查看,
  7. # 本地验证,类似于使用 http-server ./dist -p 5000
  8. yarn global add server
  9. serve ./dist

目录约定
  1. .
  2. ├── dist/ // 默认的 build 输出目录
  3. ├── mock/ // mock 文件所在目录,基于 express
  4. ├── public/ // 此目录下所有文件会被 copy 到输出路径。
  5. ├── config/
  6. ├── config.js // umi 配置,同 .umirc.js,二选一
  7. └── src/ // 源码目录,可选
  8. ├── layouts/index.js // 全局布局
  9. ├── pages/ // 页面目录,里面的文件即路由
  10. ├── .umi/ // dev 临时目录,需添加到 .gitignore
  11. ├── .umi-production/ // build 临时目录,会自动删除
  12. ├── document.ejs // HTML 模板
  13. ├── 404.js // 404 页面
  14. ├── page1.js // 页面 1,任意命名,导出 react 组件
  15. ├── page1.test.js // 用例文件,umi test 会匹配所有 .test.js 和 .e2e.js 结尾的文件
  16. └── page2.js // 页面 2,任意命名
  17. ├── global.css // 约定的全局样式文件,自动引入,也可以用 global.less
  18. ├── global.js // 可以在这里加入 polyfill
  19. ├── .umirc.js // umi 配置,同 config/config.js,二选一
  20. ├── .env // 环境变量
  21. └── package.json

命令行工具

  • https://umijs.org/zh-CN/docs/cli
    1. "scripts": {
    2. "dev": "umi dev", // 本地启动
    3. "build": "umi build" // 打包
    4. },
    ```shell

    查看当前使用的 umi 的版本号,可以使用别名 -v 调用。

    $ umi version$ umi -v

查看帮助

$ umi help $ umi help

查看 webpack 的配置

$ umi webpack $ umi webpack [options]

默认会打印 development 的配置,如需查看 production 配置,需要指定环境变量:

$ NODE_ENV=production umi webpack

快速查看当前项目使用到的所有的 umi 插件

$ umi plugin [options]

当前支持的 type 是 list,可选参数 key。

$ umi plugin list $ umi plugin list —key

  1. <a name="Txonx"></a>
  2. ## umi build 打包
  3. 编译构建 web 产物。通常需要针对部署环境,做特定的配置和环境变量修改。相关详情,请查阅[部署](https://umijs.org/zh-CN/docs/deployment)。
  4. ```shell
  5. $ umi build

默认产物输出到项目的 dist 文件夹,你可以通过修改配置 outputPath 指定产物输出目录。 默认编译时会将 public 文件夹内的所有文件,原样拷贝到 dist 目录,如果你不需要这个特性,可以通过配置 chainWebpack 来删除它。

  1. export default {
  2. chainWebpack(memo, { env, webpack }) {
  3. // 删除 umi 内置插件
  4. memo.plugins.delete('copy');
  5. }
  6. }

注意:如果 public 里面存在产物同名文件,如 index.html,将会导致产物文件被覆盖。

umi dev 启动服务器

启动本地开发服务器进行项目的开发调试

  1. $ umi dev

启动在浏览器中运行的开发服务器,并监视源文件变化,自动热加载。
默认使用 8000 端口,如果 8000 端口被占用,将会使用 8001 端口,以此类推。 你可以通过设置环境变量 PORT 来指定开发端口号。更多环境变量配置,请查阅环境变量

开启开发服务还会同时提供一个 Network 的链接,你可以在能访问到你当前运行设备的其他设备中预览页面。

注意:如果是在开启了VPN,或者虚拟机等复杂的网络环境中,这个地址很可能会错误。你可以通过访问你真实可用 ip的对应端口号来访问开发页面。

umi generate 生成器

内置的生成器功能,内置的类型有 page ,用于生成最简页面。支持别名调用 umi g

  1. $ umi generate <type> <name> [options]

这个命令支持扩展,通过 api.registerGenerator 注册,你可以通过插件来实现自己常用的生成器。

  1. import { Generator, IApi } from 'umi';
  2. const createPagesGenerator = function ({ api }: { api: IApi }) {
  3. return class PageGenerator extends Generator {
  4. constructor(opts: any) {
  5. super(opts);
  6. }
  7. async writing() {}
  8. };
  9. }
  10. api.registerGenerator({
  11. key: 'pages',
  12. Generator: createPageGenerator({ api }),
  13. });
  1. umi generate page pageName
  2. umi generate page pageName --typescript
  3. umi generate page pageName --less

更多使用类型和参数,请查阅提供生成器扩展的插件的文档。

环境变量

Windows (cmd.exe)

$ set PORT=3000&&umi dev

如果要同时考虑 OS X 和 Windows,可借助三方工具 cross-env

$ cnpm i -D cross-env $ cross-env PORT=3000 umi dev

  1. <a name="GGYT3"></a>
  2. ##### 在 .env 文件中定义
  3. Umi 中约定根目录下的 .env 为环境变量配置文件。<br />比如:
  4. ```shell
  5. PORT=3000
  6. BABEL_CACHE=none

然后执行,

  1. $ umi dev

会以 3000 端口启动 dev server,并且禁用 babel 的缓存。

以下面demo为例-介绍umi基础

初始化

  1. cnpm i -g umi
  2. mkdir 9.umi
  3. cd mkdir 9.umi
  4. cnpm init -y
  5. # 新建pages目录
  6. mkdir src/pages

约定式路由

除配置式路由外,Umi 也支持约定式路由。约定式路由也叫文件路由,就是不需要手写配置,文件系统即路由,通过目录和文件及其命名分析出路由配置。

如果没有 routes 配置,Umi 会进入约定式路由模式,然后分析 src/pages 目录拿到路由配置。

比如以下文件结构:

  1. .
  2. └── pages
  3. ├── index.tsx
  4. └── users.tsx

会得到以下路由配置:

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

注意:满足以下任意规则的文件不会被注册为路由。

  • 以 . 或 _ 开头的文件或目录
  • 以 d.ts 结尾的类型定义文件
  • 以 test.ts、spec.ts、e2e.ts 结尾的测试文件(适用于 .js、.jsx 和 .tsx 文件)
  • components 和 component 目录
  • utils 和 util 目录
  • 不是 .js、.jsx、.ts 或 .tsx 文件
  • 文件内容不包含 JSX 元素

创建 首页、用户管理、个人中心3个页面。

首页
  1. ❯❯ ~ umi g page index # 通过umi创建一个新的页面,名字叫index(首页)
  2. Write: pages\index.js
  3. Write: pages\index.css
  4. ❯❯ ~ umi g page user # 创建 user 页面
  5. Write: pages\user.js
  6. Write: pages\user.css
  7. ❯❯ ~ umi g page profile # 创建 profile 页面
  8. Write: pages\profile.js
  9. Write: pages\profile.css

pages/index.js
  1. import React from 'react';
  2. import {Link} from 'umi';
  3. export default function Page() {
  4. return (
  5. <div>
  6. <h1>首页</h1>
  7. <Link to="/profile">个人中心</Link>
  8. </div>
  9. );
  10. }

pages/profile.js
  1. import React from 'react';
  2. import {history} from 'umi';
  3. export default function Page() {
  4. return (
  5. <div>
  6. <h1>个人中心</h1>
  7. <button onClick={() => history.goBack()}>返回</button>
  8. </div>
  9. );
  10. }

全局 layout

约定 src/layuots/index.js 为全局路由,返回一个Reat组件,通过 props.children 渲染子组件(pages下文件)。

比如以下目录结构:

  1. .
  2. └── src
  3. ├── layouts
  4. └── index.tsx
  5. └── pages
  6. ├── index.tsx
  7. └── users.tsx

会生成路由:

  1. [
  2. {
  3. exact: false,
  4. path: '/',
  5. component: '@/layouts/index',
  6. routes: [
  7. { exact: true, path: '/', component: '@/pages/index' },
  8. { exact: true, path: '/users', component: '@/pages/users' },
  9. ],
  10. },
  11. ]

src/layouts/index.js
  1. import React from 'react';
  2. import {Link} from 'umi';
  3. export default class Layout extends React.Component{
  4. render(){
  5. return (
  6. <div>
  7. <ul>
  8. <li><Link to="/">首页</Link></li>
  9. <li><Link to="/user">用户管理</Link></li>
  10. <li><Link to="/profile">个人中心</Link></li>
  11. </ul>
  12. <div>{this.props.children}</div>
  13. </div>
  14. )
  15. }
  16. }

rwe.gif

嵌套路由

Umi 里约定目录下有 _layout.tsx 时会生成嵌套路由,以 _layout.tsx 为该目录的 layout。layout 文件需要返回一个 React 组件,并通过 props.children 渲染子组件。

比如以下目录结构:

  1. .
  2. └── pages
  3. └── users
  4. ├── _layout.tsx
  5. ├── index.tsx
  6. └── list.tsx

会生成路由:

  1. [
  2. {
  3. exact: false,
  4. path: '/users',
  5. component: '@/pages/users/_layout',
  6. routes: [
  7. { exact: true, path: '/users', component: '@/pages/users/index' },
  8. { exact: true, path: '/users/list', component: '@/pages/users/list' },
  9. ]
  10. }
  11. ]

rwe.gif

src/layouts/index.js 修改
  1. <li><Link to="/user/list">用户管理</Link></li>

src/pages/user/_layout.js (user页面的新入口)

如果有 pages/user/_layout.js, pages/user.js 就自动失效了。

  1. import React from 'react';
  2. import {Link} from 'umi';
  3. export default class Layout extends React.Component{
  4. render(){
  5. return (
  6. <div>
  7. <ul>
  8. <li><Link to="/user/list">用户列表</Link></li>
  9. <li><Link to="/user/add">新增用户</Link></li>
  10. </ul>
  11. <div>{this.props.children}</div>
  12. </div>
  13. )
  14. }
  15. }

src/pages/user/list.js
  1. import React from 'react';
  2. import {Link} from 'umi';
  3. export default function List() {
  4. return (
  5. <ul>
  6. <li><Link to="/user/detail/1">张三</Link></li>
  7. <li><Link to="/user/detail/2">李四</Link></li>
  8. </ul>
  9. );
  10. }

src/pages/user/add.js
  1. import React from 'react';
  2. export default function Add() {
  3. return (
  4. <form>
  5. <input type="text" />
  6. <button type="submit">提交</button>
  7. </form>
  8. );
  9. }

动态路由

约定 [] 包裹的文件或文件夹为动态路由。

比如:

  • src/pages/users/[id].tsx 会成为 /users/:id
  • src/pages/users/[id]/settings.tsx 会成为 /users/:id/settings

比如以下文件结构:

  1. .
  2. └── pages
  3. └── [post]
  4. ├── index.tsx
  5. └── comments.tsx
  6. └── users
  7. └── [id].tsx
  8. └── index.tsx

会生成路由配置:

  1. [
  2. { exact: true, path: '/', component: '@/pages/index' },
  3. { exact: true, path: '/users/:id', component: '@/pages/users/[id]' },
  4. { exact: true, path: '/:post/', component: '@/pages/[post]/index' },
  5. { exact: true, path: '/:post/comments', component: '@/pages/[post]/comments' },
  6. ];

src/pages/user/detail/[id].js
  1. import React from 'react';
  2. import {Link} from 'umi';
  3. export default function Detail(props) {
  4. console.log(props);
  5. return (
  6. <div>
  7. <div>ID: {props.match.params.id}</div>
  8. </div>
  9. );
  10. }

动态可选路由

约定 [ $] 包裹的文件或文件夹为动态可选路由。

比如:

  • src/pages/users/[id$].tsx 会成为 /users/:id?
  • src/pages/users/[id$]/settings.tsx 会成为 /users/:id?/settings

比如以下文件结构:

  1. .
  2. └── pages
  3. └── [post$]
  4. └── comments.tsx
  5. └── users
  6. └── [id$].tsx
  7. └── index.tsx

会生成路由配置:

  1. [
  2. { exact: true, path: '/', component: '@/pages/index' },
  3. { exact: true, path: '/users/:id?', component: '@/pages/users/[id$]' },
  4. { exact: true, path: '/:post?/comments', component: '@/pages/[post$]/comments' },
  5. ];

权限路由

  • 通过指定高阶组件 wrappers 达成效果。
    src/pages/profile.js
    ```jsx import React from ‘react’; import {history} from ‘umi’;

function Profile() { return (

个人中心

); }

// 用 auth 高阶组件包裹 Profile。 Profile.wrappers = [‘@/wrappers/auth’]; export default Profile;

  1. <a name="QD3Ie"></a>
  2. ##### src/wrappers/auth.js
  3. ```jsx
  4. import {Redirect} from 'umi';
  5. function auth(props){
  6. const isLogin = localStorage.getItem('isLogin');
  7. if (isLogin){
  8. return props.children;
  9. } else {
  10. return <Redirect to={{pathname: '/login', state: {from: '/profile'}}} />
  11. }
  12. }
  13. export default auth;

src/pages/login.js
  1. import React from 'react';
  2. import {history} from 'umi';
  3. function Login(props){
  4. let toLogin = () => {
  5. localStorage.setItem('isLogin', 'true');
  6. if (props.location.state && props.location.state.from){
  7. history.push(props.location.state.from);
  8. }
  9. }
  10. return (
  11. <div>
  12. <h1>登录</h1>
  13. <button onClick={toLogin}>登录</button>
  14. </div>
  15. )
  16. }
  17. export default Login;

动态路由

  • 运行时配置和配置的区别是他跑在浏览器端,基于此,我们可以在这里写函数、jsx、import 浏览器端依赖等等,注意不要引入 node 依赖。
  • 约定 src/app.tsx 为运行时配置。

前台运行时

src/components/Foo.js
  1. import React from 'react';
  2. export default function Foo(){
  3. return <div>Foo</div>
  4. }

src/app.js
  1. // 修改路由。
  2. // 比如在最前面添加一个 /foo 路由
  3. export function patchRoutes({routes}){
  4. routes.unshift({
  5. path: '/foo',
  6. exact: true,
  7. component: require('@/components/Foo').default,
  8. });
  9. }

接口返回

项目目录/mock/routes.js (umi 约定 /mock 文件夹下所有文件为 mock 文件。)

添加后 http://localhost:8001/api/routes 就可以直接访问了,返回 [{path: ‘/foo’, component: ‘Foo.js’}]

  1. export default {
  2. // GET 可忽略
  3. 'GET /api/routes': [
  4. {
  5. path: '/foo',
  6. component: 'Foo.js',
  7. }
  8. ],
  9. // 支持自定义函数,API 参考 express@4
  10. 'POST /api/users/create': (req, res) => {
  11. // 添加跨域请求头
  12. res.setHeader('Access-Control-Allow-Origin', '*');
  13. res.end('ok');
  14. },
  15. }

src/app.js
  1. let extraRoutes;
  2. // 修改 clientRender 参数。
  3. export function modifyClientRenderOpts(memo){
  4. memo.routes.unshift(...extraRoutes);
  5. return memo;
  6. }
  7. // 覆写 render。
  8. export function render(oldRender){
  9. fetch('/api/routes').then(res => res.json()).then(res => {
  10. extraRoutes = res.map(item => {
  11. const component = require(`./components/${item.component}`).default;
  12. return {...item, component};
  13. })
  14. oldRender();
  15. })
  16. }