认识
一、认识
MicroApp 一款轻量、高效、功能强大的微前端框架。
MicroApp
借鉴了 WebComponent
的思想,通过 CustomElement
结合自定义的 ShadowDom
,将微前端封装成一个类 WebComponent
组件,并且基于 HTML Entry
设置 HTML
作为资源入口,通过加载远程 html
,解析其 DOM
结构从而获取 js
、css
等静态资源实现微前端的组件化渲染。在此基础上,通过实现 JS
隔离、样式隔离、路由隔离,降低子应用的接入成本,子应用只需设置允许跨域请求,不需要改动任何代码即可接入微前端,使用方式和 iframe
几乎一致,但却没有 iframe
存在的问题。Micro-app
不需要像 Single-Spa
和 QianKun
一样要求子应用修改渲染逻辑并暴露出方法,也不需要修改Webpack
配置,是目前市面上接入微前端成本最低的方案。
-
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
基本一致的功能。
二、特点
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-Spa
和 QianKun
一样要求子应用修改渲染逻辑并暴露出方法,也不需要修改Webpack
配置,是目前市面上接入微前端成本最低的方案。
3.2 MicroApp 是如何实现的?
我们借鉴了 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
配置。MicroApp
的核心功能在CustomElement
基础上进行构建,CustomElement
用于创建自定义标签 micro-app
,并提供了元素的渲染、卸载、属性修改等钩子函数,我们通过钩子函数获知微应用的渲染时机,并将自定义标签作为容器,微应用的所有元素和样式作用域都无法逃离容器边界,从而形成一个封闭的环境。
渲染流程: 通过自定义元素 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
页面类似。初次之外, 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
借鉴了 qiankun
的 JS
沙箱和样式隔离方案,这也是目前应用广泛且成熟的方案。
作用域前缀 隔离方案: 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
借鉴了 qiankun
的 JS
沙箱和样式隔离方案,这也是目前应用广泛且成熟的方案。
快照与恢复沙箱: 为了应对某些不容易被 Proxy
捕捉的全局副作用,Qiankun
还支持在沙箱启动时对全局状态做快照,卸载时恢复。这种方式能够更彻底地还原全局环境,但成本较高,通常在严格隔离需求下使用。快照沙箱 SnapshotSandbox
顾名思义,即在某个阶段给当前的运行环境打一个快照,再在需要的时候把快照恢复,从而实现隔离。在创建微应用的时候会实例化一个沙盒对象,对于每一个子应用,运行时将其内部保存的上下文加载到对应的变量上,销毁时再将当前浏览器环境中各个变量的值保存到快照中。它有两个方法,active
是在激活微应用的时候执行,而 inactive
是在离开微应用的时候执行。整体的思路是在激活微应用时将当前的 window
对象拷贝存起来,然后从 modifyPropsMap
中恢复这个微应用上次修改的属性到 window
中。在离开微应用时会与原有的 window
对象做对比,将有修改的属性保存起来,以便再次进入这个微应用时进行数据恢复,然后把有修改的属性值恢复到以前的状态。
Proxy
代理沙箱: 在子应用加载时, 首先为沙箱构造一个伪全局对象 window
, 它复制了真实全局对象中不可配置的属性,并对部分关键属性(如 top
、parent
、self
、window
)进行调整,使其在沙箱中可以被代理或修改, 将处理后的属性描述符冻结后定义到伪全局对象中。基于 Proxy
代理伪全局 window
。 get
拦截器 如果访问的 key
为 window
、self
或 globalThis
, 返回代理自身, 从而确保沙箱内引用始终指向代理对象, 否则优先返回 fakeWindow
中的属性,如果不存在,再从全局 window
中取值。set
拦截器 只有当沙箱处于激活状态(isRunning
为 true
)时,才允许写入操作,将属性写入到 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.addEventListener
和 window.removeEventListener
,记录所有全局监听事件,在应用卸载时如果有残余的全局监听事件则进行清空。
3.5 MicroApp 在使用过程中, 遇到了哪些问题?
一、Model
挂载问题: 如果基座应用向子应用传递事件回调, 子应用调用回调后, 基座应用打开弹窗。此时, Model
会挂载到子应用中。
解决方案:
-
通过消息通知,
window.microApp.dispatch({type: '子应用发送给主应用的数据'})
-
解除元素绑定,
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-Spa
和 QianKun
一样要求子应用修改渲染逻辑并暴露出方法,也不需要修改Webpack
配置,是目前市面上接入微前端成本最低的方案。MicroApp
封装了一个自定义标签micro-app
,它的渲染机制和功能与WebComponent
类似,开发者可以像使用web
组件一样接入微前端。它可以兼容任何框架,在使用方式和数据通信上也更加组件化,这显著降低了基座应用的接入成本,并且由于元素隔离的属性,子应用的改动量也大大降低。
QianKun
要求每个子应用必须实现标准生命周期方法: bootstrap
, 应用初始化,只调用一次; mount
, 将应用挂载到页面上,渲染 UI; unmount
, 卸载应用,清理资源; Qiankun
激活子应用时, 根据激活规则调用相应的生命周期方法,实现子应用的动态加载与卸载。并且, QianKun
可能还会对 Webpack
构建有影响。因此, QianKun
对源代码有侵入性, 而且有代码修改和沟通成本, 接入成本有点高。