qiankun 的 JS 沙箱隔離機制

柏成發表於2024-12-04

為什麼需要JS沙箱

想象一下🧐

當一個應用(比如應用 A)載入時,可能會對 window 物件的屬性進行修改或新增。如果不加控制,這些修改會影響到之後載入的其他應用(比如應用 B),就會導致屬性讀寫衝突

所以!對於各應用的 js檔案來說,就需要一個獨立的環境來執行,防止 window 全域性物件發生屬性讀寫衝突,這個獨立的執行環境就叫做 JS沙箱

乾坤沙箱

乾坤目前存在三種 JS隔離機制,分別是 SnapshotSandboxLegacySandboxProxySandbox

  1. SnapshotSandbox(快照沙箱)
    這是乾坤最早期的沙箱機制,在每次應用啟用和失活時遍歷 window 物件的所有屬性,記錄並恢復其狀態 。
    效能很差,浪費記憶體;優點是可以相容不支援 Proxy 的舊版瀏覽器

  2. LegacySandbox(單應用代理沙箱)
    在 SnapshotSandbox 基礎上進行最佳化的一種沙箱機制,透過 ES6 的Proxywindow 物件進行更高效的代理。
    然而,它仍會讀寫 window 物件,存在全域性汙染的問題;並且只能支援單個微應用的執行,意味著在一個頁面上不能同時執行多個微應用
    事實上,LegacySandbox 在未來應該會消失, 逐漸被能夠同時支援多個微應用的 ProxySandbox 所取代

  3. 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 恢復到最初狀態(從 modifyPropsMapaddedPropsMap 中讀取)

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)
  • 效能最佳化:透過 Proxywindow 物件進行代理 ,避免了遍歷 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,這使得多個微應用可以在同一頁面中獨立執行而不會相互影響
  • 高效效能:透過 Proxywindow 物件進行代理,能夠在不修改原始 window 的情況下實現沙箱隔離,效能更高。
  • 依賴現代瀏覽器:依賴於 ES6 的 Proxy,不支援較舊的瀏覽器

ProxySandbox 透過 Proxy 為每個微應用建立了一個獨立的虛擬 window,有效地隔離了微應用之間的全域性狀態。它是現代微前端架構中實現多應用支援和環境隔離的關鍵技術之一。透過這種方式,開發者可以在同一頁面中並行執行多個微應用,而不用擔心全域性變數汙染或應用間的干擾。

參考文件

GitHub - zuopf769/qiankun-js-sandbox: 乾坤的JS沙箱隔離機制原理剖析

GitHub - careyke/frontend_knowledge_structure: qiankun中JS沙箱的實現

微前端01 : 乾坤的Js隔離機制原理剖析(快照沙箱、兩種代理沙箱)

相關文章