记录---一篇文了解qiankun的代码隔离原理
<h1 data-id="heading-0">🧑💻 写在开头</h1><p>点赞 + 收藏 === 学会🤣🤣🤣</p>
<blockquote>
<p>随着前端业务的快速发展,微前端架构已经被广泛采用,其中 qiankun 作为主流解决方案也越来越受到关注。前几天面试时,我就被问到了一个高频问题:qiankun 是如何实现 JS 和 CSS 隔离的?</p>
</blockquote>
<div>
<div>
<h2 data-id="heading-0">qiankun 的JS 沙箱</h2>
<p>qiankun 的微前端场景是:<strong>主应用加载多个子应用</strong>,不同子应用可能依赖不同版本的库、全局变量,甚至可能会互相覆盖 <code>window</code> 上的属性。为了避免“全局污染”,qiankun 提供了沙箱机制。</p>
<p>常见的JS 沙箱实现思路有下面三种:</p>
<h3 data-id="heading-1">SnapshotSandbox(快照沙箱)</h3>
<p>快照沙箱是微前端里最直观的 JS 隔离方式之一:</p>
<ul>
<li><strong>挂载应用前</strong> → 对 <code>window</code> 对象做一次“快照”,保存所有属性及其值。</li>
<li><strong>应用运行中</strong> → 子应用可以随意修改全局变量。</li>
<li><strong>卸载应用时</strong> → 把 <code>window</code> 恢复到挂载前的快照状态(新增的删掉、改过的还原)。</li>
</ul>
<p>它的过程使用伪代码大致如下:</p>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">/**
* 快照沙箱
* - 挂载前:拍快照(浅拷贝 window 属性)
* - 卸载时:恢复快照(删除新增,还原修改)
*/
function createSnapshotSandbox() {
const rawWindow = window;
let snapshot = null; // 存储拍下来的全局状态
let modifiedProps = {}; // 存储运行过程中被修改的属性
return {
// 激活:拍下当前 window 状态
activate() {
snapshot = {};
for (const key in rawWindow) {
try {
snapshot = rawWindow;
} catch (_) {
// 某些属性可能不可访问,忽略即可
}
}
},
// 记录全局修改(手动写变量时调用)
set(key, value) {
modifiedProps = rawWindow;
rawWindow = value;
},
// 失活:恢复 window 到快照
deactivate() {
for (const key in rawWindow) {
if (!(key in snapshot)) {
// 卸载后删除新增的
delete rawWindow;
} else if (rawWindow !== snapshot) {
// 还原被修改的
rawWindow = snapshot;
}
}
modifiedProps = {};
}
};
}</pre>
</div>
<div>
<div>
<p>上述代码中,<code>snapshot</code> 是全局变量的“拍照备份”,在 <code>sandbox.activate()</code> 时,会遍历一次 <code>window</code>,保存所有当前的属性和值。它用于记录挂载子应用之前的 <code>window</code> 状态,在卸载时(<code>deactivate</code>)时,拿这个备份和当前 <code>window</code> 对比,使 <code>window</code> 回到快照时的状态。</p>
<ul>
<li>删除新增属性(子应用新增的全局变量)。</li>
<li>还原被修改的属性(子应用修改过的变量)。</li>
</ul>
<p><code>modifiedProps</code> 是运行时的“变更记录”,使用它快速知道子应用改动了哪些属性,卸载时可以更高效地只恢复被改动过的,而不是全量比对。</p>
<p>使用示例:</p>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">const sandbox = createSnapshotSandbox();
sandbox.activate();// 挂载前,拍快照
window.foo = 123; // 模拟子应用写全局
console.log(window.foo); // 123
sandbox.deactivate(); // 卸载后恢复
console.log(window.foo); // undefined(被删除)</pre>
</div>
<div>
<div>
<h3 data-id="heading-2">LegacySandbox(单实例沙箱)</h3>
<p><strong>快照沙箱 (SnapshotSandbox)</strong> 虽然能恢复全局变量,但性能差,还不支持并行运行。<br>
因此 qiankun 在 <strong>支持</strong> <code>Proxy</code> <strong>之前</strong>,实现了一个改进版的沙箱 —— <strong>LegacySandbox</strong>。</p>
<p>简化版代码示例:</p>
</div>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">class LegacySandbox {
constructor(name) {
this.name = name;
this.addedPropsMap = new Map(); // 记录新增的全局属性
this.modifiedPropsOriginalMap = new Map(); // 记录修改前的原始值
this.currentUpdatedPropsValueMap = new Map();// 记录当前子应用改动后的值
}
// 激活:恢复上次的运行环境
activate() {
this.currentUpdatedPropsValueMap.forEach((v, p) => {
window = v;
});
}
// 失活:清理全局变量
deactivate() {
// 删除新增属性
this.addedPropsMap.forEach((_, p) => {
delete window;
});
// 恢复修改过的属性
this.modifiedPropsOriginalMap.forEach((v, p) => {
window = v;
});
}
// 设置全局变量时调用
setWindowProp(prop, value) {
if (!window.hasOwnProperty(prop)) {
// 新增属性
this.addedPropsMap.set(prop, value);
} else if (!this.modifiedPropsOriginalMap.has(prop)) {
// 第一次修改,记录原始值
this.modifiedPropsOriginalMap.set(prop, window);
}
// 记录最新值
this.currentUpdatedPropsValueMap.set(prop, value);
window = value;
}
}</pre>
</div>
<div>
<div>
<p>LegacySandbox 的核心思路是:</p>
<ul>
<li><strong>维护三份状态:</strong> <code>addedPropsMap</code>:记录子应用新增的全局属性。<code>modifiedPropsOriginalMap</code>:记录子应用修改前的原始值。<code>currentUpdatedPropsValueMap</code>:记录子应用修改后的值。</li>
<li><strong>激活(activate):</strong> 遍历 <code>currentUpdatedPropsValueMap</code>,恢复上次运行时的修改。</li>
<li><strong>运行中:</strong> 每当子应用往 <code>window</code> 上赋值时:如果是新增 → 记录到 <code>addedPropsMap</code>。如果是修改 → 记录原始值到 <code>modifiedPropsOriginalMap</code>,并把新值写到 <code>currentUpdatedPropsValueMap</code>。</li>
<li><strong>失活(deactivate):</strong> 删除 <code>addedPropsMap</code> 中的属性(还原新增)。用 <code>modifiedPropsOriginalMap</code> 恢复被修改过的属性(还原修改)。</li>
</ul>
<p>使用示例:</p>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">const sandbox = new LegacySandbox("app1");
sandbox.activate(); // 激活应用
sandbox.setWindowProp("foo", 123);
console.log(window.foo); // 123
sandbox.deactivate(); // 卸载应用
console.log(window.foo); // undefined(被删除)</pre>
</div>
<div>
<div>
<h3 data-id="heading-3">ProxySandbox(代理沙箱,多实例沙箱)</h3>
<p><strong>ProxySandbox</strong> 可以说是 qiankun 沙箱的“终极形态”,现代浏览器环境下的主力方案。前面说的两种沙箱存在下面的问题</p>
<ul>
<li><strong>SnapshotSandbox</strong>:全量快照,对比恢复,性能差。</li>
<li><strong>LegacySandbox</strong>:单实例(只能一个子应用同时运行),多个并行时会冲突。</li>
</ul>
<p>为了解决 <strong>性能 + 并行运行</strong> 的问题,引入了 <strong>ProxySandbox</strong>。</p>
<p>它的核心是 <strong>ES6 的 Proxy</strong>,拦截对 <code>window</code> 的访问:</p>
<ul>
<li>给每个子应用创建一个「假的 window」对象(称为 <code>fakeWindow</code>)。</li>
<li><code>fakeWindow</code> 的原型指向真正的 <code>window</code>,这样子应用能正常访问到全局属性。</li>
<li>子应用对全局变量的 <strong>修改、删除、新增</strong> 都只会作用在 <code>fakeWindow</code> 上,而不会污染真实的 <code>window</code>。</li>
<li>不同子应用有不同的 <code>fakeWindow</code>,天然实现多实例隔离。</li>
</ul>
</div>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 1. 创建 ProxySandbox
function createProxySandbox() {
// 创建一个空对象 没有原型链。
const fakeWindow = Object.create(null);
return new Proxy(fakeWindow, {
get(target, prop) {
if (prop in target) {
return target; // 优先取子应用自己的
}
return window; // 否则取宿主的全局
},
set(target, prop, value) {
target = value;// 写只写在 fakeWindow 上
return true;
}
});
}
// 2. 模拟子应用执行环境
function runInSandbox(code, sandbox) {
const wrapper = new Function("window", `
with(window) {
${code}
}
`);
wrapper(sandbox); // 关键:传入 proxy
}
// 3. 使用
const sandbox1 = createProxySandbox();
const sandbox2 = createProxySandbox();
runInSandbox(`window.foo = "app1"; console.log("app1 foo =", window.foo);`, sandbox1);
runInSandbox(`window.foo = "app2"; console.log("app2 foo =", window.foo);`, sandbox2);
console.log("真实 window.foo =", window.foo); // undefined,没有污染</pre>
</div>
</div>
<div>
<div>
<h4 data-id="heading-4">new Proxy(fakeWindow, handler)</h4>
<p>这里的逻辑简化一下主演干了下面的事情:</p>
<ul>
<li><strong>get</strong><br>
读属性时触发。优先取 <code>fakeWindow</code>,否则兜底真实 <code>window</code>。<br>
👉 写过的值会“遮挡”宿主值。</li>
<li><strong>set</strong><br>
写属性时触发。只写入 <code>fakeWindow</code>,不污染真实 <code>window</code>。</li>
<li><strong>has</strong><br>
<code>with</code> 语句查找变量时触发。返回 <code>prop in fakeWindow || prop in window</code>。<br>
👉 确保像 <code>console</code>、<code>document</code> 这些全局在子应用里能被正常访问。</li>
<li><strong>deleteProperty</strong><br>
删除属性时触发。只删 <code>fakeWindow</code> 的内容,不影响真实 <code>window</code>。</li>
</ul>
<h4 data-id="heading-5"><code>runInSandbox</code> 是如何把子应用“绑”到 proxy 的</h4>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">const wrapper = new Function("window", `
with(window) {
${code}
}
`);
wrapper(proxy);</pre>
</div>
<div>
<div>
<ul>
<li><code>new Function("window", "with(window){ ... }")</code> 创建了一个函数,函数参数名是 <code>window</code>。</li>
<li><code>wrapper(proxy)</code> 把我们造的 <code>proxy</code> 作为形参 <code>window</code> 传入。</li>
<li><code>with(window) { ... }</code> 会把这个 <code>window</code>(即 <code>proxy</code>)加入当前作用域链,<strong>所以代码里的未限定标识符(比如</strong> <code>foo</code> <strong>、</strong> <code>location</code> <strong>、</strong> <code>document</code> <strong>)会先在</strong> <code>proxy</code> <strong>上被查找/操作</strong>。</li>
<li>结合上面的 <code>get/set/has</code>,所有读取/写入都会被代理到 handler,从而实现拦截。</li>
</ul>
<h2 data-id="heading-6">CSS 隔离原理</h2>
<p>qiankun 没有强制启用某种隔离,而是给开发者提供了几种选择:</p>
<ul>
<li><strong>默认:无强隔离,</strong> 子应用样式直接插入主应用 <code>head</code>,容易污染,但性能最好。</li>
<li><strong>StrictStyleIsolation(严格隔离):</strong> 使用 <strong>Shadow DOM</strong> 把子应用包裹起来。</li>
</ul>
</div>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">registerMicroApps(apps, {
sandbox: { strictStyleIsolation: true }
})</pre>
</div>
<p>这种方式的优点是彻底隔离,但某些全局样式/第三方库不兼容</p>
<ul>
<li>ExperimentalStyleIsolation(实验性隔离): 给子应用容器加 <code>data-qiankun="xxx"</code> 属性,然后动态给所有 CSS 规则加前缀。</li>
</ul>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">registerMicroApps(apps, {
sandbox: { experimentalStyleIsolation: true }
})</pre>
</div>
这种范式类似 Vue 的 scoped CSS,兼容性比 Shadow DOM 更好。<br>
</div>
<br>
</div>
<div>
<h2>本文转载于:https://juejin.cn/post/7542506863206383668</h2>
</div>
<h3 id="tid-D8HBxE">如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。</h3>
<p><em><img src="https://img2024.cnblogs.com/blog/2149129/202501/2149129-20250122165814748-630765389.png" alt="" loading="lazy"></em></p>
</div><br><br>
来源:https://www.cnblogs.com/smileZAZ/p/19068642
頁:
[1]