跳到主要内容

认识

2023年08月15日
柏拉文
越努力,越幸运

一、认识


MicroApp 一款轻量、高效、功能强大的微前端框架。

MicroApp 借鉴了 WebComponent 的思想,通过 CustomElement 结合自定义的 ShadowDom,将微前端封装成一个类 WebComponent 组件,并且基于 HTML Entry 设置 HTML 作为资源入口,通过加载远程 html,解析其 DOM 结构从而获取 jscss 等静态资源实现微前端的组件化渲染。在此基础上,通过实现 JS隔离、样式隔离、路由隔离,降低子应用的接入成本,子应用只需设置允许跨域请求,不需要改动任何代码即可接入微前端,使用方式和 iframe 几乎一致,但却没有 iframe 存在的问题。Micro-app不需要像 Single-SpaQianKun 一样要求子应用修改渲染逻辑并暴露出方法,也不需要修改Webpack配置,是目前市面上接入微前端成本最低的方案。

  • HTML Entry: 是指设置html作为资源入口,通过加载远程html,解析其DOM结构从而获取jscss等静态资源来实现微前端的渲染,这也是qiankun目前采用的渲染方案。

  • WebComponent: web 原生组件,它有两个核心组成部分: CustomElement和ShadowDomCustomElement用于创建自定义标签,ShadowDom用于创建阴影DOM,阴影DOM具有天然的样式隔离和元素隔离属性。由于WebComponent是原生组件,它可以在任何框架中使用,理论上是实现微前端最优的方案。但WebComponent有一个无法解决的问题 - ShadowDom的兼容性非常不好,一些前端框架在ShadowDom环境下无法正常运行,尤其是react框架。

  • WebComponent: 就是使用 CustomElement 结合自定义的 ShadowDom 实现 WebComponent 基本一致的功能。

二、特点


2.1 JS沙箱

2.2 样式隔离

2.3 元素隔离

2.4 数据通信

2.5 插件系统

2.6 预加载

2.7 生命周期

2.8 资源地址补全

2.9 零依赖

micro-app 没有任何依赖,这赋予它小巧的体积和更高的扩展性。

三、问题


3.1 认识 MicroApp

MicroApp 借鉴了 WebComponent 的思想,通过 CustomElement 结合自定义的 ShadowDom,将微前端封装成一个类 WebComponent 组件,实现微前端的组件化渲染。在此基础上,通过实现 JS隔离、样式隔离、路由隔离,降低子应用的接入成本,子应用只需设置允许跨域请求,不需要改动任何代码即可接入微前端,使用方式和 iframe 几乎一致,但却没有iframe存在的问题。Micro-app不需要像 Single-SpaQianKun 一样要求子应用修改渲染逻辑并暴露出方法,也不需要修改Webpack配置,是目前市面上接入微前端成本最低的方案。

3.2 MicroApp 是如何实现的?

我们借鉴了 WebComponent 的思想,以此为基础推出另一种更加组件化的实现方式: WebComponent + HTML Entry

  • HTML Entry: 是指设置html作为资源入口,通过加载远程html,解析其DOM结构从而获取jscss等静态资源来实现微前端的渲染,这也是qiankun目前采用的渲染方案。

  • WebComponent: web 原生组件,它有两个核心组成部分: CustomElement和ShadowDomCustomElement用于创建自定义标签,ShadowDom用于创建阴影DOM,阴影DOM具有天然的样式隔离和元素隔离属性。由于WebComponent是原生组件,它可以在任何框架中使用,理论上是实现微前端最优的方案。但WebComponent有一个无法解决的问题 - ShadowDom的兼容性非常不好,一些前端框架在ShadowDom环境下无法正常运行,尤其是react框架。

  • WebComponent: 就是使用 CustomElement 结合自定义的 ShadowDom 实现 WebComponent 基本一致的功能。

由于 ShadowDom 存在的问题,我们采用自定义的样式隔离和元素隔离实现 ShadowDom 类似的功能,然后将微前端应用封装在一个 CustomElement 中,从而模拟实现了一个类 WebComponent 组件,它的使用方式和兼容性与 WebComponent 一致,同时也避开了 ShadowDom 的问题。并且由于自定义 ShadowDom 的隔离特性,Micro App不需要像 single-spaqiankun 一样要求子应用修改渲染逻辑并暴露出方法,也不需要修改 webpack 配置。MicroApp 的核心功能在CustomElement基础上进行构建,CustomElement用于创建自定义标签 micro-app,并提供了元素的渲染、卸载、属性修改等钩子函数,我们通过钩子函数获知微应用的渲染时机,并将自定义标签作为容器,微应用的所有元素和样式作用域都无法逃离容器边界,从而形成一个封闭的环境。

渲染流程: 通过自定义元素 micro-app 的生命周期函数 connectedCallback 监听元素被渲染,加载子应用的 html 并转换为 DOM 结构,递归查询所有 jscss等静态资源并加载,设置元素隔离,拦截所有动态创建的 scriptlink等标签,提取标签内容。将加载的 js 经过插件系统处理后放入沙箱中运行,对 css 资源进行样式隔离,最后将格式化后的元素放入 micro-app 中,最终将 micro-app 元素渲染为一个微前端的子应用。在渲染的过程中,会执行开发者绑定的生命周期函数,用于进一步操作。

元素隔离: 元素隔离源于 ShadowDom 的概念,即 ShadowDom 中的元素可以和外部的元素重复但不会冲突,ShadowDom只能对自己内部的元素进行操作。MicroApp 模拟实现了类似的功能,我们拦截了底层原型链上元素的方法,保证子应用只能对自己内部的元素进行操作,每个子应用都有自己的元素作用域。元素隔离可以有效的防止子应用对基座应用和其它子应用元素的误操作,常见的场景是多个应用的根元素都使用相同的id,元素隔离可以保证子应用的渲染框架能够正确找到自己的根元素。micro-app 元素内部渲染的就是一个子应用,它还有两个自定义元素 micro-app-headmicro-app-body,这两个元素的作用分别对应html中的headbody元素。子应用在原head元素中的内容和一些动态创建并插入headlinkscript元素都会移动到micro-app-head中,在原body元素中的内容和一些动态创建并插入body的元素都会移动到micro-app-body中。这样可以防止子应用的元素泄漏到全局,在进行元素查询、删除等操作时,只需要在micro-app内部进行处理,是实现元素隔离的重要基础。可以将 micro-app 理解为一个内嵌的html页面,它的结构和功能都和html页面类似。初次之外, MicroApp 还进行了 JS 沙箱隔离样式隔离全局事件重写 等。

micro-app 自定义元素实现如下

// 自定义元素
class MyElement extends HTMLElement {
// 声明需要监听的属性名,只有这些属性变化时才会触发attributeChangedCallback
static get observedAttributes () {
return ['name', 'url']
}

constructor() {
super();
}

connectedCallback() {
// 元素被插入到DOM时执行,此时去加载子应用的静态资源并渲染
console.log('micro-app is connected')
}

disconnectedCallback () {
// 元素从DOM中删除时执行,此时进行一些卸载操作
console.log('micro-app has disconnected')
}

attributeChangedCallback (attr, oldVal, newVal) {
// 元素属性发生变化时执行,可以获取name、url等属性的值
console.log(`attribute ${attrName}: ${newVal}`)
}
}

/**
* 注册元素
* 注册后,就可以像普通元素一样使用micro-app,当micro-app元素被插入或删除DOM时即可触发相应的生命周期函数。
*/
window.customElements.define('micro-app', MyElement)

3.3 MicroApp 样式隔离是如何实现的?

MicroApp 借鉴了 qiankunJS 沙箱和样式隔离方案,这也是目前应用广泛且成熟的方案。

作用域前缀 隔离方案: Qiankun 在加载子应用的过程中,会扫描子应用页面中所有的 <style> 标签和内联样式,通过解析 CSS 规则,对每个选择器添加一个唯一的属性前缀(例如一个自定义属性,如 data-qiankun="xxx"),使得样式仅匹配包含该属性的 DOM 元素。这样,即使全局样式发生冲突,也不会影响其他区域。

// 假设子应用的 DOM 容器为:
<div id="micro-app" data-qiankun-app="remoteApp"></div>

// 当子应用的 CSS 原来写作:

.button {
color: red;
}

// 经过 Qiankun 的样式重写后,实际生效的样式可能变为
[data-qiankun-app="remoteApp"] .button {
color: red;
}

// 这样,该样式只会对带有 data-qiankun-app="remoteApp" 的容器内的 `.button` 元素生效,避免了与全局样式的冲突。

Shadow DOM 隔离方案: 利用 Shadow DOM,可以将子应用的 DOM 封装在一个独立的影子树(shadow tree)中。影子树内的样式和 DOM 是隔离的,不会与主应用或其他子应用产生样式冲突。在这种模式下,Qiankun 会在子应用的容器上创建一个 Shadow Root,然后将子应用的内容挂载到这个 Shadow Root 内部,从而实现更强的样式和 DOM 隔离。

Qiankun 中,可以通过配置项(例如开启 experimentalStyleIsolation)启用样式隔离功能。开发者在编写子应用时,也应尽量避免全局污染。

3.4 MicroApp 沙箱隔离是如何实现的?

MicroApp 借鉴了 qiankunJS 沙箱和样式隔离方案,这也是目前应用广泛且成熟的方案。

快照与恢复沙箱: 为了应对某些不容易被 Proxy 捕捉的全局副作用,Qiankun 还支持在沙箱启动时对全局状态做快照,卸载时恢复。这种方式能够更彻底地还原全局环境,但成本较高,通常在严格隔离需求下使用。快照沙箱 SnapshotSandbox 顾名思义,即在某个阶段给当前的运行环境打一个快照,再在需要的时候把快照恢复,从而实现隔离。在创建微应用的时候会实例化一个沙盒对象,对于每一个子应用,运行时将其内部保存的上下文加载到对应的变量上,销毁时再将当前浏览器环境中各个变量的值保存到快照中。它有两个方法,active 是在激活微应用的时候执行,而 inactive 是在离开微应用的时候执行。整体的思路是在激活微应用时将当前的 window 对象拷贝存起来,然后从 modifyPropsMap 中恢复这个微应用上次修改的属性到 window 中。在离开微应用时会与原有的 window 对象做对比,将有修改的属性保存起来,以便再次进入这个微应用时进行数据恢复,然后把有修改的属性值恢复到以前的状态。

Proxy 代理沙箱: 在子应用加载时, 首先为沙箱构造一个伪全局对象 window, 它复制了真实全局对象中不可配置的属性,并对部分关键属性(如 topparentselfwindow)进行调整,使其在沙箱中可以被代理或修改, 将处理后的属性描述符冻结后定义到伪全局对象中。基于 Proxy 代理伪全局 windowget 拦截器 如果访问的 keywindowselfglobalThis, 返回代理自身, 从而确保沙箱内引用始终指向代理对象, 否则优先返回 fakeWindow 中的属性,如果不存在,再从全局 window 中取值。set 拦截器 只有当沙箱处于激活状态(isRunningtrue)时,才允许写入操作,将属性写入到 fakeWindow 中;否则拒绝写入。构造伪全局对象: 通过构造一个伪全局对象,只复制真实 window 中不可配置的属性, 使得子应用在 Proxy 沙箱中对全局属性的修改只影响伪全局对象,从而保护了真实的全局环境。让子应用在修改全局变量时不会直接污染真实的 window

无论是 快照与恢复沙箱 还是 Proxy 代理沙箱, 都需要通过 width 函数包裹, 修改 js 作用域,将子应用的 window 指向代理的对象。形式如:

(function(window, self) {
with(window) {
子应用的js代码
}
}).call(代理对象, 代理对象, 代理对象)

基于 with 函数, 提供 bindScope, 可以将 script 代码放入沙箱中运行

bindScope (code) {
window.proxyWindow = this.proxyWindow
return `;(function(window, self){with(window){;${code}\n}}).call(window.proxyWindow, window.proxyWindow, window.proxyWindow);`
}

另外, 我们在沙箱中重写 window.addEventListenerwindow.removeEventListener,记录所有全局监听事件,在应用卸载时如果有残余的全局监听事件则进行清空。

3.5 MicroApp 在使用过程中, 遇到了哪些问题?

一、Model 挂载问题: 如果基座应用向子应用传递事件回调, 子应用调用回调后, 基座应用打开弹窗。此时, Model 会挂载到子应用中。 解决方案:

  1. 通过消息通知, window.microApp.dispatch({type: '子应用发送给主应用的数据'})

  2. 解除元素绑定, removeDomScope(true), 通常用于受子应用元素绑定影响,导致主应用元素错误绑定到子应用的情况

二、多个子应用频繁切换, 隔离失效问题: 多个子应用频繁切换, Window 代理出现混乱的情况, Window 为上一个子应用的 Window解决方案: 在 micro.start 注册函数中, 配置 plugins.global.scopeProperties。 配置如下:

start({
plugins: {
scopeProperties: ["webpackChunkumu_ems"], // 找出每个子应用主产物中的全局变量, 例如 Webpack 为 webpackChunkxxx, 这个全局变量记录了 SplitChunk 和 LazyImport 信息。所以, 需要将所有子应用主产物中的全局变量进行隔离。
}
});

3.6 MicroApp 相比于 QianKun 作为微前端框架, 有什么优势?

MicroApp 借鉴了 WebComponent 的思想,通过 CustomElement 结合自定义的 ShadowDom,将微前端封装成一个类 WebComponent 组件,实现微前端的组件化渲染。在此基础上,通过实现 JS隔离、样式隔离、路由隔离,降低子应用的接入成本,子应用只需设置允许跨域请求,不需要改动任何代码即可接入微前端,使用方式和 iframe 几乎一致,但却没有 iframe 存在的问题。Micro-app不需要像 Single-SpaQianKun 一样要求子应用修改渲染逻辑并暴露出方法,也不需要修改Webpack配置,是目前市面上接入微前端成本最低的方案。MicroApp 封装了一个自定义标签micro-app,它的渲染机制和功能与WebComponent类似,开发者可以像使用web组件一样接入微前端。它可以兼容任何框架,在使用方式和数据通信上也更加组件化,这显著降低了基座应用的接入成本,并且由于元素隔离的属性,子应用的改动量也大大降低。

QianKun 要求每个子应用必须实现标准生命周期方法: bootstrap, 应用初始化,只调用一次; mount, 将应用挂载到页面上,渲染 UI; unmount, 卸载应用,清理资源; Qiankun 激活子应用时, 根据激活规则调用相应的生命周期方法,实现子应用的动态加载与卸载。并且, QianKun 可能还会对 Webpack 构建有影响。因此, QianKun 对源代码有侵入性, 而且有代码修改和沟通成本, 接入成本有点高。

参考资料


【微前端】在造一个微前端轮子之前,你需要知道这些~