為什麼需要JS沙箱
想象一下🧐
當一個應用(比如應用 A)載入時,可能會對 window
物件的屬性進行修改或新增。如果不加控制,這些修改會影響到之後載入的其他應用(比如應用 B),就會導致屬性讀寫衝突
所以!對於各應用的 js檔案來說,就需要一個獨立的環境來執行,防止 window
全域性物件發生屬性讀寫衝突,這個獨立的執行環境就叫做 JS沙箱
乾坤沙箱
乾坤目前存在三種 JS隔離機制,分別是 SnapshotSandbox
、LegacySandbox
和 ProxySandbox
-
SnapshotSandbox(快照沙箱)
這是乾坤最早期的沙箱機制,在每次應用啟用和失活時遍歷window
物件的所有屬性,記錄並恢復其狀態 。
效能很差,浪費記憶體;優點是可以相容不支援Proxy
的舊版瀏覽器 -
LegacySandbox(單應用代理沙箱)
在 SnapshotSandbox 基礎上進行最佳化的一種沙箱機制,透過 ES6 的Proxy
對window
物件進行更高效的代理。
然而,它仍會讀寫window
物件,存在全域性汙染的問題;並且只能支援單個微應用的執行,意味著在一個頁面上不能同時執行多個微應用
事實上,LegacySandbox 在未來應該會消失, 逐漸被能夠同時支援多個微應用的 ProxySandbox 所取代 -
ProxySandbox(多應用代理沙箱)
是乾坤最先進的一種 JS沙箱隔離機制,透過Proxy
物件為每個微應用建立了一個獨立的虛擬window
。不會操作window
物件,不存在全域性汙染的問題;而且在同一頁面上也支援多個微應用的同時執行
缺點則是不相容不支援proxy
舊版瀏覽器
SnapshotSandbox(快照沙箱)
這是乾坤最早期的沙箱機制,其主要目的是透過記錄和恢復 window
物件的狀態,確保每個微應用在啟用和失活時都能夠擁有獨立的環境
這裡我們實現一個簡單的快照沙箱機制, 用於隔離不同微應用對全域性 window
物件的修改,對應乾坤的原始碼可以看這裡 - SnapshotSandbox原始碼
工作原理💯
當沙箱啟用時, 儲存當前 window
的狀態到 windowSnapShot
,然後恢復到上次失活前的狀態 (從 modifyPropsMap
中讀取)
當沙箱失活時, 記錄所有對 window
物件的修改(與 windowSnapShot
的差異),並將 window
恢復到最初的快照狀態
class SnapshotSandbox {
constructor() {
this.windowSnapShot = {}; // 儲存 window 物件的初始快照
this.modifyPropsMap = {}; // 儲存全域性哪些屬性被修改了
}
// 啟用
active() {
this.windowSnapShot = {};
// 記錄應用 A window 初始狀態
Object.keys(window).forEach(prop => {
this.windowSnapShot[prop] = window[prop]
})
// 恢復到應用 A 上次失活之前的狀態
Object.keys(this.modifyPropsMap).forEach(prop => {
window[prop] = this.modifyPropsMap[prop]
})
}
// 失活
inactive() {
this.modifyPropsMap = {}
Object.keys(window).forEach(prop => {
if (window[prop] !== this.windowSnapShot[prop]) {
this.modifyPropsMap[prop] = window[prop]; // 記錄應用A 所做的所有修改
window[prop] = this.windowSnapShot[prop]; // 將 window 恢復到最初狀態
}
})
}
}
let sandbox = new SnapshotSandbox();
sandbox.active();
window.a = 100;
window.b = 200;
sandbox.inactive();
console.log(window.a, window.b)
sandbox.active();
console.log(window.a, window.b)
- 效能很差, 因為需要在每次應用啟用和失活時遍歷
window
物件的所有屬性來記錄和恢復其狀態。在屬性較多或頻繁切換應用的情況下,效能瓶頸尤為明顯。 - 浪費記憶體, 儲存
window
物件的完整狀態會佔用大量記憶體 - 優點是可以相容不支援
Proxy
的舊版瀏覽器
總的來說,快照沙箱透過拍攝 window
物件的快照來實現狀態的隔離,雖然簡單有效,但存在效能和記憶體方面的不足,適用於對瀏覽器相容性有要求的場景
LegacySandbox(單應用代理沙箱)
是在 SnapshotSandbox 基礎上最佳化的一種沙箱機制。它利用 ES6 引入的 Proxy
特性來提高效能,並實現類似於快照沙箱的功能
這裡我們實現一個簡單的單應用代理沙箱機制, 對應乾坤的原始碼可以看這裡 - LegacySandbox原始碼
工作原理💯
建立一個 fakeWindow
物件,並使用 Proxy
對其進行代理,在 set
攔截器中,addedPropsMap
記錄新新增的屬性,modifyPropsMap
記錄被修改的屬性原始值,currentPropsMap
記錄所有被新增修改的屬性最新值
當沙箱啟用時,恢復到上次失活前的狀態 (從 currentPropsMap
中讀取)
當沙箱失活時,還原修改前的屬性值並刪除新新增的屬性,將 window
恢復到最初狀態(從 modifyPropsMap
、addedPropsMap
中讀取)
class LegacySandbox {
constructor() {
this.modifyPropsMap = new Map() // 儲存被修改過的屬性原始值
this.addedPropsMap = new Map() // 儲存新新增的屬性值
this.currentPropsMap = new Map() // 儲存所有被修改或新新增的屬性最新值
// 建立一個 fakeWindow 物件,並使用 Proxy 對其進行代理
const fakeWindow = Object.create(null)
const proxy = new Proxy(fakeWindow, {
get: (target, key, recevier) => {
return window[key]
},
set: (target, key, value) => {
if (!window.hasOwnProperty(key)) {
this.addedPropsMap.set(key, value) // 新新增的屬性
} else if (!this.modifyPropsMap.has(key)) {
this.modifyPropsMap.set(key, window[key]) // 被修改的屬性原始值
}
this.currentPropsMap.set(key, value) // 所有的新增修改操作都儲存一份最新值
window[key] = value
},
})
this.proxy = proxy
}
// 設定 window 物件的屬性
setWindowProp(key, value) {
if (value == undefined) {
delete window[key]
} else {
window[key] = value
}
}
// 啟用沙箱
active() {
// 恢復到應用 A 上次失活之前的狀態
this.currentPropsMap.forEach((value, key) => {
this.setWindowProp(key, value)
})
}
// 失活沙箱
inactive() {
// 被修改的屬性重置為原始值
this.modifyPropsMap.forEach((value, key) => {
this.setWindowProp(key, value)
})
// 移除新新增的屬性
this.addedPropsMap.forEach((value, key) => {
this.setWindowProp(key, undefined)
})
}
}
let sandbox = new LegacySandbox()
sandbox.proxy.a = 100
console.log(window.a, sandbox.proxy.a)
sandbox.inactive()
console.log(window.a, sandbox.proxy.a)
sandbox.active()
console.log(window.a, sandbox.proxy.a)
- 效能最佳化:透過
Proxy
對window
物件進行代理 ,避免了遍歷window
的效能開銷 - 全域性汙染:儘管使用了
Proxy
,LegacySandbox 依然在全域性window
上進行操作,還是會汙染全域性的window
- 單應用支援:和 SnapshotSandbox 一樣,LegacySandbox 僅支援單個微應用的執行,無法在同一頁面同時隔離多個微應用
事實上,LegacySandbox 在未來應該會消失, 逐漸被能夠同時支援多個微應用的 ProxySandbox 所取代
ProxySandbox(多應用代理沙箱)
ProxySandbox 也是一種基於 Proxy
物件實現的沙箱機制,它能夠支援在同一頁面上執行多個微應用,同時保證每個應用對全域性環境(window
物件)的修改是隔離的
這裡我們實現一個簡單的多應用代理沙箱機制, 對應乾坤的原始碼可以看這裡 - ProxySandbox原始碼
工作原理💯
建立一個 fakeWindow
物件,並使用 Proxy
對其進行代理
當沙箱啟用時,所有的修改都被限制在 fakeWindow
內,而不影響真實的 window
物件
當沙箱失活時,將 this.running
設定為 false
, 沙箱不再攔截修改操作
class ProxySandbox {
constructor() {
this.running = false
// 使用 Proxy 對 fakeWindow 進行代理
const fakeWindow = Object.create(null)
this.proxy = new Proxy(fakeWindow, {
get: (target, key) => {
return key in target ? target[key] : window[key]
},
set: (target, key, value) => {
if (this.running) {
target[key] = value // 將修改操作應用到 fakeWindow 上,而不是真實的 window 物件
}
return true
},
})
}
active() {
if (!this.running) this.running = true
}
inactive() {
this.running = false
}
}
let sandbox1 = new ProxySandbox()
let sandbox2 = new ProxySandbox()
sandbox1.active()
sandbox2.active()
sandbox1.proxy.a = 100
sandbox2.proxy.a = 100
console.log(sandbox1.proxy.a, sandbox2.proxy.a)
sandbox1.inactive()
sandbox2.inactive()
sandbox1.proxy.a = 200
sandbox2.proxy.a = 200
console.log(sandbox1.proxy.a, window.a)
console.log(sandbox2.proxy.a, window.a)
- 多應用支援:每個應用擁有自己的虛擬
window
,這使得多個微應用可以在同一頁面中獨立執行而不會相互影響 - 高效效能:透過
Proxy
對window
物件進行代理,能夠在不修改原始window
的情況下實現沙箱隔離,效能更高。 - 依賴現代瀏覽器:依賴於 ES6 的
Proxy
,不支援較舊的瀏覽器
ProxySandbox 透過 Proxy
為每個微應用建立了一個獨立的虛擬 window
,有效地隔離了微應用之間的全域性狀態。它是現代微前端架構中實現多應用支援和環境隔離的關鍵技術之一。透過這種方式,開發者可以在同一頁面中並行執行多個微應用,而不用擔心全域性變數汙染或應用間的干擾。
參考文件
GitHub - zuopf769/qiankun-js-sandbox: 乾坤的JS沙箱隔離機制原理剖析
GitHub - careyke/frontend_knowledge_structure: qiankun中JS沙箱的實現
微前端01 : 乾坤的Js隔離機制原理剖析(快照沙箱、兩種代理沙箱)