认识
一、认识
Qiankun
是基于 single-spa
理念打造的微前端框架,由蚂蚁金服团队推出。同 Single-Spa
一样, 目标是将一个大型前端项目拆分为多个相对独立的子应用,每个子应用可以独立开发、部署和运行,同时在一个统一的页面中协同工作。通过基于 single-spa
的思想, Qiankun
兼容了多种技术栈(如 React
、Vue
、Angular
等)。Qiankun
重点在于实现 JavaScript
沙箱(sandbox
)和样式隔离, 解决了单纯使用 Single-Spa
时可能出现的全局变量污染、样式冲突等问题。QianKun
在 Single-Spa
根据路由匹配微应用的基础上, 提供了手动加载微应用的功能, 可以更加灵活、动态的加载微应用。QianKun
支持 JsEntry
和 HTML Entry
。其中, HTML Entry
通过设置 HTML
作为资源入口,通过加载远程 html
,解析其 DOM
结构从而获取 js
、css
等静态资源来实现微前端的渲染。
二、特点
2.1 JS沙箱
2.2 样式隔离
2.3 数据通信
2.4 预加载
三、问题
3.1 认识 QianKun
Qiankun
是基于 single-spa
理念打造的微前端框架,由蚂蚁金服团队推出。同 Single-Spa
一样, 目标是将一个大型前端项目拆分为多个相对独立的子应用,每个子应用可以独立开发、部署和运行,同时在一个统一的页面中协同工作。通过基于 single-spa
的思想, Qiankun
兼容了多种技术栈(如 React
、Vue
、Angular
等)。Qiankun
重点在于实现 JavaScript
沙箱(sandbox
)和样式隔离, 解决了单纯使用 Single-Spa
时可能出现的全局变量污染、样式冲突等问题。QianKun
在 Single-Spa
根据路由匹配微应用的基础上, 提供了手动加载微应用的功能, 可以更加灵活、动态的加载微应用。QianKun
支持 JsEntry
和 HTML Entry
。其中, HTML Entry
通过设置 HTML
作为资源入口,通过加载远程 html
,解析其 DOM
结构从而获取 js
、css
等静态资源来实现微前端的渲染。
3.2 QianKun 是如何实现的?
一、注册、加载微应用: Qiankun
支持基于 registerApplication
注册各个子应用, 通过匹配路由来加载子应用; QianKun
也提供了基于 loadMicroApp
更加灵活的、动态加载、挂载和卸载微前端应用。
-
registerApplication
注册各个子应用: 统一管理多个子应用。通过接口(如registerApplication
)注册各个子应用, 注册后,Qiankun
会根据路由变化决定哪些子应用需要加载、挂载或者卸载。配置项通常包括:name
, 微应用的名称,用于标识和日志记录;entry
, 微应用入口URL
,指向包含微应用资源(HTML
、JS
、CSS
)的页面;container
, 微应用挂载的DOM
容器,loadMicroApp
会将微应用的内容插入到这里;props
, 传递给微应用的自定义参数,如全局状态、路由参数等;activeRule
(可选), 虽然手动调用时可能不需要,但也可以指定激活规则; 在loadMicroApp
的入口函数中,首先对传入的配置进行校验、归一化,确保各项参数符合预期。 -
loadMicroApp
加灵活的、动态加载、挂载和卸载微前端应用:loadMicroApp
接收一个配置对象,其中通常包括:name
, 微应用的名称,用于标识和日志记录;entry
, 微应用入口URL
,指向包含微应用资源(HTML
、JS
、CSS
)的页面;container
, 微应用挂载的DOM
容器,loadMicroApp
会将微应用的内容插入到这里;props
, 传递给微应用的自定义参数,如全局状态、路由参数等;activeRule
(可选), 虽然手动调用时可能不需要,但也可以指定激活规则; 在loadMicroApp
的入口函数中,首先对传入的配置进行校验、归一化,确保各项参数符合预期。
二、远程资源加载: 通过 registerApplication
注册多个子应用时, Qiankun
在子应用激活时动态加载资源。而 loadMicroApp
加载子应用时, 子应用就已经激活了。因此, QianKun
加载子应用资源的逻辑为:
-
HTML
解析: 加载子应用的入口HTML
,并解析其中的<script>
、<link>
等标签,从而获得子应用的所有资源。 -
资源插入: 将解析后的
JS
脚本、Css
样式动态插入页面,使得子应用代码得以执行。 -
异步处理: 整个加载过程采用
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
, 它复制了真实全局对象中不可配置的属性,并对部分关键属性(如 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);`
}
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
兼容了多种技术栈(如 React
、Vue
、Angular
等)。Qiankun
重点在于实现 JavaScript
沙箱(sandbox
)和样式隔离, 解决了单纯使用 Single-Spa
时可能出现的全局变量污染、样式冲突等问题。QianKun
在 Single-Spa
根据路由匹配微应用的基础上, 提供了手动加载微应用的功能, 可以更加灵活、动态的加载微应用。而且, QianKun
支持 JsEntry
和 HTML Entry
。其中, HTML Entry
通过设置 HTML
作为资源入口,通过加载远程 html
,解析其 DOM
结构从而获取 js
、css
等静态资源来实现微前端的渲染。QianKun
通过发布订阅模式来实现应用间通信,状态由框架来统一维护,每个应用在初始化时由框架生成一套通信方法,应用通过这些方法来更改全局状态和注册回调函数,全局状态发生改变时触发各个应用注册的回调函数执行,将新旧状态传递到所有应用
扩展: 如果 Webpack
做了 LazyLoad
、SplitChunks
等代码分割的优化策略, 我们还提供了一个 dependency-resource-plugin
插件, 专门用于获取每一个 entry
依赖的 js
、 css
、html
资源。js
、css
依赖信息用于给到 HTML Entry
中。或者直接加载 .html
模版就可以。
3.6 QianKun 体系下, 如何解决多个子应用的样式冲突呢?
QianKun
目前提供了 Css Scoped
作用域样式隔离 以及 Shadow DOM
样式隔离。这两种方案在当时及其不灵活, 会发生一些奇怪的问题, 比如弹窗的样式, Over
层没有全局覆盖。
因此, 我们建议子应用接入方使用中心化治理平台配置的 业务线唯一 Id
作为一个 Css
样式前缀, 可以通过 postcss-prefix-selector
这样的 PostCss
插件 增加 Css
前缀。这样, 我们关闭 QianKun
自己的样式隔离, 自己来实现隔离。
3.7 QianKun 体系下, 如何解决多个子应用依赖共享的问题?
一、通过 Webpack
、RsPack
、Vite
构建时, 将公共依赖设置为 external
, 通过 webpack
配置, 将公共依赖设置为 external
, 使得这些库不被打包到子应用中, 而是由主应用统一加载(通常通过 CDN
或在主应用中全局引入)。这样, 所有子应用在运行时都能访问同一份依赖, 既减小了 bundle
大小, 也保证了依赖版本的一致性。
二、基于 Node BFF
层, 在获取子应用 HTML
模版时, 将公共依赖标签去除
当然, 如果需要更加细粒度的来实现依赖共享, 我们可以采用 Webpack 5.x
或者 Vite
的模块联邦。Module Federation
允许各子应用在运行时共享依赖, 实现真正的动态加载和依赖版本对齐。但是 模版联邦 是有问题的, 它有构建工具息息相关, 无法做到 Webpack
共享的模块、组件 让 Vite
来使用。这样, 脱离了我们重构的目的是让接入方方便、快捷的配置中心化治理平台, 来快速接入青桔工作台。对于一些个别共享的依赖, 我们允许重复加载。
3.8 QianKun 相比于 Iframe 作为微前端框架, 有什么优势?
在所有微前端方案中,Iframe
是最稳定的、上手难度最低的,但它有一些无法解决的问题,例如性能低、通信复杂、双滚动条、弹窗无法全局覆盖,它的成长性不高,只适合简单的页面渲染。Iframe
最大的特性就是提供了浏览器原生的硬隔离方案,无论是样式隔离还是JS
隔离统统都能被完美的解决。但它最大的问题也在于Iframe
的隔离性无法突破,导致应用间上下文无法被共享,随之带来了一些开发体验、产品体验的问题。
-
URL
不同步。浏览器刷新Iframe URL
状态丢失、后退前进按钮无法使用。解决URL
不同步:Iframe
子应用切换路由时, 将路由的变化同步给父应用, 父应用通过replace
同步改变浏览器地址栏即可 -
UI
不同步,DOM
结构不共享。想象一下屏幕右下角1/4
的iframe
里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器 resize 时自动居中 -
全局上下文完全隔离,内存变量不共享。
Iframe
内外系统的通信、数据同步等需求,主应用的cookie
要透传到根域名都不同的子应用中实现免登效果 -
每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程。
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
模式下, 同样加载了 React
、 HashRouter
的 B
应用, 这时候 B
页面内部跳转路由时, B
页面的内容渲染到了 A
页面中, 造成渲染混乱的问题。原因, HashRouter
是基于全局的 URL hash
来进行路由管理的, 在 QianKun
微前端架构下, 如果多个微应用同时使用 HashRouter
, 它们会共享同一个全局的 hash
状态, 当微应用 B
内部发生路由跳转时, 会触发全局的 hashchange
事件, 这个事件会被所有使用 HashRouter
的应用监听到, 从而导致微应用 A
的路由也随之发生变化, 错误地渲染了 B
应用的内容, 从而造成页面混乱。因此, 我们当时的解决方案是: 使用 BrowserRouter
的 basename
, 为每个微应用设置不同的路由前缀, 来实现路由隔离。