微前端从入门到熟悉 | 🏆 技术专题第四期

4,319 阅读11分钟

了解微前端

微前端是最近一个很火的话题,前端就跟谈恋爱一样,真的是“合久必分,分久必合”。微前端不是炫技,而是一个解决方案。

微前端的发展历程

举个例子,公司内部有很多的系统,比如说人力系统、财务系统、工时管理、项目管理、论坛等等,当公司有新人入职时,短时间内他要记住全部的地址比较难。

如果地址不太多,存浏览器书签可以记住,但是地址有变动时(比如说新旧版本同时存在),本地书签就不好使。

为了解决这个问题,不同时期的做法:

早期: 比较常见的做法是做一个网址导航,这样能解决地址太多记不住的问题,以及地址变动的问题,但是系统之间的切换不太友好,得返回到导航页才能切换。例如 hao123。

中期: 把网址导航放到每个系统的左上角,点击弹出网址导航菜单。然后把网址导航单独做成一个 js ,维护一份,所有系统共用,缺点就是系统切换需要刷新页面,并且有太多的东西重复加载。例如 阿里云控制台。

后期: 分为主子系统,网址导航和页面顶部/底部放主系统,不重复加载,内容区用 iframe 呈现子系统,点击导航,只切换内容,不刷新页面。这就是微前端的雏形。

什么时候应该用微前端呢

  1. 项目引入其他的项目时(合)

一些遗产项目(或维护项目),也还能用,不想大改动,需要整合到一起时。

再比如,一个系统的某些功能/页面,另一个系统已经存在,需要复用时,可以考虑微前端。

  1. 当你的项目非常庞大时(分)

当然了,“庞大”这个定义很模糊,怎样算庞大,又应该如何拆分?这得根据具体情况来判断

举个例子,你们在做“人力资源管理系统”,包含了很多个模块:人员档案、入职离职升职、考勤、绩效、薪酬社保、培训、招聘等等。现在你们有个客户,只需要其中几个模块,并且需要源码。

买全部功能吧,客户不愿意付那么多钱。给全部代码吧,公司又觉得亏了。当然了,也可以挑选出客户需要的代码,但是如果多来几个这样的客户,那就需要做很多的无用功。

如果能把项目拆分一下,分为一个基础功能和 N 个附加功能,各功能独立成一个系统,独立开发部署,这样就能自由组合售卖,公司和客户皆大欢喜。

微前端不适用的情况

项目非常大,但是没有自由组合售卖的需求,需要重构时,这个时候优先考虑技术选型(vuereactangular),不建议用微前端。

即使一个项目几百个页面,只要管理得当、文档齐全、做好自动化和工程化,还是能很好地迭代的。

常见微前端方案

网上的各种微前端方案看着非常炫酷,各种名词也很高大上,实际上换汤不换药。真正能落地到生产的,可复用的方案就只有 iframesing-spa ( qiankun 基于 single-spa )。思路是一样的,细节不同而已。

微前端技术的核心是加载执行。除 iframe 方案外,加载的方案有三种:JS entryHTML entryconfig entry;执行文件比较麻烦,并不是一个简单的 script/style 标签就能实现(虽然也可以用,但是不能称之为一个成熟的方案,因为要解决 js/css 污染)。

iframe 方案

iframe 在前端的发展过程中始终扮演着重要角色:

跨 HTML 页面加载:从多页应用到单页应用时(点击菜单不刷新页面),iframe 是其中一个解决方案。后来三大框架崛起,iframe 渐渐淡出视野。

跨系统加载:一个系统中加载另一个系统时,iframe 是浏览器提供的原生方案,好用又简单,所以也是一个微前端方案。

iframe 方案除了用户体验差一点,其他的几乎没啥大问题,拥有完美的 js/css 隔离,不需要解决资源的加载问题。只要你的领导没啥意见,客户能接受,iframe 也是微前端一个很好的选择。

iframe 的门槛很低,但是,你想完善这个方案,付出和回报是不成比例的。要优化的问题太多了(具体有哪些问题,看文末链接)。

PS:chrome80+ 版本 cookie 的改版,又劝退了一波 iframe 方案:使用 http 协议时,如果 iframe 和主系统跨站iframe 居然不能存取自己的 cookie,这样系统局限性太大了(这个问题有机会会详细讲)

single-spa 方案

single-spa 其实做的事情比较少:定义了子项目的生命周期(初始化、加载、卸载、更新)、子系统状态,以及关联路由与子项目的加载/卸载时机,错误捕获等。

但是子项目的每个生命周期做什么事情,以及子项目的资源如何加载和执行,子项目如何卸载,都需要自己来完成。

好在有一些插件来帮助我们完成这些事情:

  1. single-spa-vue/(single-spa-react): 子项目的加载和卸载函数
  2. systemjs-webpack-interop: 修正资源的加载路径
  3. vue-cli-plugin-single-spa: 修改一些 webpack 打包配置
  4. sysyem.js:加载 js 资源和公共插件

这四个插件 + single-spa 就能实现微前端,一个完整的 single-spa 微前端加载执行流程:

由于 single-spa 需要根据页面的 location来判断子项目,用不同的值( hashpath)判断,对不同的子项目会产生不同的影响。如何选择判断的依据,以及如何消除这些影响,可以查看这个:主项目路由的 hash 与 history 之争

虽然这样就能实现微前端,但是给我的感觉就是拼凑出来的微前端,每一步都很巧妙。比如说子项目的 css 打包到 js,去掉 splitChunks ,这样子项目就只剩下一个 app.js 需要加载和执行。

稍微复杂一点的项目,使用 single-spa 都非常吃力,很多事情都需要靠自己动手解决,比如说:

  1. js/css 污染问题;
  2. 子项目 html 中其他的 js/css 文件,需要自己手动加载;
  3. 只有父项目给子项目传值,项目间的通信需要自己写;
  4. 打包部署比较麻烦;

single-spa 的官方在线实例:single-spa-examples

qiankun 方案

qiankun 的初衷就是像 iframe 一样简单好用,但是用户体验远远高于 iframe

single-spa 的基础上,qiankun 做了这些事情:

  1. 解决了子项目加载问题,只需要给一个入口即可
  2. 增加了 js/css 沙箱(说实话,没有完美的沙箱,可以兼容所有情况)。
  3. 增加了预请求
  4. 增加了项目间的双向通信
  5. 支持手动加载子项目

这样一来,子项目只需要打包部署一份,既可以独立访问,又可以被主项目集成,对子项目的改动也不多。

qiankun 上手很容易,但是解决问题很麻烦,尤其是 js 沙箱带来的插件冲突问题。建议先了解下 singles-spa 及沙箱的原理。

qiankun 方案的高级用法:

  1. 主项目和子项目共享依赖
  2. qiankun的嵌套
  3. 子项目实现 keep-alive

这些在以前的文章都有介绍(链接在文末)

微前端痛点:难以解决的 css 隔离问题

这里主要指的是qiankunsingle-spa 方案。

如果同时只存在一个子应用运行,子项目间的css 污染很好解决:

  • single-spa 可以通过修改 bodyclass ,子项目将样式写到这个class里面来解决。
  • qiankun 则通过子项目卸载时移除子项目的 style 标签解决。

但是主应用和子应用之间的样式污染,以及多个子应用同时运行时的 css 污染,难以解决。

qiankuncss 污染的问题上做了很多的新的尝试:

  1. shadow dom 来隔离( sandbox : { strictStyleIsolation: true } 开启)

如果不了解 shadow dom ,可以看看 MDN的介绍 。说实话,shadow dom 隔离效果挺好,就类似于 iframe 。但是会带来新的问题:

  • dom不通,导致子项目拿不到根节点

    解决方案:子项目查找根节点从容器开始,而不是document

    function render({ container } = {}) {
      // 省略无关内容
      instance = new Vue({
        router,
        store,
        render: h => h(App),
      }).$mount(container ? container.querySelector('#app') : '#app');
    }
    
  • 由于 react 的点击事件都是 document 代理,shadow dom 会导致 document 无法访问到子项目的 DOM,导致点击事件无效:github.com/facebook/re…

解决方案:react-shadow-dom-retarget-events

  • appendbody 的弹窗样式不生效:大部分情况下弹窗不需 要 append to body ,但是在弹窗中打开另一个弹窗(即嵌套弹窗)则需要 append to bodyPopover 弹出框是强制 append to body 的。

  • 兼容性堪忧:Firefox(从版本 63 开始),Chrome,Opera 和 Safari 默认支持 Shadow DOM。基于 Chromium 的新 Edge 也支持 Shadow DOM;而旧 Edge 未能撑到支持此特性

  • 子项目的字体文件无法生效,显示异常

  1. 将子项目的样式都包裹在一个特殊的选择器中。( sandbox : { experimentalStyleIsolation: true } 开启)

也就是会把子项目的样式局限到容器范围内生效,所有的样式都会包裹在容器里面,这是一个实验性的功能,生产不建议用,效果如下:

// div[data-qiankun="app-vue-hash"] 是 qiankun 动态加的
div[data-qiankun="app-vue-hash"] .app-main {
  font-size: 14px;
}

这个方案同样存在 append to body 元素样式无法生效的问题。

这两个方案并不完美,CSS 污染还得靠规范解决:如果是 vue 项目,每个组件都写上 scoped 属性,全局样式都用特殊的 class 名称/前缀,但是这样一来,有没有 css 沙箱都不重要了,从根源上避免了CSS 污染。

微前端的衍生需求:跨项目组件共享

一般项目间的组件共享,用私有 npm 就可以了,但是,如果项目的技术栈不同,比如说:想在 react 项目中使用 vue 组件,npm 包就比较麻烦。

借鉴微前端的做法:

  1. 项目 A 在入口文件 export 这个组件实例化的函数
  2. 项目 A 打包成 umd 格式,并将 css 打包到 js,去掉 chunk-vendors.js
  3. 打包项目 A ,将其 app.js 引入到项目 B 中,调用实例化函数即可

具体代码如下:

项目 A 的入口文件:

import Vue from 'vue';
import HelloWorld from '@/components/HelloWorld.vue'
import store from './store';
import i18n from './i18n';
// 该组件如果没有用到 store 和 i18n 等全局的插件,
// 并且调用的地方也是 vue 技术栈,则可以直接 export 这个组件:
// export HelloWorld;
// 使用时直接注册:
// components: {
//   HelloWorld: window['app-vue-hash'].HelloWorld
// },
export const createHelloWorld = container => {
  return new Vue({
    store,
    i18n,
    render: h => h(HelloWorld),
  }).$mount(container);
}

项目 A 的打包配置:

module.exports = {
  publicPath: 'http://localhost:8080/',
  css: {
    extract: false
  },
  // 自定义webpack配置
  configureWebpack: {
    output: {
      library: `app-vue-hash`,
      libraryTarget: 'umd',// 把子应用打包成 umd 库格式
    }
  },
  chainWebpack: config => {
    config.optimization.delete('splitChunks')
  }
};

项目 B 使用这个组件:

<div id=appVueHash></div>
<script src=http://localhost:8080/js/app.js></script>
<script>
    window['app-vue-hash'].createHelloWorld('#appVueHash')
</script>

注意的事项:

  1. 该组件一定要与路由无关,因为调用时的路由实例和组件实例化时路由实例不是同一个。
  2. 如果有参数需要传给组件,可以详细写一下 render 函数。
  3. 该组件需要手动销毁,函数调用返回的 vue 实例,直接调用 $destroy 即可。
  4. 该组件及其子孙组件引用资源的前缀需要通过 publicPath 设置,也可以动态修改,如果没有引用资源,则忽略。

其实还可以再优化一下:直接将这个组件挂载到 window 上,并且不需要打包 umd

window.createHelloWorld = container => {
  return new Vue({
    store,
    i18n,
    render: h => h(HelloWorld),
  }).$mount(container);
}

打包配置去掉 configureWebpack ,使用也很简单: window.createHelloWorld('#appVueHash')

同理,qiankun 的子项目也可以这样写,让父项目拿到生命周期函数。

微前端方案如何选型

不要以为跑通了 qiankun 的官方 demo,就可以直接用了。实际项目的复杂程度可能比这个复杂 N 倍,每一点差别都会带来新的问题。

所以经常有人出现这种情况:沙箱开启吧,项目跑步起来(一堆报错);不开沙箱吧,js/css 污染很严重。甚至是本地都跑得好好的,但是部署又不行了(这个问题相对好解决)。

选择 qiankun 之前务必想清楚两件事:

  1. 项目的资源是否能正确加载

    • 如果项目有用到 webpack

      确认下:修改 webpackpublicPath 项,能否解决所有的资源加载问题,可以看看 vue-cli3 的介绍:HTML 和静态资源

      简单判断:资源文件( js/css 除外)是否放在了 static 目录(vue-cli2) 或者 public 目录(vue-cli3),如果没有,或者有但是不多,修改起来也不麻烦的,就可以用qiankun

      精确判断:写死 webpackpublicPath 项,值为服务器 A 的地址,然后打包,把 index.html 放到服务器 B 上,其他所有资源放到服务器 A 上(记得允许跨域),然后访问项目(服务器 B 的地址),看看是否正常(模拟 qiankun 加载)。

    • 如果项目没有 webpack,比如说 jQuery 项目。

      • 如果资源都放在 cdn ,或者说资源的链接都是完整的 URL,那就可以放心大胆的接入。

      • 如果资源都是相对链接,jQuery 老项目的资源加载问题 这个方案能否解决,如果还不行,那就得慎重。

      • jQuery 的多页应用,优先考虑 iframe

  2. js/css 污染是否难以解决

    • 模块化之后,js 污染的问题几乎很少了,并且 js 沙箱比较完美,即使有问题,也是插件之间的插件冲突,比较好解决

    • css 是否会污染 ,这个没有一个很好的标准判断,以 vue 为例,如果所有的组件样式都带上了 scoped,基本上避免了 99% 的污染,剩下的一些全局样式,如果都使用特殊的 class/id,那就完美,有没有沙箱都不会污染。理想很丰满,现实很残忍,如果项目全都是全局样式,那就要三思了。

如果上述两个问题都 OK,就可以尝试使用 qiankun

最后,没有十全十美的方案,有问题别慌,对症下药,逐个击破。只要肯花时间,iframe 也可以是一个完美的方案。

附录

我记录了自己从 iframesingle-spa,再到 qiankun 的技术选型历程,以及其中的一些坑,感兴趣可以了解下:

如果有什么问题或者错误,欢迎指出!

🏆 技术专题第四期 | 聊聊微前端的那些事......