每日优鲜供应链前端团队微前端改造
我所在团队是做 toB 业务的,技术栈是 Vue,团队目前有十多个典型的 toB 业务(菜单+内容布局),这些业务都是服务于一个大平台的,因为历史原因,每个业务都是独立的,都有一个 html 入口,所以当用户在这个大平台上使用这十多个业务的时候,每当切换系统时,页面都会刷新,体验很差;在开发层面,这十多个业务又有太多共同之处,每次修改成本都很高。
最近有一个很重要的需求 X,内容是这样的:从十多个项目中,每个项目抽取若干功能组成一个新项目,基于现有架构的话,每当点击来自不同系统的功能页面就要刷新一次,这是不可接受的。为了新需求 X 重复开发一遍这些业务功能又不现实,所以从技术角度来看,架构改造不可避免。
经过一番调研比对,我们决定使用当下比较火的 SingleSpa [1] 来完成改造(iframe 方案毫无亮点,弃之),目前改造已完成,我们实现了以下效果:
-
只有一个不包含子项目(子项目指的是那十多个业务)资源的主项目,主项目只有一个 html 入口,子项目通过主项目来按需加载,子系统间切换不再刷新;
-
菜单栏、登录、退出等功能都从子项目剥离,写在主项目里,再有相关改动只需修改主项目,包括错误监控、埋点等行为,只需处理一个主项目,十几个子项目不再需要处理;
-
子项目原本需要加载的公共部分(如 vue、vuex、vue-router、ivew/element、私有 npm 包等),全部由主项目调度,配合 webpack 的 externals 功能通过外链的方式按需加载,一旦有一个子项目加载过,下一个子项目就不需要再加载,这样一来每个子项目的 dist 文件里就只有子项目自己的业务代码(最终子项目包的体积缩小了 80%,只有几十 k),项目实际加载速度快了很多,肉眼可见;
-
子项目并没有重新开发,只是进行了一些改造,接入了微前端这套架构,所以新需求 X 的开发成本也极大的降低了,接入功能同时可供未来新增子项目使用;
-
我们的项目有自己的 tab 系统(类似浏览器的 tab 页签),这些 tab 页签通过 keep-alive 和一系列对缓存的处理,使其体验接近原生浏览器 tab。
二、展示以及技术点
图 1:项目外观示意图:

做微前端改造之前,蓝色系区域都是用公共包的方式由每个子项目引入,所以子项目运行的时候展示的蓝色系部分都是相同的,给人一种在使用同一个系统的错觉,实际上切换系统的时候整个页面都要重新载入。
微前端改造后,只有橘色区域是变化的,页面也不再刷新。
图 2:局部效果动图

图 2 展示了图 1 中的 tab 页签区以及子项目展示区。信息做了马赛克处理。
乍一看没什么特别的,但如果我说这些 tab 分别来自于不同 git 仓库的独立 vue 项目 呢?这就是这套微前端架构的强大之处,让 不同单页 vue 项目可以随意组合成一个项目,而这些项目自己又是独立的 vue 项目 。
仔细看图 2 中路由的变化, hash 路由的第一级 决定了要加载哪个子项目(work、sms、tms 是三个不同的 git 工程),不同子项目间的切换也完全没有刷新 😀
为了让 tab 切换不刷新,这里使用了 keep-alive 去缓存页面,考虑到内存性能,在关闭 tab 页签时通过一些方法(主要是 keep-alive 的 exclude 属性)去除了 keep-alive 缓存,同时为了让子项目间的 tab 切换也不刷新,对图 3 下面提到的包装器也进行了不小的改造。让 tab 切换不刷新只是为了提升用户体验,这一步不是必要的,有一定的成本。
图 3:部署架构示意图

实现一套微前端架构,可以把其分成四部分(参考: alili.tech/archive/110… [2] )
-
加载器:也就是微前端架构的核心,图 3 中的“加载器 JS 文件”就是由加载器打包压缩出来的,这是原始的加载器: github.com/Fantasy9527… [3] —— 可以把它理解成电源
-
包装器:有了加载器,我们要把现有的 vue 项目包装一下,使得加载器可以使用它们,这是原始的包装器: github.com/CanopyTax/s… [4] —— 如果想改造,建议改造这个部分,它相当于电源适配器
-
主项目:一般是包含所有项目公共部分的项目—— 它相当于电器底座
-
子项目:众多展示在主项目内容区的项目—— 它相当于你要使用的电器
所以是这么个概念: 电源(加载器)→ 电源适配器(包装器)→️ 电器底座(主项目)→️ 电器(子项目)️
主项目和子项目都需要用包装器包装,只不过主项目的配置写法有不同
加载器和包装器需要根据自己的需求做一些二次开发
总的来说是这样一个流程:用户访问 index.html 后,浏览器运行加载器的 js 文件,加载器去读取图 4 中的配置文件,然后注册配置文件中配置的各个项目后,首先加载主项目(菜单等),再通过路由判定,动态远程加载子项目。
这里有个 vue 微前端版 demo [5] ,包含最基础的 效果与源码 ,务必研究一下这个 demo 再结合以上理论来帮助理解 *远程加载的子项目资源要在 chrome 的 network 中的 xhr 那一栏才能看到
图 4:图 3 中的 apps.config.js

用户访问 index.html 后,js 加载器会加载 apps.config.js。无论路由是什么,每次必会首先加载主项目,再根据路由来匹配要加载哪个子项目。apps.config.js 的生成如图 3 的绿色部分所示:
在资源服务器上起一个监听服务(我使用的是 nodejs 脚本+pm2 守护),原有子项目的部署方式完全不变(前后端完全分离,资源带 hash),当监听服务检测到文件改动时,去子项目部署文件夹里找它的 index.html,把入口 js 用如下正则匹配出来,写入 apps.config.js。
// content[i]为子项目文件夹名称。这段代码是nodejs脚本片段。 const reg = new RegExp(`src="(\/${content[i]}\/index\.\w{8}.js)`) // 对应图中的 /brain/index.3c4b55cf.js
图 4 中的 brain 即是主项目,它的 base 属性为 true,其余子项目的 base 属性为 false
三、一些技术细节
这里说的的项目打包都是基于 webpack。
System.js
它是实现远程加载子项目的核心。我们使用的是 0.21 版本的: github.com/systemjs/sy… [6] 因为要动态通过 http 引入外部 js,又不影响在开发的时候使用 import、require 方法,所以找到了 systemjs 来做这件事。根据 systemjs 文档说明,我们只需要把子项目打成 umd 格式(umd 糅合了 AMD 和 CommonJS)的包即可动态外部加载。
// 每个子项目的webpack.config.js output: { path: xxx, publicPath: xxx, filename: '[name].[chunkhash:8].js', chunkFilename: 'js/[name].[chunkhash:8].chunk.js', libraryTarget: 'umd', // 这里一定要写成umd,不然打出来的包system.js无法读取 library: xxx, //模块的名称 },
Webpack Externals
文档: www.webpackjs.com/configurati… [7] 这么多同类型的 vue 项目,一定有大量的重复代码、重复引用,所以这是一块巨大的性能优化点,通过配置 externals 可以极大减小子项目打包出来的体积。
我并没有完全按照文档说明的方式来从 CDN 引入,原因是这样的:入口 index.html 只有一个,如果按文档来做,一次引入所有 CDN 资源,可能子项目 A 用得到这些,但子项目 B 用不到这些,而我只访问了子项目 B 而已,这样不就多加载了无用的资源吗?经过一番调研,同样利用 systemjs 解决了这个问题
// 每个子项目自己的webpack.config.js,根据使用情况设置externals externals: { 'axios': 'axios', 'vue': 'Vue', 'vue-router': 'VueRouter', 'vuex': 'Vuex', 'iview': 'iview', 'moment': 'moment', 'echarts': 'echarts', '@mfb/pc-utils-micro':'@mfb/pc-utils-micro', // 私有公共方法包 '@mfb/pc-components-micro':'@mfb/pc-components-micro', // 私有公共组件包 // '@mfb/pc-components-micro':'@mfb/pc-components-micro-0.2.1', // 如果需要指定版本,则用这一行替换上一行 ... },
// index.html 整个微前端的唯一入口
如此一来,systemjs 只是在加载 index.html 时注册了这些 CDN 地址,不会直接去加载,当子项目里用到的时候,systemjs 会接管模块引入,systemjs 会去上面注册的 map 中查找匹配的模块,就再动态去加载资源。这样就避免了不同子项目在这套架构下产生的多余加载。
按我们的配置,webpack 打包后,externals 配置的模块不会打包进 bundle,会被摘出来按 umd 规范通过 requre/define 方式去加载。
看 systemjs 源码会发现它重新定义了 require 和 define 方法,所以它能接管 externals 的外部引入过程。
四、总结体会
我最直白的感受是实现了项目级别的模块化,把不同项目变成了一个个模块来拼装组合,也就是说模块化从项目内提升到了项目本身
总结一下使用这套架构收到的好处,分为以下几点:
-
缩小项目打包体积(平均每个子项目 bundle 不到 100k),而整合后的公共资源只需加载一次,性能得到很大提升 (技术角度)
-
用户体验更好,用户感知不到自己在使用多个不同的项目,更加平顺流畅 (产品角度)
-
不同 git 的项目经过改造后,可以随意以项目内每个路由页面为单元拼装成一个新项目,产品灵活性本质上得到提升 (产品/技术角度)
-
技术尝新,使用业界比较先进的微前端理念,几十个项目,成千上百个功能也能很好的分模块管理。(管理角度)
也是有很多麻烦之处,需要消耗一定成本:
-
因为多个 vue 实例在同一个 document 里,需要避免全局变量污染、全局监听污染、样式污染等,需要制定接入规范。
-
使用了 external 抽离公共模块(比如 Vue、Vue-router 等)后,构造函数(或者 Class)的污染也需要避免,比如 Vue.mixin、Vue.components、Vue .use 等等都需要做一些额外的工作去避免它们产生冲突。
-
如果你也想要 tab 切换不刷新(使用 keep-alive),那需要做的工作更多,主要是处理缓存,防止堆内存溢出(用 chrome 自带的 performance monitor 查看),还有项目间切换时路由钩子等等的处理。
不过跟收益比起来,这些成本就不算什么了~
最后要说一下, 并不是所有场景都适合微前端 ,尤其是项目规模小、数量少的场景不建议使用。什么样的 场景 适合这套架构呢?一般有以下特征:
-
项目很 多 ,规模很 大 ,都是每个项目 独立使用 git 此类仓库维护的、技术栈为 vue/react/angular 的这类应用
-
需要整合到统一平台上,你正在寻找 比 iframe 好得多 的替代方案
-
项目 A 有功能 A1、A2、A3,项目 B 有功能 B1、B2、B3,产品经理要你把 A2、B1、B3 组合成一个 包含这些功能的新项目
可能你会问:为什么不一开始就把所有需要整合的功能用一个 git 来维护?答:理想是美好的,谁也没有先知能力,随着公司业务发展亦或是组织架构的改变、人员更迭,以上场景是几乎不可避免的;我很难想象十多个项目的好几百个功能都在一个 git 里管理起来有多困难。 可能你还会问,那我把需要整合的业务整合成到一个 git 仓库呢? 答:这当然是一个解决办法,前提是整合的成本你能接受;并且将来还有这类需求呢?每次都要手动整合业务代码到同一个 git 仓库吗?假设所有人都只维护这个整合完的 git 仓库,并行的需求线多了,上线时间会不会拥挤?一个功能产生了致命错误,会不会所有功能跟着出问题?
最后我想说:
我们做这套框架的初衷是解决眼前的问题,然而发现它附带的潜力价值却比想象的多得多。
原文: https://juejin.im/post/5d7f702ce51d4561f777e258
参考资料
SingleSpa: https://link.juejin.im?target=https%3A%2F%2Fsingle-spa.js.org%2F
alili.tech/archive/110…: https://link.juejin.im?target=https%3A%2F%2Falili.tech%2Farchive%2F11052bf4%2F
github.com/Fantasy9527…: https://link.juejin.im?target=https%3A%2F%2Fgithub.com%2FFantasy9527%2Flotus-scaffold-micro-frontend-portal
github.com/CanopyTax/s…: https://link.juejin.im?target=https%3A%2F%2Fgithub.com%2FCanopyTax%2Fsingle-spa-vue
vue 微前端版 demo: https://link.juejin.im?target=https%3A%2F%2Fgithub.com%2Fjoeldenning%2Fcoexisting-vue-microfrontends
github.com/systemjs/sy…: https://link.juejin.im?target=https%3A%2F%2Fgithub.com%2Fsystemjs%2Fsystemjs%2Ftree%2F0.21
www.webpackjs.com/configurati…: https://link.juejin.im?target=https%3A%2F%2Fwww.webpackjs.com%2Fconfiguration%2Fexternals%2F