Stories

Detail Return Return

記錄---一篇文了解qiankun的代碼隔離原理 - Stories Detail

🧑‍💻 寫在開頭

點贊 + 收藏 === 學會🤣🤣🤣

隨着前端業務的快速發展,微前端架構已經被廣泛採用,其中 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
    👉 確保像 consoledocument 這些全局在子應用裏能被正常訪問。
  • 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 }
})
這種範式類似 Vue 的 scoped CSS,兼容性比 Shadow DOM 更好。

本文轉載於:https://juejin.cn/post/7542506863206383668

如果對您有所幫助,歡迎您點個關注,我會定時更新技術文檔,大家一起討論學習,一起進步。

user avatar atguigu Avatar anchen_5c17815319fb5 Avatar jdcdevloper Avatar kitty-38 Avatar wangyiyunyidun Avatar ranck Avatar ouysh1981 Avatar webinfoq Avatar xingzhaodezhaoxiansheng Avatar pxzsl Avatar tingtinger Avatar
Favorites 11 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.