記錄---一篇文了解qiankun的代碼隔離原理
????? 寫在開頭
點贊 + 收藏 === 學會??????
隨著前端業務的快速發展,微前端架構已經被廣泛采用,其中 qiankun 作為主流解決方案也越來越受到關注。前幾天面試時,我就被問到了一個高頻問題:qiankun 是如何實現 JS 和 CSS 隔離的?
qiankun 的JS 沙箱
qiankun 的微前端場景是:主應用加載多個子應用,不同子應用可能依賴不同版本的庫、全局變量,甚至可能會互相覆蓋 window 上的屬性。為了避免“全局污染”,qiankun 提供了沙箱機制。
常見的JS 沙箱實現思路有下面三種:
SnapshotSandbox(快照沙箱)
快照沙箱是微前端里最直觀的 JS 隔離方式之一:
- 掛載應用前 → 對
window對象做一次“快照”,保存所有屬性及其值。 - 應用運行中 → 子應用可以隨意修改全局變量。
- 卸載應用時 → 把
window恢復到掛載前的快照狀態(新增的刪掉、改過的還原)。
它的過程使用偽代碼大致如下:
/**
* 快照沙箱
* - 掛載前:拍快照(淺拷貝 window 屬性)
* - 卸載時:恢復快照(刪除新增,還原修改)
*/
function createSnapshotSandbox() {
const rawWindow = window;
let snapshot = null; // 存儲拍下來的全局狀態
let modifiedProps = {}; // 存儲運行過程中被修改的屬性
return {
// 激活:拍下當前 window 狀態
activate() {
snapshot = {};
for (const key in rawWindow) {
try {
snapshot[key] = rawWindow[key];
} catch (_) {
// 某些屬性可能不可訪問,忽略即可
}
}
},
// 記錄全局修改(手動寫變量時調用)
set(key, value) {
modifiedProps[key] = rawWindow[key];
rawWindow[key] = value;
},
// 失活:恢復 window 到快照
deactivate() {
for (const key in rawWindow) {
if (!(key in snapshot)) {
// 卸載后刪除新增的
delete rawWindow[key];
} else if (rawWindow[key] !== snapshot[key]) {
// 還原被修改的
rawWindow[key] = snapshot[key];
}
}
modifiedProps = {};
}
};
}
上述代碼中,snapshot 是全局變量的“拍照備份”,在 sandbox.activate() 時,會遍歷一次 window,保存所有當前的屬性和值。它用于記錄掛載子應用之前的 window 狀態,在卸載時(deactivate)時,拿這個備份和當前 window 對比,使 window 回到快照時的狀態。
- 刪除新增屬性(子應用新增的全局變量)。
- 還原被修改的屬性(子應用修改過的變量)。
modifiedProps 是運行時的“變更記錄”,使用它快速知道子應用改動了哪些屬性,卸載時可以更高效地只恢復被改動過的,而不是全量比對。
使用示例:
const sandbox = createSnapshotSandbox(); sandbox.activate(); // 掛載前,拍快照 window.foo = 123; // 模擬子應用寫全局 console.log(window.foo); // 123 sandbox.deactivate(); // 卸載后恢復 console.log(window.foo); // undefined(被刪除)
LegacySandbox(單實例沙箱)
快照沙箱 (SnapshotSandbox) 雖然能恢復全局變量,但性能差,還不支持并行運行。
因此 qiankun 在 支持 Proxy 之前,實現了一個改進版的沙箱 —— LegacySandbox。
簡化版代碼示例:
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[p] = v;
});
}
// 失活:清理全局變量
deactivate() {
// 刪除新增屬性
this.addedPropsMap.forEach((_, p) => {
delete window[p];
});
// 恢復修改過的屬性
this.modifiedPropsOriginalMap.forEach((v, p) => {
window[p] = v;
});
}
// 設置全局變量時調用
setWindowProp(prop, value) {
if (!window.hasOwnProperty(prop)) {
// 新增屬性
this.addedPropsMap.set(prop, value);
} else if (!this.modifiedPropsOriginalMap.has(prop)) {
// 第一次修改,記錄原始值
this.modifiedPropsOriginalMap.set(prop, window[prop]);
}
// 記錄最新值
this.currentUpdatedPropsValueMap.set(prop, value);
window[prop] = value;
}
}
LegacySandbox 的核心思路是:
- 維護三份狀態:
addedPropsMap:記錄子應用新增的全局屬性。modifiedPropsOriginalMap:記錄子應用修改前的原始值。currentUpdatedPropsValueMap:記錄子應用修改后的值。 - 激活(activate): 遍歷
currentUpdatedPropsValueMap,恢復上次運行時的修改。 - 運行中: 每當子應用往
window上賦值時:如果是新增 → 記錄到addedPropsMap。如果是修改 → 記錄原始值到modifiedPropsOriginalMap,并把新值寫到currentUpdatedPropsValueMap。 - 失活(deactivate): 刪除
addedPropsMap中的屬性(還原新增)。用modifiedPropsOriginalMap恢復被修改過的屬性(還原修改)。
使用示例:
const sandbox = new LegacySandbox("app1");
sandbox.activate(); // 激活應用
sandbox.setWindowProp("foo", 123);
console.log(window.foo); // 123
sandbox.deactivate(); // 卸載應用
console.log(window.foo); // undefined(被刪除)
ProxySandbox(代理沙箱,多實例沙箱)
ProxySandbox 可以說是 qiankun 沙箱的“終極形態”,現代瀏覽器環境下的主力方案。前面說的兩種沙箱存在下面的問題
- SnapshotSandbox:全量快照,對比恢復,性能差。
- LegacySandbox:單實例(只能一個子應用同時運行),多個并行時會沖突。
為了解決 性能 + 并行運行 的問題,引入了 ProxySandbox。
它的核心是 ES6 的 Proxy,攔截對 window 的訪問:
- 給每個子應用創建一個「假的 window」對象(稱為
fakeWindow)。 fakeWindow的原型指向真正的window,這樣子應用能正常訪問到全局屬性。- 子應用對全局變量的 修改、刪除、新增 都只會作用在
fakeWindow上,而不會污染真實的window。 - 不同子應用有不同的
fakeWindow,天然實現多實例隔離。
// 1. 創建 ProxySandbox
function createProxySandbox() {
// 創建一個空對象 沒有原型鏈。
const fakeWindow = Object.create(null);
return new Proxy(fakeWindow, {
get(target, prop) {
if (prop in target) {
return target[prop]; // 優先取子應用自己的
}
return window[prop]; // 否則取宿主的全局
},
set(target, prop, value) {
target[prop] = 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,沒有污染
new Proxy(fakeWindow, handler)
這里的邏輯簡化一下主演干了下面的事情:
- get
讀屬性時觸發。優先取fakeWindow,否則兜底真實window。
?? 寫過的值會“遮擋”宿主值。 - set
寫屬性時觸發。只寫入fakeWindow,不污染真實window。 - has
with語句查找變量時觸發。返回prop in fakeWindow || prop in window。
?? 確保像console、document這些全局在子應用里能被正常訪問。 - deleteProperty
刪除屬性時觸發。只刪fakeWindow的內容,不影響真實window。
runInSandbox 是如何把子應用“綁”到 proxy 的
const wrapper = new Function("window", `
with(window) {
${code}
}
`);
wrapper(proxy);
new Function("window", "with(window){ ... }")創建了一個函數,函數參數名是window。wrapper(proxy)把我們造的proxy作為形參window傳入。with(window) { ... }會把這個window(即proxy)加入當前作用域鏈,所以代碼里的未限定標識符(比如foo、location、document)會先在proxy上被查找/操作。- 結合上面的
get/set/has,所有讀取/寫入都會被代理到 handler,從而實現攔截。
CSS 隔離原理
qiankun 沒有強制啟用某種隔離,而是給開發者提供了幾種選擇:
- 默認:無強隔離, 子應用樣式直接插入主應用
head,容易污染,但性能最好。 - StrictStyleIsolation(嚴格隔離): 使用 Shadow DOM 把子應用包裹起來。
registerMicroApps(apps, {
sandbox: { strictStyleIsolation: true }
})
這種方式的優點是徹底隔離,但某些全局樣式/第三方庫不兼容
- ExperimentalStyleIsolation(實驗性隔離): 給子應用容器加
data-qiankun="xxx"屬性,然后動態給所有 CSS 規則加前綴。
registerMicroApps(apps, {
sandbox: { experimentalStyleIsolation: true }
})
本文轉載于:https://juejin.cn/post/7542506863206383668
如果對您有所幫助,歡迎您點個關注,我會定時更新技術文檔,大家一起討論學習,一起進步。


浙公網安備 33010602011771號