前言
在微前端實踐過程中有一個必然會遇到的問題:全域性作用域變數的汙染問題,具體來說就是window物件掛載資料會被主子應用獲取和修改導致資料相互汙染問題,這時候如果能在應用之間做個資料隔離,最好能實現一個沙箱環境,對解決問題很有幫助。
iframe方案
說到沙箱隔離,首先想到的是iframe,自帶資料隔離能力,從iframe中獲取到的window物件是一個全新和純淨的物件,然而在如果要作為沙箱執行業務程式碼的話是不行的,但是完全可以作為一個執行指令碼環境,既安全,又簡單:
const parent = window;
const frame = document.createElement('iframe');
const data = [1, 2, 3, 4, 5, 6];
// 當前頁面給 iframe 傳送訊息
frame.onload = function (e) {
frame.contentWindow.postMessage(data);
};
document.body.appendChild(frame);
// iframe 接收到訊息後處理
const code = `return dataInIframe.filter((item) => item % 2 === 0)`;
frame.contentWindow.addEventListener('message', function (e) {
const func = new frame.contentWindow.Function('dataInIframe', code);
parent.postMessage(func(e.data));
});
// 父頁面接收 iframe 傳送過來的訊息
parent.addEventListener(
'message',
function (e) {
console.log('message from iframe:', e.data);
},
false,
);
快照方案
在微前端框架qiankun中提供了快照方案,其原理就是在應用載入之時儲存最初的window物件,解除安裝應用之時透過diff操作記錄改過的屬性即製作快照,當再次啟用應用的時候恢復之前的快照。該方案的缺點是會汙染window導致,多個應用無法同時處於啟用狀態,優點是相容性好。
// 儲存差異的方式
function createSandbox(){
let originWindow = {}
let diffMap = {};
return {
toActive(){
originWindow = {};
// 儲存初始window物件
Object.keys(window).forEach(prop=>{
originWindow[prop] = window[prop];
})
// 將上次退出的時候儲存的差異還原回去,也就是恢復快照
Object.keys(diffMap).forEach(prop=>{
window[prop] = diffMap[prop];
})
},
toInActive(){
Object.keys(window).forEach(prop=>{
if(window[prop] !== originWindow[prop]){
// 儲存差異
diffMap[prop] = window[prop]
// 還原現場
window[prop] = originWindow[prop];
}
})
}
}
}
window.originData = '最初的window上的資料';
console.log(window.originData, window.a1, window.b1); // 最初的window上的資料 undefined undefined
const sandbox1 = createSandbox(); // 建立應用的時候,同時建立沙箱
sandbox1.toActive(); // 沙箱啟用
window.a1 = 'aaaaa'; // 應用修改window上的屬性
console.log(window.originData, window.a1, window.b1); // 最初的window上的資料 aaaaa undefined
sandbox1.toInActive(); // 切換應用前沙箱1退出
const sandbox2 = createSandbox(); // 建立應用的時候,同時建立沙箱
sandbox2.toActive(); // 沙箱啟用
console.log(window.originData, window.a1, window.b1); // 最初的window上的資料 undefined undefined
window.b1 = 'bbbbb'; // 應用修改window上的屬性
console.log(window.originData, window.a1, window.b1); // 最初的window上的資料 undefined bbbbb 和上面的資料做個對比
sandbox2.toInActive(); // 從應用2切換至1
sandbox1.toActive(); // 從應用2切換至1
console.log(window.originData, window.a1, window.b1); // 最初的window上的資料 aaaaa undefined 和上面的資料做個對比
sandbox1.toInActive(); // 從應用1切換至2
sandbox2.toActive(); // 從應用1切換至2
console.log(window.originData, window.a1, window.b1); // 最初的window上的資料 undefined bbbbb 和上面的資料做個對比
代理方案
使用ES6中的proxy語法對自定義的全域性物件代理,這樣當在沙箱內部對window物件修改的時候,實際上修改的是自定義的全域性物件,而不會影響到真正的window物件。其優點是不會汙染window,支援多個應用同時啟用。 缺點是部分瀏覽器不支援proxy,
function createProxySandBox(){
const rawWindow = window;
const fakeWindow = {};
const proxy = new Proxy(fakeWindow, {
get:(target, p)=>{
if(target.hasOwnProperty(p)){
return target[p];
}
return rawWindow[p];
},
set(target, p, value){
if(!target.hasOwnProperty(p) && rawWindow.hasOwnProperty(p)){
rawWindow[p] = value
} else {
target[p] = value;
}
}
})
return proxy;
}
const sandbox1 = createProxySandBox();
((window) => {
window.a = 'a';
})(sandbox1);
const sandbox2 = createProxySandBox();
((window) => {
console.log(window.a)
window.a = 'fff';
})(sandbox2);
console.log(window.a)
總結
proxy方案是比較優雅和實用的方案