MicroApp 是一款基于类 WebComponent 进行渲染的微前端框架,不同于目前流行的开源框架,它从组件化的思维实现微前端,旨在降低上手难度、提升工作效率。它是目前市面上接入微前端成本最低的框架,并且提供了 JS 沙箱、样式隔离、元素隔离、预加载、资源地址补全、插件系统、数据通信等一系列完善的功能。MicroApp 与技术栈无关,也不和业务绑定,可以用于任何前端框架和业务。
本篇文章中我们会从业务背景、实现思路介绍 MicroApp,也会详细介绍它的使用方式和技术原理。
随着这些年互联网的飞速发展,很多企业的 web 应用在持续迭代中功能越来越复杂,参与的人员、团队不断增多,导致项目出现难以维护的问题,这种情况 PC 端尤其常见,许多研发团队也在找寻一种高效管理复杂应用的方案,于是微前端被提及的越来越频繁。
微前端并不是一项新的技术,而是一种架构理念,它将单一的 web 应用拆解成多个可以独立开发、独立运行、独立部署的小型应用,并将它们整合为一个应用。
在实际业务中,我们也遇到同样的问题,并且在不同的业务场景下尝试了各种解决方案,如 iframe 、npm 包、微前端框架,并对各种方案的优劣进行了对比。
iframe:在所有微前端方案中,iframe 是最稳定的、上手难度最低的,但它有一些无法解决的问题,例如性能低、通信复杂、双滚动条、弹窗无法全局覆盖,它的成长性不高,只适合简单的页面渲染。
npm 包:将子应用封装成 npm 包,通过组件的方式引入,在性能和兼容性上是最优的方案,但却有一个致命的问题就是版本更新,每次版本发布需要通知接入方同步更新,管理非常困难。
微前端框架:流行的微前端框架有 single-spa 和 qiankun,它们将维护成本和功能上达到一种平衡,是目前实现微前端备受推崇的方案。
由于 iframe 和 npm 包存在问题理论上无法解决,在最初我们采用 qiankun 作为解决方案,qiankun 是在 single-spa 基础上进行了封装,提供了 js 沙箱、样式隔离、预加载等功能,并且与技术栈无关,可以兼容不同的框架。
qiankun 虽然优秀,但依然无法满足我们的预期。第一个问题是在我们实际使用场景中,每个接入微前端的项目运行已久,且每个项目由不同的人员和团队负责,如何降低对源代码的侵入性,减少代码修改和沟通成本,这是我们非常关心的点,所以我们需要一种比 qiankun 接入成本更小的方案。第二个问题是在多方应用接入的情况下,沙箱并不能完美规避所有问题,但 qiankun 处理此类不可预料的问题的能力并不是非常高效。在不停的摸索中,我们找到一种极致简洁的实现思路,它像使用组件一样简单,只修改一点点代码就可以接入微前端,并且还提供插件系统,赋予开发者灵活处理问题的能力。
微前端分为主应用和子应用,主应用也称为基座应用,是其它应用的容器载体,子应用则是被嵌入方。我们分别从主应用和子应用的角度出发,探寻一种更简洁和有效的接入微前端的方式。
在 single-spa 和 qiankun 中都是通过监听 url change 事件,在路由变化时匹配到渲染的子应用并进行渲染。这种基于路由监听渲染是 single-spa 最早实现的,作为出现最早、最有影响力的微前端框架,single-spa 被很多框架和公司借鉴,也导致目前实现的微前端的方式大多是基于路由监听。
同时 single-spa 要求子应用修改渲染逻辑并暴露出三个方法:bootstrap 、mount 、unmount,分别对应初始化、渲染和卸载,这也导致子应用需要对入口文件进行修改。这个特点也被 qiankun 继承下来,并且需要对 webpack 配置进行一些修改。
基于路由监听的实现方式和对子应用入口文件以及 webpack 配置的修改是必须的吗?
其实并不是,微前端的核心在于资源加载与渲染,iframe 的渲染方式就是一个典型,只要能够实现一种元素隔离的功能并且路由符合要求,子应用理论上不需要修改代码就可以嵌入另外一个页面渲染,我们试图从这个角度中找到不一样的实现思路。
要想简化微前端的实现步骤,必须摒弃旧的实现思路,探索出不同的道路。
我们借鉴了 WebComponent 的思想,以此为基础推出另一种更加组件化的实现方式:类 WebComponent + HTML Entry 。
HTML Entry:是指设置 html 作为资源入口,通过加载远程 html,解析其 DOM 结构从而获取 js 、css 等静态资源来实现微前端的渲染,这也是 qiankun 目前采用的渲染方案。
WebComponent:web 原生组件,它有两个核心组成部分:CustomElement 和 ShadowDom 。CustomElement 用于创建自定义标签,ShadowDom 用于创建阴影 DOM,阴影 DOM 具有天然的样式隔离和元素隔离属性。由于 WebComponent 是原生组件,它可以在任何框架中使用,理论上是实现微前端最优的方案。但 WebComponent 有一个无法解决的问题 - ShadowDom 的兼容性非常不好,一些前端框架在 ShadowDom 环境下无法正常运行,尤其是 react 框架。
类 WebComponent:就是使用 CustomElement 结合自定义的 ShadowDom 实现 WebComponent 基本一致的功能。
由于 ShadowDom 存在的问题,我们采用自定义的样式隔离和元素隔离实现 ShadowDom 类似的功能,然后将微前端应用封装在一个 CustomElement 中,从而模拟实现了一个类 WebComponent 组件,它的使用方式和兼容性与 WebComponent 一致,同时也避开了 ShadowDom 的问题。并且由于自定义 ShadowDom 的隔离特性,Micro App 不需要像 single-spa 和 qiankun 一样要求子应用修改渲染逻辑并暴露出方法,也不需要修改 webpack 配置。
我们通过上述方案封装了一个自定义标签micro-app
,它的渲染机制和功能与 WebComponent 类似,开发者可以像使用 web 组件一样接入微前端。它可以兼容任何框架,在使用方式和数据通信上也更加组件化,这显著降低了基座应用的接入成本,并且由于元素隔离的属性,子应用的改动量也大大降低。
接下来我们将分别介绍主应用和子应用的接入方式。
以 react 代码举例
每个自定义标签micro-app
渲染后就是一个微前端的子应用,它的使用方式类似于 iframe 标签。
我们需要给标签传递三个基础属性:
使用方式如下:
如果子应用只有一个页面,没有路由配置,则不需要做任何修改。
如果子应用是多页面,只需要修改路由配置,添加路由前缀。
如下:
window.__MICRO_APP_BASE_URL__是由基座应用下发的路由前缀,在非微前端环境下,这个值为 undefined
完成以上配置即可实现微前端的渲染,对源码的改动量很少。当然 MicroApp 还提供了其它一些能力,如插件系统、数据通信,我们接下来做详细介绍。
MicroApp 的核心功能在 CustomElement 基础上进行构建,CustomElement 用于创建自定义标签,并提供了元素的渲染、卸载、属性修改等钩子函数,我们通过钩子函数获知微应用的渲染时机,并将自定义标签作为容器,微应用的所有元素和样式作用域都无法逃离容器边界,从而形成一个封闭的环境。
通过自定义元素micro-app
的生命周期函数connectedCallback
监听元素被渲染,加载子应用的 html 并转换为 DOM 结构,递归查询所有 js 和 css 等静态资源并加载,设置元素隔离,拦截所有动态创建的 script 、link 等标签,提取标签内容。将加载的 js 经过插件系统处理后放入沙箱中运行,对 css 资源进行样式隔离,最后将格式化后的元素放入micro-app
中,最终将micro-app
元素渲染为一个微前端的子应用。在渲染的过程中,会执行开发者绑定的生命周期函数,用于进一步操作。
元素隔离源于 ShadowDom 的概念,即 ShadowDom 中的元素可以和外部的元素重复但不会冲突,ShadowDom 只能对自己内部的元素进行操作。
MicroApp 模拟实现了类似的功能,我们拦截了底层原型链上元素的方法,保证子应用只能对自己内部的元素进行操作,每个子应用都有自己的元素作用域。
元素隔离可以有效的防止子应用对基座应用和其它子应用元素的误操作,常见的场景是多个应用的根元素都使用相同的 id,元素隔离可以保证子应用的渲染框架能够正确找到自己的根元素。
如上图所示,micro-app
元素内部渲染的就是一个子应用,它还有两个自定义元素 micro-app-head
、micro-app-body
,这两个元素的作用分别对应 html 中的 head 和 body 元素。子应用在原 head 元素中的内容和一些动态创建并插入 head 的 link 、script 元素都会移动到micro-app-head
中,在原 body 元素中的内容和一些动态创建并插入 body 的元素都会移动到micro-app-body
中。这样可以防止子应用的元素泄漏到全局,在进行元素查询、删除等操作时,只需要在micro-app
内部进行处理,是实现元素隔离的重要基础。
可以将micro-app
理解为一个内嵌的 html 页面,它的结构和功能都和 html 页面类似。
微前端的使用场景非常复杂,即便有沙箱机制也无法避免所有的问题,所以我们提供了一套插件系统用于解决一些无法预知的问题。
插件可以理解为符合特定规则的对象,对象中提供一个函数用于对资源进行处理,插件通常由开发者自定义。
插件系统的作用是对传入的静态资源进行初步处理,并依次调用符合条件的插件,将初步处理后的静态资源作为参数传入插件,由插件对资源内容进一步的修改,并将修改后的内容返回。插件系统赋予开发者灵活处理静态资源的能力,对有问题的资源文件进行修改。
插件系统本身是纯净的,不会对资源内容造成影响,它的作用是统筹各个插件如何执行,当开发者没有设置插件时,则传入和传出的内容是一致的。
js 沙箱通过 Proxy 代理子应用的全局对象,防止应用之间全局变量的冲突,记录或清空子应用的全局副作用函数,也可以向子应用注入全局变量用于定制化处理。
样式隔离是指对子应用的 link 和 style 元素的 css 内容进行格式化处理,确保子应用的样式只作用域自身,无法影响外部。
MicroApp 借鉴了 qiankun 的 js 沙箱和样式隔离方案,这也是目前应用广泛且成熟的方案。
MicroApp 提供了预加载子应用的功能,它是基于 requestIdleCallback 实现的,预加载不会对基座应用和其它子应用的渲染速度造成影响,它会在浏览器空闲时间加载应用的静态资源,在应用真正被渲染时直接从缓存中获取资源并渲染。
微前端中经常出现资源丢失的现象,原因是基座应用将子应用的资源加载到自己的页面渲染,如果子应用的静态资源地址是相对地址,浏览器会以基座应用所在域名地址补全静态资源,从而导致资源丢失。
资源地址补全就是将子应用静态资源的相对地址补全为绝对地址,保证地址指向正确的资源路径,这种操作类似于 webpack 在运行时设置 publicPath 。
在微应用渲染时,micro-app
元素在不同渲染阶段会触发相应的生命周期事件,基座应用可以通过监听事件来进行相应的操作。
生命周期列表:
在卸载时,子应用也会接收到一个卸载的事件,用于执行卸载相关操作。
数据通信是微前端中非常重要的功能,实现数据通信的技术方案很多,优秀的方案可以提升开发效率,减少试错成本。我们也研究了 qiankun 等微前端框架数据通信的方式,但他们的实现方式并不适合我们,我们尝试直接通过元素属性传递复杂数据的形式实现数据通信。
对于前端研发人员最熟悉的是组件化的数据交互的方式,而自定义元素 micro-app 作为类 WebComponent,通过组件属性进行数据交互必然是最优的方式。但 MicroApp 在数据通信中遇到的最大的问题是自定义元素无法支持设置对象类型属性,例如<micro-app data={x: 1}></micro-app>
会转换为 <micro-app data='[object Object]'></micro-app>
,想要以组件化形式进行数据通信必须让元素支持对象属性。
为了解决这个问题,我们重写了micro-app
元素原型链上属性设置的方法,在micro-app
元素设置对象属性时将传递的值保存到数据中心,通过数据中心将值分发给子应用。
MicroApp 中数据是绑定通信的,即每个micro-app
元素只能与自己指向的子应用进行通信,这样每个应用都有着清晰的数据链,可以避免数据的混乱,同时 MicroApp 也支持全局通信,以便跨应用传递数据。
为了更直观的感受 Micro App 和其它框架的区别,我们使用一张图进行对比。
MicroApp 已经在公司内部多个项目中使用,表现良好,尤其是将一些老项目改造成微前端,在项目不受影响的情况下,即降低接入成本,又可以保证项目平稳运行,减小耦合。
当初我们团队打算使用微前端时,调研了市面上实现微前端的框架,可供选择的只有 sigle-spa 和 qiankun 。single-spa 太过于基础,对原有项目的改造过多,成本太高。剩下的只有 qiankun,但因为接入很多老项目,在实际使用中出了很多问题,我们不得不对 qiankun 的源码进行大量的魔改。在此过程中,我们对微前端的实现方式产生了一些自己的想法,并将这些想法付诸实践,于是有了 MicroApp 。
目前像 qiankun 类似提供完善功能的微前端框架太少了,当接入 qiankun 失败时,没有其他方案可供选择,这是我们当初经历过的痛。所以我们选择将 MicroApp 开源,一是因为 MicroApp 有诸多创新点,可以更简单的接入微前端,功能更加丰富,二是可以让大家多一种选择,没有完美的微前端框架,只有选择多了,才知道哪一个更适合自己。
如果你对这个项目感兴趣,可以通过加入组织或提 pull requests 的方式参与共建,非常欢迎与期待你的加入。
GitHub 地址: https://github.com/micro-zoe/micro-app
官网地址: https://cangdu.org/micro-app
特别鸣谢:qiankun
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.