微前端中實現沙箱環境的方案調研

空山與新雨發表於2022-12-21

前言

在微前端實踐過程中有一個必然會遇到的問題:全域性作用域變數的汙染問題,具體來說就是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方案是比較優雅和實用的方案

相關文章