阅读 9693

一文带你看懂 UmiJS (3.x版本)

Umi介绍

Umi是蚂蚁金服的底层前端框架,我用这个框架也有一段时间了,但是网上umi相关的文章甚少呀,特别对于是3.x版本,本文就来简单介绍一下umi框架。

从3.0版本开始它们的slogan就改成了插件化的企业级前端应用框架,插件这个概念让整个umi框架的扩展性大大提高,插件让框架支持各种功能扩展和业务需求。可以说插件就是umi的灵魂

官网文档描述Umi的6大特性:

  • 🎉 可扩展,Umi 实现了完整的生命周期,并使其插件化,Umi 内部功能也全由插件完成。此外还支持插件和插件集,以满足功能和垂直域的分层需求。
  • 📦 开箱即用,Umi 内置了路由、构建、部署、测试等,仅需一个依赖即可上手开发。并且还提供针对 React 的集成插件集,内涵丰富的功能,可满足日常 80% 的开发需求。
  • 🐠 企业级,经蚂蚁内部 3000+ 项目以及阿里、优酷、网易、飞猪、口碑等公司项目的验证,值得信赖。
  • 🚀 大量自研,包含微前端、组件打包、文档工具、请求库、hooks 库、数据流等,满足日常项目的周边需求。
  • 🌴 完备路由,同时支持配置式路由和约定式路由,同时保持功能的完备性,比如动态路由、嵌套路由、权限路由等等。
  • 🚄 面向未来,在满足需求的同时,我们也不会停止对新技术的探索。比如 dll 提速、modern mode、webpack@5、自动化 external、bundler less 等等

个人使用下来的感受,umi确实是个成熟的框架了,很懂得给我们前端省事,用起来很舒服。

真正的开箱即用

umi跟create-react-app有什么区别呢?大多数前端同学都试过用cra来搭react项目吧,每次总是要花很多时间来配置typescript/less/css modules,还有配套的react router/redux等,搭建一个基础项目就能花上好几天,而umi这个企业级的前端应用框架,则已经帮我们内置了很多前端开发常用的功能,让我们节省了很多时间去配置,做到真正的开箱即用。

执行npx @umijs/create-umi-app初始化出来的react项目,天然就支持typecsript、less、css modules,并整合了antd、dva(阿里自研的数据流方案),提供国际化、权限、数据流、配置式路由等开发者常用的功能,能够节省大量的初始化项目时间。

约定化的思想

在使用umi框架的时候,很容易就发现它很多东西都是约定式的。所谓约定式就是指,按照约定好的方式开发,就能达到某种效果,中间的过程由框架帮我们完成,特别适合我们这种懒懒的开发。

  • 建一个 locales 目录,就拥有了国际化
  • 建一个 models 目录,就拥有了数据流
  • 建一个 mock 目录,就拥有了数据 mock
  • 建一个 access.ts 文件,就拥有了权限策略
  • ...

这看起来是非常黑盒非常酷的,用这种方式其实对于团队代码风格的统一是非常有好处的,直接在框架层面就约束了大家的目录组织模式,便于团队维护。

但是缺点也是挺明显的,灵活性不如配置式的高,因为只能按特定的模式来开发,如果原本约定的方式不满足业务需求,就需要额外开发umi插件来魔改原本的功能。而且约定式的开发是相对其他框架来说很特别的一点,对新上手的同学来说需要时间去通读官网文档了解约定式的规则。

插件体系

可以说插件体系是 Umi 最重要的基建,因为包括 Umi内部实现也是全部由插件构成,Umi通过插件体系实现了技术收敛,把大家常用的技术栈进行整理,让大家只用 Umi 就可以完成 80% 的日常工作。3.x版本重构了整个插件体系。从图看出 presets插件集 和 plugins插件 垂直分层,插件组合成不同的插件集来处理不同的业务场景。而umi本身已经内置了一个针对react应用的插件集(@umijs/preset-react),正是这个插件集中的插件引入了react项目中常用的一些功能如antd,数据流,国际化等。

3.x版本的变化

  • 彻底重写的代码和文档,80%+ 的覆盖率,~100M 的尺寸
  • 官方插件、插件集和最佳实践
  • 更智能(CSS Modules 的自动识别、约定式路由的改进等)
  • Import “所有” from Umi。通过插件扩展 import from umi 的能力
  • node_modules 走 babel 编译
  • ...

有兴趣的小伙伴可以去看看云谦大佬的文章,讲得很详细

发布 UMI 3,插件化的企业级前端应用框架

Umi基础

这里介绍下umi项目的目录结构已经它最有特色的约定式路由。

目录结构

注意不要提交 .umi 目录到 git 仓库,这些是临时文件,它们会在 umi dev 和 umi build 时被删除并重新生成。

里面最重要的文件是.umirc.ts配置文件,在里面可以配置各种功能和插件,umi支持不同环境读取不同的配置文件

约定式路由

约定式路由也叫文件路由,就是不需要手写配置,文件系统即路由,通过src/pages目录和文件及其命名分析出路由配置, 也就是让umi根据约定好的目录结构帮我们生成路由配置文件。

比如以下文件结构:

  └── pages
    ├── index.tsx
    └── users.tsx
    └── setting
       └── index.tsx
复制代码

会得到以下路由配置,

[
  { exact: true, path: '/', component: '@/pages/index' },
  { exact: true, path: '/users', component: '@/pages/users' },
  { exact: true, path: '/setting', component: '@/pages/setting/index' }
]
复制代码

需要注意的是,满足以下任意规则的文件不会被注册为路由,

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

动态路由

src/pages/users/[id].tsx 生成的对应path /users/:id

嵌套路由

└── pages
    └── users
        ├── _layout.tsx
        ├── index.tsx
        └── list.tsx
复制代码

生成的对应path

[
  { exact: false, path: '/users', component: '@/pages/users/_layout',
    routes: [
      { exact: true, path: '/users', component: '@/pages/users/index' },
      { exact: true, path: '/users/list', component: '@/pages/users/list' },
    ]
  }
]
复制代码

全局Layout

src/layouts/index.tsx

生成的对应path

[
  { exact: false, path: '/', component: '@/layouts/index',
    routes: [
      { exact: true, path: '/', component: '@/pages/index' },
      { exact: true, path: '/users', component: '@/pages/users' },
    ],
  },
]
复制代码

404路由

src/pages/404.tsx

生成的对应path

[
  { exact: true, path: '/', component: '@/pages/index' },
  { exact: true, path: '/users', component: '@/pages/users' },
  { component: '@/pages/404' },
]

复制代码

扩展路由属性

umi 3.0开始就支持在代码层通过导出静态属性的方式扩展路由。比如:

function HomePage() {
  return <h1>Home Page</h1>;
}
HomePage.title = 'Home Page';
HomePage.redirect = '/exception';
HomePage.wrappers = ['@/components/Permission'];

export default HomePage;
复制代码

title、redirect、wrappers会附加到路由配置中 (注意如果用connenct导出组件,属性要写在connect后的外层组件上)

{
  "path": "/home",
  "title": "Home Page",
  "exact": true,
  "component":  …
  "redirect": "/exception",
  "wrappers": [require('@/components/Permission').default]
}
复制代码

可不要小看了这个功能,导出的redirect属性可以帮我们重定向,wrappers则可以帮我们实现高阶组件来做权限校验等处理

Umi插件

Umi从编译、打包到cli的运转都是通过运行插件去完成这一些的功能,对于它自身的核心包而言,就是负责去管理注册调度这些插件,你可以认为它整合核心是非常小的,离开了插件,甚至很多内置的api没办法调用。可以说插件驱动整个内核运转,微内核的思想体现得很好。

什么场景需要用到Umi插件?

当一个功能涉及到前端的各个部分(比如 HTML,CSS,JS)或者涉及到运行或构建时的逻辑时,而你又希望能够极简的使用并能够方便的提供给其它项目复用该功能。那么你就应该使用插件来实现你的功能。

umi 的插件机制在项目的各个阶段和各个部分提供了不同的接口,使得插件能够在 web 开发中在不同的阶段对不同的部分去执行它需要的操作。

比如如下的一些例子: 在构建时添加一个 webpack 插件或修改某个 webpack 配置。 往 HTML 中添加内容。 构建完成后处理构建产物。 获得应用和路由对应的配置。

Umi插件的目的

因为框架是没法满足所有人的要求的,所以最好是通过插件去满足不同的业务需求和不同的技术实现。

  • 满足不同的业务需求

比如无线业务,会比较关注性能,所以可能会选一个切 preact 到 react 的插件、极速版补丁插件、高清方案、fastclick 等等,形成一个插件集

  • 满足不同的技术实现

在一个业务类型丰富的大团队中,是允许有不同的选择的。比如数据流,大家的选择可能不同,有些用 dva,有些用 hooks,有些用 mobx,有些自研一套;比如补丁方案,有常规版、极限版,还有终极版

Umi插件三态

这是插件生命周期图,包含:

node 环境执行的 编译时, 浏览器上执行的 运行时, ui 辅助层的 编辑时

大部分插件体系只会考虑 node 编译时,而umi插件加上运行时和编辑时的支持,赋予了插件更大的能力。

Umi Ui是一个本地研发工作台,让用户通过可视化界面去配置管理项目

Umi插件在项目中的使用

Umi插件都会对应一个 id 和一个 key。 id 是路径的简写,key 是进一步简化后用于配置的唯一值。 在配置文件.umirc.ts中在插件的key这个属性上配置插件的参数

Umi 会自动检测 dependencies和devDependencies 里的 umi 插件,符合这些命名的都会被自动引入

Umi插件开发

我们可以通过自己写插件去使用修改代码打包配置,修改启动代码,约定目录结构,修改 HTML 等。通过umi插件基本可以魔改 umi 内部 70% 的功能,以此来达到满足需求业务需求的目的。

Umi插件实际上就是一个 JS 模块,你需要定义一个插件的初始化方法并默认导出

export default (api) => {
  // your plugin code here
};
复制代码

我给官方插件开发的 API 归为以下几类:

  • 属性
    • 环境变量、配置、路径等
  • 功能函数
    • 工具类函数,日志,获取路由,写临时文件,检查是否有插件等
    • 获取端口号、重启服务等
  • 生命周期hook
    • Event类 API,服务启动、服务暂停、路由修改等监听事件 (无需返回)
    • Modify类 API,修改config、修改路由 (对第一个参数做修改)
    • Add类API,增加Html头等需要返回内容的API (数组)

使用这些api就可以来开发一个Umi插件了

来看一个例子

背景:Umi 约定式路由中的表现是主路由,对应到index路由,即访问 http://localhost:8000 实际上访问到的页面是 src/pages/index,有时候我们在开发过程中会遇到,希望修改主路由的情况,比如希望路由 / 访问的是 src/pages/home。

export default function (api: IApi) {
  // 打印日志
  api.logger.info('use plugin');

 // 修改html
 api.modifyHTML(($) => {
    $('body').prepend(`<h1>hello umi plugin</h1>`);
    return $;
 });

 // 描述插件的 id、key、配置信息、启用方式等
 api.describe({
   key: 'mainPath',
   config: {
     schema(joi) {
       return joi.string();
     },
   },
 });

 if (api.userConfig.mainPath) {
   // 修改路由文件
   api.modifyRoutes((routes: any[]) => {
     return resetMainPath(routes, api.config.mainPath);
   });
 }
}
复制代码

开发完插件后就可以在项目中引入使用了:

import { defineConfig } from 'umi';

export default defineConfig({
  plugins: [require.resolve('../lib')],
  mainPath: '/home'
});
复制代码

按命名规范发布npm包后,就不用配置plugins这一项了。

参考资料

文章分类
前端
文章标签