跳到主要内容

认识

2024年03月20日
柏拉文
越努力,越幸运

一、认识


Qiankun 是基于 single-spa 理念打造的微前端框架,由蚂蚁金服团队推出。同 Single-Spa 一样, 目标是将一个大型前端项目拆分为多个相对独立的子应用,每个子应用可以独立开发、部署和运行,同时在一个统一的页面中协同工作。通过基于 single-spa 的思想, Qiankun 兼容了多种技术栈(如 ReactVueAngular 等)。Qiankun 重点在于实现 JavaScript 沙箱(sandbox)和样式隔离, 解决了单纯使用 Single-Spa 时可能出现的全局变量污染、样式冲突等问题。QianKunSingle-Spa 根据路由匹配微应用的基础上, 提供了手动加载微应用的功能, 可以更加灵活、动态的加载微应用。QianKun 支持 JsEntryHTML Entry。其中, HTML Entry 通过设置 HTML 作为资源入口,通过加载远程 html,解析其 DOM 结构从而获取 jscss 等静态资源来实现微前端的渲染。

二、特点


2.1 JS沙箱

2.2 样式隔离

2.3 数据通信

2.4 预加载

三、问题


3.1 认识 QianKun

Qiankun 是基于 single-spa 理念打造的微前端框架,由蚂蚁金服团队推出。同 Single-Spa 一样, 目标是将一个大型前端项目拆分为多个相对独立的子应用,每个子应用可以独立开发、部署和运行,同时在一个统一的页面中协同工作。通过基于 single-spa 的思想, Qiankun 兼容了多种技术栈(如 ReactVueAngular 等)。Qiankun 重点在于实现 JavaScript 沙箱(sandbox)和样式隔离, 解决了单纯使用 Single-Spa 时可能出现的全局变量污染、样式冲突等问题。QianKunSingle-Spa 根据路由匹配微应用的基础上, 提供了手动加载微应用的功能, 可以更加灵活、动态的加载微应用。QianKun 支持 JsEntryHTML Entry。其中, HTML Entry 通过设置 HTML 作为资源入口,通过加载远程 html,解析其 DOM 结构从而获取 jscss 等静态资源来实现微前端的渲染。

3.2 QianKun 是如何实现的?

一、注册、加载微应用: Qiankun 支持基于 registerApplication 注册各个子应用, 通过匹配路由来加载子应用; QianKun 也提供了基于 loadMicroApp 更加灵活的、动态加载、挂载和卸载微前端应用。

  1. registerApplication 注册各个子应用: 统一管理多个子应用。通过接口(如 registerApplication)注册各个子应用, 注册后, Qiankun 会根据路由变化决定哪些子应用需要加载、挂载或者卸载。配置项通常包括: name, 微应用的名称,用于标识和日志记录; entry, 微应用入口 URL,指向包含微应用资源(HTMLJSCSS)的页面; container, 微应用挂载的 DOM 容器,loadMicroApp 会将微应用的内容插入到这里; props, 传递给微应用的自定义参数,如全局状态、路由参数等; activeRule(可选), 虽然手动调用时可能不需要,但也可以指定激活规则; 在 loadMicroApp 的入口函数中,首先对传入的配置进行校验、归一化,确保各项参数符合预期。

  2. loadMicroApp 加灵活的、动态加载、挂载和卸载微前端应用: loadMicroApp 接收一个配置对象,其中通常包括: name, 微应用的名称,用于标识和日志记录; entry, 微应用入口 URL,指向包含微应用资源(HTMLJSCSS)的页面; container, 微应用挂载的 DOM 容器,loadMicroApp 会将微应用的内容插入到这里; props, 传递给微应用的自定义参数,如全局状态、路由参数等; activeRule(可选), 虽然手动调用时可能不需要,但也可以指定激活规则; 在 loadMicroApp 的入口函数中,首先对传入的配置进行校验、归一化,确保各项参数符合预期。

二、远程资源加载: 通过 registerApplication 注册多个子应用时, Qiankun 在子应用激活时动态加载资源。而 loadMicroApp 加载子应用时, 子应用就已经激活了。因此, QianKun 加载子应用资源的逻辑为:

  1. HTML 解析: 加载子应用的入口 HTML,并解析其中的 <script><link> 等标签,从而获得子应用的所有资源。

  2. 资源插入: 将解析后的 JS 脚本、Css 样式动态插入页面,使得子应用代码得以执行。

  3. 异步处理: 整个加载过程采用 Promise 链式调用,确保子应用的资源加载和初始化按顺序完成。

三、生命周期管理: 每个子应用必须实现标准生命周期方法: bootstrap, 应用初始化,只调用一次; mount, 将应用挂载到页面上,渲染 UI; unmount, 卸载应用,清理资源; Qiankun 激活子应用时, 根据激活规则调用相应的生命周期方法,实现子应用的动态加载与卸载。

四、沙箱隔离: 为避免子应用之间的全局污染,Qiankun 实现了一套沙箱机制: 1. 基于 Proxy 的沙箱, 利用 ES6 Proxy 技术,拦截对子应用全局变量(如 window)的读写操作,使得子应用对全局的修改局限于沙箱对象内。2. 快照与恢复, 在子应用卸载时,可以将全局状态还原到子应用加载前的状态,从而避免污染主应用和其他子应用。此外,Qiankun 还通过 CSS 隔离技术(如动态添加样式前缀或使用 Shadow DOM)保证样式不会冲突。

五、错误边界与协同机制: Qiankun 为各个生命周期阶段增加错误边界,确保某个子应用发生异常时不会影响整体页面。子应用之间可以通过全局事件、共享状态管理或基于 props 的方式进行通信,而这些通信机制通常也会在沙箱隔离的基础上进行协调。

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

作用域前缀 隔离方案: 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 QianKun JS 沙箱隔离是如何实现的?

快照与恢复沙箱: 为了应对某些不容易被 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);`
}

3.5 QianKun 相比于 Single-Spa 有什么优势?

Singles-SPA 基于路由匹配的方式, 通过监听 url change 事件,在路由变化时匹配到渲染的子应用并进行渲染。Single-spa 并不内置完善的隔离机制, 它的重点在于调度和管理微应用的加载、卸载和生命周期。而且, 只提供了 JS-Entry 的方式接入微应用, 需要将微应用整个打包成一个 JS 文件,发布到静态资源服务器,然后在主应用中配置该 JS 文件的地址告诉 single-spa 去这个地址加载微应用, 不能使用一些原有的构建优化, 比如: 比如:按需加载、首屏资源加载优化、css 独立打包等优化措施。而且, Single-SPA 没有提供灵活的主子应用通信、子应用预加载等。

Qiankun 是基于 single-spa 理念打造的微前端框架,由蚂蚁金服团队推出。同 Single-Spa 一样, 目标是将一个大型前端项目拆分为多个相对独立的子应用,每个子应用可以独立开发、部署和运行,同时在一个统一的页面中协同工作。通过基于 single-spa 的思想, Qiankun 兼容了多种技术栈(如 ReactVueAngular 等)。Qiankun 重点在于实现 JavaScript 沙箱(sandbox)和样式隔离, 解决了单纯使用 Single-Spa 时可能出现的全局变量污染、样式冲突等问题。QianKunSingle-Spa 根据路由匹配微应用的基础上, 提供了手动加载微应用的功能, 可以更加灵活、动态的加载微应用。而且, QianKun 支持 JsEntryHTML Entry。其中, HTML Entry 通过设置 HTML 作为资源入口,通过加载远程 html,解析其 DOM 结构从而获取 jscss 等静态资源来实现微前端的渲染。QianKun 通过发布订阅模式来实现应用间通信,状态由框架来统一维护,每个应用在初始化时由框架生成一套通信方法,应用通过这些方法来更改全局状态和注册回调函数,全局状态发生改变时触发各个应用注册的回调函数执行,将新旧状态传递到所有应用

扩展: 如果 Webpack 做了 LazyLoadSplitChunks 等代码分割的优化策略, 我们还提供了一个 dependency-resource-plugin 插件, 专门用于获取每一个 entry 依赖的 jscsshtml 资源。jscss 依赖信息用于给到 HTML Entry 中。或者直接加载 .html 模版就可以。

3.6 QianKun 体系下, 如何解决多个子应用的样式冲突呢?

QianKun 目前提供了 Css Scoped 作用域样式隔离 以及 Shadow DOM 样式隔离。这两种方案在当时及其不灵活, 会发生一些奇怪的问题, 比如弹窗的样式, Over 层没有全局覆盖。

因此, 我们建议子应用接入方使用中心化治理平台配置的 业务线唯一 Id 作为一个 Css 样式前缀, 可以通过 postcss-prefix-selector 这样的 PostCss 插件 增加 Css 前缀。这样, 我们关闭 QianKun 自己的样式隔离, 自己来实现隔离。

3.7 QianKun 体系下, 如何解决多个子应用依赖共享的问题?

一、通过 WebpackRsPackVite 构建时, 将公共依赖设置为 external, 通过 webpack 配置, 将公共依赖设置为 external, 使得这些库不被打包到子应用中, 而是由主应用统一加载(通常通过 CDN 或在主应用中全局引入)。这样, 所有子应用在运行时都能访问同一份依赖, 既减小了 bundle 大小, 也保证了依赖版本的一致性。

二、基于 Node BFF 层, 在获取子应用 HTML 模版时, 将公共依赖标签去除

当然, 如果需要更加细粒度的来实现依赖共享, 我们可以采用 Webpack 5.x 或者 Vite 的模块联邦。Module Federation 允许各子应用在运行时共享依赖, 实现真正的动态加载和依赖版本对齐。但是 模版联邦 是有问题的, 它有构建工具息息相关, 无法做到 Webpack 共享的模块、组件Vite 来使用。这样, 脱离了我们重构的目的是让接入方方便、快捷的配置中心化治理平台, 来快速接入青桔工作台。对于一些个别共享的依赖, 我们允许重复加载。

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

在所有微前端方案中,Iframe 是最稳定的、上手难度最低的,但它有一些无法解决的问题,例如性能低、通信复杂、双滚动条、弹窗无法全局覆盖,它的成长性不高,只适合简单的页面渲染。Iframe 最大的特性就是提供了浏览器原生的硬隔离方案,无论是样式隔离还是JS隔离统统都能被完美的解决。但它最大的问题也在于Iframe的隔离性无法突破,导致应用间上下文无法被共享,随之带来了一些开发体验、产品体验的问题。

  1. URL 不同步。浏览器刷新 Iframe URL 状态丢失、后退前进按钮无法使用。解决URL不同步: Iframe 子应用切换路由时, 将路由的变化同步给父应用, 父应用通过 replace 同步改变浏览器地址栏即可

  2. UI 不同步,DOM 结构不共享。想象一下屏幕右下角 1/4iframe 里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器 resize 时自动居中

  3. 全局上下文完全隔离,内存变量不共享。Iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果

  4. 每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程。 Iframe 通常会带来额外的渲染和内存开销。

Qiankun 加载速度很快, 基于动态加载和资源共享, 不需要为每个子应用创建独立的浏览器进程。基座应用和子应用本质是同一个页面, 切换、通信和样式管理都更高效, 不会出现 iframe 间的边框、滚动条等 UI 问题。而且, Qiankun 允许主应用和子应用共享公共依赖,避免了重复加载资源的问题,而 iframe 则每个独立加载,浪费带宽和内存。

3.9 QianKun 在重构 Iframe 平台的过程中, 遇到了呢些问题呢?

影响最深刻的有两个, 问题如下:

问题一: A 页面中加载了腾讯地图、高德地图, 并且 A 页面通过 QianKun 的方式加载, 但是打开页面后发现相关 map.qq.com 的请求都出现跨域拦截的情况, 导致地图加载不出来。原因: qiankun 会将微应用的动态 script 加载(例如 JSONP)转化为 fetch 请求, 因此需要相应的后端服务支持跨域, 否则会导致错误。解决: 在单实例模式下, 你可以使用 excludeAssetFilter 参数来放行这部分资源请求, 但是注意, 被该选项放行的资源会逃逸出沙箱,由此带来的副作用需要你自行处理。扩展, QianKun 加载子应用资源的逻辑为: 1. HTML 解析: 加载子应用的入口 HTML,并解析其中的 <script><link> 等标签,从而获得子应用的所有资源; 2. 资源插入: 将解析后的 JS 脚本、Css 样式动态插入页面,使得子应用代码得以执行。

问题二: 微应用路由模式问题, 微应用 A 基于 React , 使用了 HashRouter , 如果在多 tab 模式下, 同样加载了 ReactHashRouterB 应用, 这时候 B 页面内部跳转路由时, B 页面的内容渲染到了 A 页面中, 造成渲染混乱的问题。原因, HashRouter 是基于全局的 URL hash 来进行路由管理的, 在 QianKun 微前端架构下, 如果多个微应用同时使用 HashRouter, 它们会共享同一个全局的 hash 状态, 当微应用 B 内部发生路由跳转时, 会触发全局的 hashchange 事件, 这个事件会被所有使用 HashRouter 的应用监听到, 从而导致微应用 A 的路由也随之发生变化, 错误地渲染了 B 应用的内容, 从而造成页面混乱。因此, 我们当时的解决方案是: 使用 BrowserRouterbasename, 为每个微应用设置不同的路由前缀, 来实现路由隔离。

3.10 QianKun 子应用开启样式隔离后, 如何解决 Dialog Over 层无法全局覆盖的问题?

参考资料


晒兜斯

微前端问题汇总

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