qiankun 2.x 執行時沙箱 原始碼分析

李永寧發表於2022-02-07

簡介

從原始碼層面詳細講解了 qiankun 框架中的 JS 沙箱 和 樣式沙箱的實現原理。

序言

沙箱 這個詞想必大家應該不陌生,即使陌生,讀完這篇文章也就不那麼陌生了

沙箱 (Sandboxie) ,又叫沙盤,即是一個虛擬系統程式,允許你在沙盤環境中執行瀏覽器或其他程式,因此執行所產生的變化可以隨後刪除。它創造了一個類似沙盒的獨立作業環境,在其內部執行的程式並不能對硬碟產生永久性的影響。 在網路安全中,沙箱指在隔離環境中,用以測試不受信任的檔案或應用程式等行為的工具

而今天要說的沙箱來自 qiankun 的實現,是為了解決微前端方案中的隔離問題,qiankun 目前可以說的最好的 微前端 實現方案吧,它基於 single-spa 做了二次封裝,解決了 single-spa 遺留的眾多問題, 執行時沙箱 就是其中之一

為什麼需要它

single-spa 雖好,但卻存在一些需要在框架層面解決但卻沒有解決的問題,比如為每個微應用提供一個乾淨、獨立的執行環境

JS 全域性物件汙染是一個很常見的現象,比如:微應用 A 在全域性物件上新增了一個自己特有的屬性 window.A,這時候切換到微應用 B,這時候如何保證 window物件是乾淨的呢?答案就是 qiankun 實現的 執行時沙箱

總結

先上總結,執行時沙箱分為 JS 沙箱樣式沙箱

JS 沙箱

JS 沙箱,通過 proxy 代理 window 物件,記錄 window 物件上屬性的增刪改查

  • 單例模式

    直接代理了原生 window 物件,記錄原生 window 物件的增刪改查,當 window 物件啟用時恢復 window 物件到上次即將失活時的狀態,失活時恢復 window 物件到初始初始狀態

  • 多例模式

    代理了一個全新的物件,這個物件是複製的 window 物件的一部分不可配置屬性,所有的更改都是基於這個 fakeWindow 物件,從而保證多個例項之間屬性互不影響

將這個 proxy 作為微應用的全域性物件,所有的操作都在這個 proxy 物件上,這就是 JS 沙箱的原理

樣式沙箱

通過增強多例模式下的 createElement 方法,負責建立元素並劫持 script、link、style 三個標籤的建立動作

增強 appendChild、insertBefore 方法,負責新增元素,並劫持 script、link、style 三個標籤的新增動作,根據是否是主應用呼叫決定標籤是插入到主應用還是微應用,並且將 proxy 物件傳遞給微應用,作為其全域性物件,以達到 JS 隔離的目的

初始化完成後返回一個 free 函式,會在微應用解除安裝時被呼叫,負責清除 patch、快取動態新增的樣式(因為微應用被解除安裝後所有的相關DOM元素都會被刪掉)

free 函式執行完成後返回 rebuild 函式,在微應用重新掛載時會被呼叫,負責向微應用新增剛才快取的動態樣式

其實嚴格來說這個樣式沙箱有點名不副實,真正的樣式隔離是 嚴格樣式隔離模式 和 scoped css模式 提供的,當然如果開啟了 scoped css,樣式沙箱中動態新增的樣式也會經過 scoped css 的處理

回到正題,樣式沙箱實際做的事情其實很簡單,就是將動態新增的 script、link、style 這三個元素插入到對的位置,屬於主應用的插入主應用,屬於微應用的插入到對應的微應用中,方便微應用解除安裝的時候一起刪除,

當然樣式沙箱還額外做了兩件事:

  • 在解除安裝之前為動態新增樣式做快取,在微應用重新掛載時再插入到微應用內
  • 將 proxy 物件傳遞給 execScripts 函式,將其設定為微應用的執行上下文

以上內容就是對執行時沙箱的一個總結,更加詳細的實現過程,可繼續閱讀下面的原始碼分析部分

原始碼分析

接下來就進入令人頭昏腦脹的原始碼分析部分,說實話,執行時沙箱這段程式碼還是有一些難度的,我在閱讀 qiankun 原始碼的時候,這部分內容反反覆覆閱讀了好幾遍,github

入口位置 - createSandbox

/**
 * 生成執行時沙箱,這個沙箱其實由兩部分組成 => JS 沙箱(執行上下文)、樣式沙箱
 *
 * @param appName 微應用名稱
 * @param elementGetter getter 函式,通過該函式可以獲取 <div id="__qiankun_microapp_wrapper_for_${appInstanceId}__" data-name="${appName}">${template}</div>
 * @param singular 是否單例模式
 * @param scopedCSS
 * @param excludeAssetFilter 指定部分特殊的動態載入的微應用資源(css/js) 不被 qiankun 劫持處理
 */
export function createSandbox(
  appName: string,
  elementGetter: () => HTMLElement | ShadowRoot,
  singular: boolean,
  scopedCSS: boolean,
  excludeAssetFilter?: (url: string) => boolean,
) {
  /**
   * JS 沙箱,通過 proxy 代理 window 物件,記錄 window 物件上屬性的增刪改查,區別在於:
   *  單例模式直接代理了原生 window 物件,記錄原生 window 物件的增刪改查,當 window 物件啟用時恢復 window 物件到上次即將失活時的狀態,
   * 失活時恢復 window 物件到初始初始狀態
   *  多例模式代理了一個全新的物件,這個物件是複製的 window 物件的一部分不可配置屬性,所有的更改都是基於這個 fakeWindow 物件,從而保證多個例項
   * 之間屬性互不影響
   * 後面會將 sandbox.proxy 作為微應用的全域性物件,所有的操作都在這個 proxy 物件上,這就是 JS 沙箱的原理
   */
  let sandbox: SandBox;
  if (window.Proxy) {
    sandbox = singular ? new LegacySandbox(appName) : new ProxySandbox(appName);
  } else {
    // 不支援 proxy 的瀏覽器,通過 diff 方式實現的沙箱
    sandbox = new SnapshotSandbox(appName);
  }

  /**
   * 樣式沙箱
   * 
   * 增強多例模式下的 createElement 方法,負責建立元素並劫持 script、link、style 三個標籤的建立動作
   * 增強 appendChild、insertBefore 方法,負責新增元素,並劫持 script、link、style 三個標籤的新增動作,做一些特殊的處理 => 
   * 根據是否是主應用呼叫決定標籤是插入到主應用還是微應用,並且將 proxy 物件傳遞給微應用,作為其全域性物件,以達到 JS 隔離的目的
   * 初始化完成後返回 free 函式,會在微應用解除安裝時被呼叫,負責清除 patch、快取動態新增的樣式(因為微應用被解除安裝後所有的相關DOM元素都會被刪掉)
   * free 函式執行完成後返回 rebuild 函式,在微應用重新掛載時會被呼叫,負責向微應用新增剛才快取的動態樣式
   * 
   * 其實嚴格來說這個樣式沙箱有點名不副實,真正的樣式隔離是之前說的 嚴格樣式隔離模式 和 scoped css模式 提供的,當然如果開啟了 scoped css,
   * 樣式沙箱中動態新增的樣式也會經過 scoped css 的處理;回到正題,樣式沙箱實際做的事情其實很簡單,將動態新增的 script、link、style 
   * 這三個元素插入到對的位置,屬於主應用的插入主應用,屬於微應用的插入到對應的微應用中,方便微應用解除安裝的時候一起刪除,
   * 當然樣式沙箱還額外做了兩件事:一、在解除安裝之前為動態新增樣式做快取,在微應用重新掛載時再插入到微應用內,二、將 proxy 物件傳遞給 execScripts
   * 函式,將其設定為微應用的執行上下文
   */
  const bootstrappingFreers = patchAtBootstrapping(
    appName,
    elementGetter,
    sandbox,
    singular,
    scopedCSS,
    excludeAssetFilter,
  );
  // mounting freers are one-off and should be re-init at every mounting time
  // mounting freers 是一次性的,應該在每次掛載時重新初始化
  let mountingFreers: Freer[] = [];

  let sideEffectsRebuilders: Rebuilder[] = [];

  return {
    proxy: sandbox.proxy,

    /**
     * 沙箱被 mount
     * 可能是從 bootstrap 狀態進入的 mount
     * 也可能是從 unmount 之後再次喚醒進入 mount
     * mount 時重建副作用(rebuild 函式),即微應用在被解除安裝時希望重新掛載時做的一些事情,比如重建快取的動態樣式
     */
    async mount() {
      /* ------------------------------------------ 因為有上下文依賴(window),以下程式碼執行順序不能變 ------------------------------------------ */

      /* ------------------------------------------ 1. 啟動/恢復 沙箱------------------------------------------ */
      sandbox.active();

      const sideEffectsRebuildersAtBootstrapping = sideEffectsRebuilders.slice(0, bootstrappingFreers.length);
      const sideEffectsRebuildersAtMounting = sideEffectsRebuilders.slice(bootstrappingFreers.length);

      // must rebuild the side effects which added at bootstrapping firstly to recovery to nature state
      if (sideEffectsRebuildersAtBootstrapping.length) {
        // 微應用再次掛載時重建剛才快取的動態樣式
        sideEffectsRebuildersAtBootstrapping.forEach(rebuild => rebuild());
      }

      /* ------------------------------------------ 2. 開啟全域性變數補丁 ------------------------------------------*/
      // render 沙箱啟動時開始劫持各類全域性監聽,儘量不要在應用初始化階段有 事件監聽/定時器 等副作用
      mountingFreers = patchAtMounting(appName, elementGetter, sandbox, singular, scopedCSS, excludeAssetFilter);

      /* ------------------------------------------ 3. 重置一些初始化時的副作用 ------------------------------------------*/
      // 存在 rebuilder 則表明有些副作用需要重建
      // 現在只看到針對 umi 的那個 patchHistoryListener 有 rebuild 操作
      if (sideEffectsRebuildersAtMounting.length) {
        sideEffectsRebuildersAtMounting.forEach(rebuild => rebuild());
      }

      // clean up rebuilders,解除安裝時會再填充回來
      sideEffectsRebuilders = [];
    },

    /**
     * 恢復 global 狀態,使其能回到應用載入之前的狀態
     */
    // 撤銷初始化和掛載階段打的 patch;快取微應用希望自己再次被掛載時需要做的一些事情(rebuild),比如重建動態樣式表;失活微應用
    async unmount() {
      // record the rebuilders of window side effects (event listeners or timers)
      // note that the frees of mounting phase are one-off as it will be re-init at next mounting
      // 解除安裝時,執行 free 函式,釋放初始化和掛載時打的 patch,儲存所有的 rebuild 函式,在微應用再次掛載時重建通過 patch 做的事情(副作用)
      sideEffectsRebuilders = [...bootstrappingFreers, ...mountingFreers].map(free => free());

      sandbox.inactive();
    },
  };
}

JS 沙箱

SingularProxySandbox 單例 JS 沙箱
/**
 * 基於 Proxy 實現的單例模式下的沙箱,直接操作原生 window 物件,並記錄 window 物件的增刪改查,在每次微應用切換時初始化 window 物件;
 * 啟用時:將 window 物件恢復到上次即將失活時的狀態
 * 失活時:將 window 物件恢復為初始狀態
 * 
 * TODO: 為了相容性 singular 模式下依舊使用該沙箱,等新沙箱穩定之後再切換
 */
export default class SingularProxySandbox implements SandBox {
  // 沙箱期間新增的全域性變數
  private addedPropsMapInSandbox = new Map<PropertyKey, any>();

  // 沙箱期間更新的全域性變數,key 為被更新的屬性,value 為被更新的值
  private modifiedPropsOriginalValueMapInSandbox = new Map<PropertyKey, any>();

  // 持續記錄更新的(新增和修改的)全域性變數的 map,用於在任意時刻做 snapshot
  private currentUpdatedPropsValueMap = new Map<PropertyKey, any>();

  name: string;

  proxy: WindowProxy;

  type: SandBoxType;

  sandboxRunning = true;

  // 啟用沙箱
  active() {
    // 如果沙箱由失活 -> 啟用,則恢復 window 物件到上次失活時的狀態
    if (!this.sandboxRunning) {
      this.currentUpdatedPropsValueMap.forEach((v, p) => setWindowProp(p, v));
    }

    // 切換沙箱狀態為 啟用
    this.sandboxRunning = true;
  }

  // 失活沙箱
  inactive() {
    // 開發環境,列印被改變的全域性屬性
    if (process.env.NODE_ENV === 'development') {
      console.info(`[qiankun:sandbox] ${this.name} modified global properties restore...`, [
        ...this.addedPropsMapInSandbox.keys(),
        ...this.modifiedPropsOriginalValueMapInSandbox.keys(),
      ]);
    }

    // restore global props to initial snapshot
    // 將被更改的全域性屬性再改回去
    this.modifiedPropsOriginalValueMapInSandbox.forEach((v, p) => setWindowProp(p, v));
    // 新增的屬性刪掉
    this.addedPropsMapInSandbox.forEach((_, p) => setWindowProp(p, undefined, true));

    // 切換沙箱狀態為 失活
    this.sandboxRunning = false;
  }

  constructor(name: string) {
    this.name = name;
    this.type = SandBoxType.LegacyProxy;
    const { addedPropsMapInSandbox, modifiedPropsOriginalValueMapInSandbox, currentUpdatedPropsValueMap } = this;

    const self = this;
    const rawWindow = window;
    const fakeWindow = Object.create(null) as Window;

    const proxy = new Proxy(fakeWindow, {
      set(_: Window, p: PropertyKey, value: any): boolean {
        if (self.sandboxRunning) {
          if (!rawWindow.hasOwnProperty(p)) {
            // 屬性不存在,則新增
            addedPropsMapInSandbox.set(p, value);
          } else if (!modifiedPropsOriginalValueMapInSandbox.has(p)) {
            // 如果當前 window 物件存在該屬性,且 record map 中未記錄過,則記錄該屬性初始值,說明是更改已存在的屬性
            const originalValue = (rawWindow as any)[p];
            modifiedPropsOriginalValueMapInSandbox.set(p, originalValue);
          }

          currentUpdatedPropsValueMap.set(p, value);
          // 直接設定原生 window 物件,因為是單例模式,不會有其它的影響
          // eslint-disable-next-line no-param-reassign
          (rawWindow as any)[p] = value;

          return true;
        }

        if (process.env.NODE_ENV === 'development') {
          console.warn(`[qiankun] Set window.${p.toString()} while sandbox destroyed or inactive in ${name}!`);
        }

        // 在 strict-mode 下,Proxy 的 handler.set 返回 false 會丟擲 TypeError,在沙箱解除安裝的情況下應該忽略錯誤
        return true;
      },

      get(_: Window, p: PropertyKey): any {
        // avoid who using window.window or window.self to escape the sandbox environment to touch the really window
        // or use window.top to check if an iframe context
        // see https://github.com/eligrey/FileSaver.js/blob/master/src/FileSaver.js#L13
        if (p === 'top' || p === 'parent' || p === 'window' || p === 'self') {
          return proxy;
        }

        // 直接從原生 window 物件拿資料
        const value = (rawWindow as any)[p];
        return getTargetValue(rawWindow, value);
      },

      // trap in operator
      // see https://github.com/styled-components/styled-components/blob/master/packages/styled-components/src/constants.js#L12
      has(_: Window, p: string | number | symbol): boolean {
        return p in rawWindow;
      },
    });

    this.proxy = proxy;
  }
}

/**
 * 在 window 物件上設定 key value 或 刪除指定屬性(key)
 * @param prop key
 * @param value value
 * @param toDelete 是否刪除
 */
function setWindowProp(prop: PropertyKey, value: any, toDelete?: boolean) {
  if (value === undefined && toDelete) {
    // 刪除 window[key]
    delete (window as any)[prop];
  } else if (isPropConfigurable(window, prop) && typeof prop !== 'symbol') {
    // window[key] = value
    Object.defineProperty(window, prop, { writable: true, configurable: true });
    (window as any)[prop] = value;
  }
}

ProxySandbox 多例 JS 沙箱
// 記錄被啟用的沙箱的數量
let activeSandboxCount = 0;

/**
 * 基於 Proxy 實現的多例模式下的沙箱
 * 通過 proxy 代理 fakeWindow 物件,所有的更改都是基於 fakeWindow,這點和單例不一樣(很重要),
 * 從而保證每個 ProxySandbox 例項之間屬性互不影響
 */
export default class ProxySandbox implements SandBox {
  /** window 值變更記錄 */
  private updatedValueSet = new Set<PropertyKey>();

  name: string;

  type: SandBoxType;

  proxy: WindowProxy;

  sandboxRunning = true;

  // 啟用
  active() {
    // 被啟用的沙箱數 + 1
    if (!this.sandboxRunning) activeSandboxCount++;
    this.sandboxRunning = true;
  }

  // 失活
  inactive() {
    if (process.env.NODE_ENV === 'development') {
      console.info(`[qiankun:sandbox] ${this.name} modified global properties restore...`, [
        ...this.updatedValueSet.keys(),
      ]);
    }

    // 被啟用的沙箱數 - 1
    clearSystemJsProps(this.proxy, --activeSandboxCount === 0);

    this.sandboxRunning = false;
  }

  constructor(name: string) {
    this.name = name;
    this.type = SandBoxType.Proxy;
    const { updatedValueSet } = this;

    const self = this;
    const rawWindow = window;
    // 全域性物件上所有不可配置屬性都在 fakeWindow 中,且其中具有 getter 屬性的屬性還存在 propertesWithGetter map 中,value 為 true
    const { fakeWindow, propertiesWithGetter } = createFakeWindow(rawWindow);

    const descriptorTargetMap = new Map<PropertyKey, SymbolTarget>();
    // 判斷全域性物件是否存在指定屬性
    const hasOwnProperty = (key: PropertyKey) => fakeWindow.hasOwnProperty(key) || rawWindow.hasOwnProperty(key);

    const proxy = new Proxy(fakeWindow, {
      set(target: FakeWindow, p: PropertyKey, value: any): boolean {
        // 如果沙箱在執行,則更新屬性值並記錄被更改的屬性
        if (self.sandboxRunning) {
          // 設定屬性值
          // @ts-ignore
          target[p] = value;
          // 記錄被更改的屬性
          updatedValueSet.add(p);

          // 不用管,和 systemJs 有關
          interceptSystemJsProps(p, value);

          return true;
        }

        if (process.env.NODE_ENV === 'development') {
          console.warn(`[qiankun] Set window.${p.toString()} while sandbox destroyed or inactive in ${name}!`);
        }

        // 在 strict-mode 下,Proxy 的 handler.set 返回 false 會丟擲 TypeError,在沙箱解除安裝的情況下應該忽略錯誤
        return true;
      },

      // 獲取執行屬性的值
      get(target: FakeWindow, p: PropertyKey): any {
        if (p === Symbol.unscopables) return unscopables;

        // avoid who using window.window or window.self to escape the sandbox environment to touch the really window
        // see https://github.com/eligrey/FileSaver.js/blob/master/src/FileSaver.js#L13
        if (p === 'window' || p === 'self') {
          return proxy;
        }

        if (
          p === 'top' ||
          p === 'parent' ||
          (process.env.NODE_ENV === 'test' && (p === 'mockTop' || p === 'mockSafariTop'))
        ) {
          // if your master app in an iframe context, allow these props escape the sandbox
          if (rawWindow === rawWindow.parent) {
            return proxy;
          }
          return (rawWindow as any)[p];
        }

        // proxy.hasOwnProperty would invoke getter firstly, then its value represented as rawWindow.hasOwnProperty
        if (p === 'hasOwnProperty') {
          return hasOwnProperty;
        }

        // mark the symbol to document while accessing as document.createElement could know is invoked by which sandbox for dynamic append patcher
        if (p === 'document') {
          document[attachDocProxySymbol] = proxy;
          // remove the mark in next tick, thus we can identify whether it in micro app or not
          // this approach is just a workaround, it could not cover all the complex scenarios, such as the micro app runs in the same task context with master in som case
          // fixme if you have any other good ideas
          nextTick(() => delete document[attachDocProxySymbol]);
          return document;
        }
        // 以上內容都是一些特殊屬性的處理

        // 獲取特定屬性,如果屬性具有 getter,說明是原生物件的那幾個屬性,否則是 fakeWindow 物件上的屬性(原生的或者使用者設定的)
        // eslint-disable-next-line no-bitwise
        const value = propertiesWithGetter.has(p) ? (rawWindow as any)[p] : (target as any)[p] || (rawWindow as any)[p];
        return getTargetValue(rawWindow, value);
      },

      // 判斷是否存在指定屬性
      // see https://github.com/styled-components/styled-components/blob/master/packages/styled-components/src/constants.js#L12
      has(target: FakeWindow, p: string | number | symbol): boolean {
        return p in unscopables || p in target || p in rawWindow;
      },

      getOwnPropertyDescriptor(target: FakeWindow, p: string | number | symbol): PropertyDescriptor | undefined {
        /*
         as the descriptor of top/self/window/mockTop in raw window are configurable but not in proxy target, we need to get it from target to avoid TypeError
         see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/getOwnPropertyDescriptor
         > A property cannot be reported as non-configurable, if it does not exists as an own property of the target object or if it exists as a configurable own property of the target object.
         */
        if (target.hasOwnProperty(p)) {
          const descriptor = Object.getOwnPropertyDescriptor(target, p);
          descriptorTargetMap.set(p, 'target');
          return descriptor;
        }

        if (rawWindow.hasOwnProperty(p)) {
          const descriptor = Object.getOwnPropertyDescriptor(rawWindow, p);
          descriptorTargetMap.set(p, 'rawWindow');
          // A property cannot be reported as non-configurable, if it does not exists as an own property of the target object
          if (descriptor && !descriptor.configurable) {
            descriptor.configurable = true;
          }
          return descriptor;
        }

        return undefined;
      },

      // trap to support iterator with sandbox
      ownKeys(target: FakeWindow): PropertyKey[] {
        return uniq(Reflect.ownKeys(rawWindow).concat(Reflect.ownKeys(target)));
      },

      defineProperty(target: Window, p: PropertyKey, attributes: PropertyDescriptor): boolean {
        const from = descriptorTargetMap.get(p);
        /*
         Descriptor must be defined to native window while it comes from native window via Object.getOwnPropertyDescriptor(window, p),
         otherwise it would cause a TypeError with illegal invocation.
         */
        switch (from) {
          case 'rawWindow':
            return Reflect.defineProperty(rawWindow, p, attributes);
          default:
            return Reflect.defineProperty(target, p, attributes);
        }
      },

      deleteProperty(target: FakeWindow, p: string | number | symbol): boolean {
        if (target.hasOwnProperty(p)) {
          // @ts-ignore
          delete target[p];
          updatedValueSet.delete(p);

          return true;
        }

        return true;
      },
    });

    this.proxy = proxy;
  }
}

createFakeWindow
/**
 * 拷貝全域性物件上所有不可配置屬性到 fakeWindow 物件,並將這些屬性的屬性描述符改為可配置的然後凍結
 * 將啟動具有 getter 屬性的屬性再存入 propertiesWithGetter map 中
 * @param global 全域性物件 => window
 */
function createFakeWindow(global: Window) {
  // 記錄 window 物件上的 getter 屬性,原生的有:window、document、location、top,比如:Object.getOwnPropertyDescriptor(window, 'window') => {set: undefined, enumerable: true, configurable: false, get: ƒ}
  // propertiesWithGetter = {"window" => true, "document" => true, "location" => true, "top" => true, "__VUE_DEVTOOLS_GLOBAL_HOOK__" => true}
  const propertiesWithGetter = new Map<PropertyKey, boolean>();
  // 儲存 window 物件中所有不可配置的屬性和值
  const fakeWindow = {} as FakeWindow;

  /*
   copy the non-configurable property of global to fakeWindow
   see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/getOwnPropertyDescriptor
   > A property cannot be reported as non-configurable, if it does not exists as an own property of the target object or if it exists as a configurable own property of the target object.
   */
  Object.getOwnPropertyNames(global)
    // 遍歷出 window 物件所有不可配置屬性
    .filter(p => {
      const descriptor = Object.getOwnPropertyDescriptor(global, p);
      return !descriptor?.configurable;
    })
    .forEach(p => {
      // 得到屬性描述符
      const descriptor = Object.getOwnPropertyDescriptor(global, p);
      if (descriptor) {
        // 獲取其 get 屬性
        const hasGetter = Object.prototype.hasOwnProperty.call(descriptor, 'get');

        /*
         make top/self/window property configurable and writable, otherwise it will cause TypeError while get trap return.
         see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/get
         > The value reported for a property must be the same as the value of the corresponding target object property if the target object property is a non-writable, non-configurable data property.
         */
        if (
          p === 'top' ||
          p === 'parent' ||
          p === 'self' ||
          p === 'window' ||
          (process.env.NODE_ENV === 'test' && (p === 'mockTop' || p === 'mockSafariTop'))
        ) {
          // 將 top、parent、self、window 這幾個屬性由不可配置改為可配置
          descriptor.configurable = true;
          /*
           The descriptor of window.window/window.top/window.self in Safari/FF are accessor descriptors, we need to avoid adding a data descriptor while it was
           Example:
            Safari/FF: Object.getOwnPropertyDescriptor(window, 'top') -> {get: function, set: undefined, enumerable: true, configurable: false}
            Chrome: Object.getOwnPropertyDescriptor(window, 'top') -> {value: Window, writable: false, enumerable: true, configurable: false}
           */
          if (!hasGetter) {
            // 如果這幾個屬性沒有 getter,則說明由 writeable 屬性,將其設定為可寫
            descriptor.writable = true;
          }
        }

        // 如果存在 getter,則以該屬性為 key,true 為 value 存入 propertiesWithGetter map
        if (hasGetter) propertiesWithGetter.set(p, true);

        // 將屬性和描述設定到 fakeWindow 物件,並且凍結屬性描述符,不然有可能會被更改,比如 zone.js
        // freeze the descriptor to avoid being modified by zone.js
        // see https://github.com/angular/zone.js/blob/a5fe09b0fac27ac5df1fa746042f96f05ccb6a00/lib/browser/define-property.ts#L71
        rawObjectDefineProperty(fakeWindow, p, Object.freeze(descriptor));
      }
    });

  return {
    fakeWindow,
    propertiesWithGetter,
  };
}
            
SnapshotSandbox
function iter(obj: object, callbackFn: (prop: any) => void) {
  // eslint-disable-next-line guard-for-in, no-restricted-syntax
  for (const prop in obj) {
    if (obj.hasOwnProperty(prop)) {
      callbackFn(prop);
    }
  }
}

/**
 * 基於 diff 方式實現的沙箱,用於不支援 Proxy 的低版本瀏覽器
 */
export default class SnapshotSandbox implements SandBox {
  proxy: WindowProxy;

  name: string;

  type: SandBoxType;

  sandboxRunning = true;

  private windowSnapshot!: Window;

  private modifyPropsMap: Record<any, any> = {};

  constructor(name: string) {
    this.name = name;
    this.proxy = window;
    this.type = SandBoxType.Snapshot;
  }

  active() {
    // 記錄當前快照
    this.windowSnapshot = {} as Window;
    iter(window, prop => {
      this.windowSnapshot[prop] = window[prop];
    });

    // 恢復之前的變更
    Object.keys(this.modifyPropsMap).forEach((p: any) => {
      window[p] = this.modifyPropsMap[p];
    });

    this.sandboxRunning = true;
  }

  inactive() {
    this.modifyPropsMap = {};

    iter(window, prop => {
      if (window[prop] !== this.windowSnapshot[prop]) {
        // 記錄變更,恢復環境
        this.modifyPropsMap[prop] = window[prop];
        window[prop] = this.windowSnapshot[prop];
      }
    });

    if (process.env.NODE_ENV === 'development') {
      console.info(`[qiankun:sandbox] ${this.name} origin window restore...`, Object.keys(this.modifyPropsMap));
    }

    this.sandboxRunning = false;
  }
}

樣式沙箱

patchAtBootstrapping
/**
 * 初始化階段給 createElement、appendChild、insertBefore 三個方法打一個 patch
 * @param appName 
 * @param elementGetter 
 * @param sandbox 
 * @param singular 
 * @param scopedCSS 
 * @param excludeAssetFilter 
 */
export function patchAtBootstrapping(
  appName: string,
  elementGetter: () => HTMLElement | ShadowRoot,
  sandbox: SandBox,
  singular: boolean,
  scopedCSS: boolean,
  excludeAssetFilter?: Function,
): Freer[] {
  // 基礎 patch,增強 createElement、appendChild、insertBefore 三個方法
  const basePatchers = [
    () => patchDynamicAppend(appName, elementGetter, sandbox.proxy, false, singular, scopedCSS, excludeAssetFilter),
  ];

  // 每一種沙箱都需要打基礎 patch
  const patchersInSandbox = {
    [SandBoxType.LegacyProxy]: basePatchers,
    [SandBoxType.Proxy]: basePatchers,
    [SandBoxType.Snapshot]: basePatchers,
  };

  // 返回一個陣列,陣列元素是 patch 的執行結果 => free 函式
  return patchersInSandbox[sandbox.type]?.map(patch => patch());
}

patch
/**
 * 增強多例模式下的 createElement 方法,負責建立元素並劫持 script、link、style 三個標籤的建立動作
 * 增強 appendChild、insertBefore 方法,負責新增元素,並劫持 script、link、style 三個標籤的新增動作,做一些特殊的處理 => 
 * 根據是否是主應用呼叫決定標籤是插入到主應用還是微應用,並且將 proxy 物件傳遞給微應用,作為其全域性物件,以打包 JS 隔離的目的
 * 初始化完成後返回 free 函式,負責清除 patch、快取動態新增的樣式(因為微應用被解除安裝後所有的相關DOM元素都會被刪掉)
 * free 函式執行完成後返回 rebuild 函式,rebuild 函式在微應用重新掛載時向微應用新增剛才快取的動態樣式
 * 
 * Just hijack dynamic head append, that could avoid accidentally hijacking the insertion of elements except in head.
 * Such a case: ReactDOM.createPortal(<style>.test{color:blue}</style>, container),
 * this could made we append the style element into app wrapper but it will cause an error while the react portal unmounting, as ReactDOM could not find the style in body children list.
 * @param appName 微應用名稱
 * @param appWrapperGetter getter 函式,通過該函式可以獲取 <div id="__qiankun_microapp_wrapper_for_${appInstanceId}__" data-name="${appName}">${template}</div>
 * @param proxy window 代理
 * @param mounting 是否為 mounting 階段
 * @param singular 是否為單例
 * @param scopedCSS 是否棄用 scoped css
 * @param excludeAssetFilter 指定部分特殊的動態載入的微應用資源(css/js) 不被 qiankun 劫持處理
 */
export default function patch(
  appName: string,
  appWrapperGetter: () => HTMLElement | ShadowRoot,
  proxy: Window,
  mounting = true,
  singular = true,
  scopedCSS = false,
  excludeAssetFilter?: CallableFunction,
): Freer {
  // 動態樣式表,儲存所有動態新增的樣式
  let dynamicStyleSheetElements: Array<HTMLLinkElement | HTMLStyleElement> = [];

  // 在多例模式下增強 createElement 方法,讓其除了可以建立元素,還可以了劫持建立 script、link、style 元素的情況
  const unpatchDocumentCreate = patchDocumentCreateElement(
    appName,
    appWrapperGetter,
    singular,
    proxy,
    dynamicStyleSheetElements,
  );

  // 增強 appendChild、insertBefore、removeChild 三個元素;除了本職工作之外,appendChild 和 insertBefore 還可以額外處理 script、style、link
  // 三個標籤的插入,可以根據情況決定元素被插入到微應用模版空間中還是主應用模版空間,removeChild 也是可以根據情況移除主應用的元素還是移除微應用中這三個元素
  const unpatchDynamicAppendPrototypeFunctions = patchHTMLDynamicAppendPrototypeFunctions(
    appName,
    appWrapperGetter,
    proxy,
    singular,
    scopedCSS,
    dynamicStyleSheetElements,
    excludeAssetFilter,
  );

  // 記錄初始化的次數
  if (!mounting) bootstrappingPatchCount++;
  // 記錄掛載的次數
  if (mounting) mountingPatchCount++;

  // 初始化完成後返回 free 函式,負責清除 patch、快取動態新增的樣式、返回 rebuild 函式,rebuild 函式在微應用重新掛載時向微應用新增剛才快取的動態樣式
  return function free() {
    // bootstrap patch just called once but its freer will be called multiple times
    if (!mounting && bootstrappingPatchCount !== 0) bootstrappingPatchCount--;
    if (mounting) mountingPatchCount--;

    // 判斷所有微應用是否都被解除安裝了
    const allMicroAppUnmounted = mountingPatchCount === 0 && bootstrappingPatchCount === 0;
    // 微應用都解除安裝以後移除 patch, release the overwrite prototype after all the micro apps unmounted
    unpatchDynamicAppendPrototypeFunctions(allMicroAppUnmounted);
    unpatchDocumentCreate(allMicroAppUnmounted);

    // 因為微應用被解除安裝的時候會刪除掉剛才動態新增的樣式,這裡快取了動態新增的樣式內容,在微應用解除安裝後重新掛載時就可以用了
    dynamicStyleSheetElements.forEach(stylesheetElement => {
      if (stylesheetElement instanceof HTMLStyleElement && isStyledComponentsLike(stylesheetElement)) {
        if (stylesheetElement.sheet) {
          // record the original css rules of the style element for restore
          setCachedRules(stylesheetElement, (stylesheetElement.sheet as CSSStyleSheet).cssRules);
        }
      }
    });

    // 返回一個 rebuild 函式,微應用重新掛載時呼叫
    return function rebuild() {
      // 遍歷動態樣式表
      dynamicStyleSheetElements.forEach(stylesheetElement => {
        // 像微應用容器中新增樣式節點
        document.head.appendChild.call(appWrapperGetter(), stylesheetElement);

        // 新增樣式內容到樣式節點,這個樣式內容從剛才的快取中找
        if (stylesheetElement instanceof HTMLStyleElement && isStyledComponentsLike(stylesheetElement)) {
          const cssRules = getCachedRules(stylesheetElement);
          if (cssRules) {
            // eslint-disable-next-line no-plusplus
            for (let i = 0; i < cssRules.length; i++) {
              const cssRule = cssRules[i];
              (stylesheetElement.sheet as CSSStyleSheet).insertRule(cssRule.cssText);
            }
          }
        }
      });

      // As the hijacker will be invoked every mounting phase, we could release the cache for gc after rebuilding
      if (mounting) {
        dynamicStyleSheetElements = [];
      }
    };
  };
}

patchDocumentCreateElement
/**
 * 多例模式下增強 createElement 方法,讓其除了具有建立元素功能之外,還可以劫持建立 script、link、style 這三個元素的情況
 * @param appName 微應用名稱
 * @param appWrapperGetter 
 * @param singular 
 * @param proxy 
 * @param dynamicStyleSheetElements 
 */
function patchDocumentCreateElement(
  appName: string,
  appWrapperGetter: () => HTMLElement | ShadowRoot,
  singular: boolean,
  proxy: Window,
  dynamicStyleSheetElements: HTMLStyleElement[],
) {
  // 如果是單例模式直接 return
  if (singular) {
    return noop;
  }

  // 以微應用執行時的 proxy 為 key,儲存該微應用的一些資訊,比如 名稱、proxy、微應用模版、自定義樣式表等
  proxyContainerInfoMapper.set(proxy, { appName, proxy, appWrapperGetter, dynamicStyleSheetElements, singular });

  // 第一個微應用初始化時會執行這段,增強 createElement 方法,讓其除了可以建立元素之外,還可以劫持 script、link、style 三個標籤的建立動作
  if (Document.prototype.createElement === rawDocumentCreateElement) {
    Document.prototype.createElement = function createElement<K extends keyof HTMLElementTagNameMap>(
      this: Document,
      tagName: K,
      options?: ElementCreationOptions,
    ): HTMLElement {
      // 建立元素
      const element = rawDocumentCreateElement.call(this, tagName, options);
      // 劫持 script、link、style 三種標籤
      if (isHijackingTag(tagName)) {
        // 下面這段似乎沒啥用,因為沒發現有哪個地方執行設定,proxyContainerInfoMapper.set(this[attachDocProxySysbol])
        // 獲取這個東西的值,然後將該值新增到 element 物件上,以 attachElementContainerSymbol 為 key
        const proxyContainerInfo = proxyContainerInfoMapper.get(this[attachDocProxySymbol]);
        if (proxyContainerInfo) {
          Object.defineProperty(element, attachElementContainerSymbol, {
            value: proxyContainerInfo,
            enumerable: false,
          });
        }
      }

      // 返回建立的元素
      return element;
    };
  }

  // 後續的微應用初始化時直接返回該函式,負責還原 createElement 方法
  return function unpatch(recoverPrototype: boolean) {
    proxyContainerInfoMapper.delete(proxy);
    if (recoverPrototype) {
      Document.prototype.createElement = rawDocumentCreateElement;
    }
  };
}

patchTHMLDynamicAppendPrototypeFunctions
// 增強 appendChild、insertBefore、removeChild 方法,返回 unpatch 方法,解除增強
function patchHTMLDynamicAppendPrototypeFunctions(
  appName: string,
  appWrapperGetter: () => HTMLElement | ShadowRoot,
  proxy: Window,
  singular = true,
  scopedCSS = false,
  dynamicStyleSheetElements: HTMLStyleElement[],
  excludeAssetFilter?: CallableFunction,
) {
  // Just overwrite it while it have not been overwrite
  if (
    HTMLHeadElement.prototype.appendChild === rawHeadAppendChild &&
    HTMLBodyElement.prototype.appendChild === rawBodyAppendChild &&
    HTMLHeadElement.prototype.insertBefore === rawHeadInsertBefore
  ) {
    // 增強 appendChild 方法
    HTMLHeadElement.prototype.appendChild = getOverwrittenAppendChildOrInsertBefore({
      rawDOMAppendOrInsertBefore: rawHeadAppendChild,
      appName,
      appWrapperGetter,
      proxy,
      singular,
      dynamicStyleSheetElements,
      scopedCSS,
      excludeAssetFilter,
    }) as typeof rawHeadAppendChild;
    HTMLBodyElement.prototype.appendChild = getOverwrittenAppendChildOrInsertBefore({
      rawDOMAppendOrInsertBefore: rawBodyAppendChild,
      appName,
      appWrapperGetter,
      proxy,
      singular,
      dynamicStyleSheetElements,
      scopedCSS,
      excludeAssetFilter,
    }) as typeof rawBodyAppendChild;

    HTMLHeadElement.prototype.insertBefore = getOverwrittenAppendChildOrInsertBefore({
      rawDOMAppendOrInsertBefore: rawHeadInsertBefore as any,
      appName,
      appWrapperGetter,
      proxy,
      singular,
      dynamicStyleSheetElements,
      scopedCSS,
      excludeAssetFilter,
    }) as typeof rawHeadInsertBefore;
  }

  // Just overwrite it while it have not been overwrite
  if (
    HTMLHeadElement.prototype.removeChild === rawHeadRemoveChild &&
    HTMLBodyElement.prototype.removeChild === rawBodyRemoveChild
  ) {
    HTMLHeadElement.prototype.removeChild = getNewRemoveChild({
      appWrapperGetter,
      headOrBodyRemoveChild: rawHeadRemoveChild,
    });
    HTMLBodyElement.prototype.removeChild = getNewRemoveChild({
      appWrapperGetter,
      headOrBodyRemoveChild: rawBodyRemoveChild,
    });
  }

  return function unpatch(recoverPrototype: boolean) {
    if (recoverPrototype) {
      HTMLHeadElement.prototype.appendChild = rawHeadAppendChild;
      HTMLHeadElement.prototype.removeChild = rawHeadRemoveChild;
      HTMLBodyElement.prototype.appendChild = rawBodyAppendChild;
      HTMLBodyElement.prototype.removeChild = rawBodyRemoveChild;

      HTMLHeadElement.prototype.insertBefore = rawHeadInsertBefore;
    }
  };
}

getOverwrittenAppendChildOrInsertBefore
/**
 * 增強 appendChild 和 insertBefore 方法,讓其除了具有新增元素的功能之外,還具有一些其它的邏輯,比如:
 * 根據是否是微應用或者特殊元素決定 link、style、script 元素的插入位置是在主應用還是微應用
 * 劫持 script 標籤的新增,支援遠端載入指令碼和設定指令碼的執行上下文(proxy)
 * @param opts 
 */
function getOverwrittenAppendChildOrInsertBefore(opts: {
  appName: string;
  proxy: WindowProxy;
  singular: boolean;
  dynamicStyleSheetElements: HTMLStyleElement[];
  appWrapperGetter: CallableFunction;
  rawDOMAppendOrInsertBefore: <T extends Node>(newChild: T, refChild?: Node | null) => T;
  scopedCSS: boolean;
  excludeAssetFilter?: CallableFunction;
}) {
  return function appendChildOrInsertBefore<T extends Node>(
    this: HTMLHeadElement | HTMLBodyElement,
    newChild: T,
    refChild?: Node | null,
  ) {
    // 要插入的元素
    let element = newChild as any;
    // 原始方法
    const { rawDOMAppendOrInsertBefore } = opts;
    if (element.tagName) {
      // 解析引數
      // eslint-disable-next-line prefer-const
      let { appName, appWrapperGetter, proxy, singular, dynamicStyleSheetElements } = opts;
      const { scopedCSS, excludeAssetFilter } = opts;

      // 多例模式會走的一段邏輯
      const storedContainerInfo = element[attachElementContainerSymbol];
      if (storedContainerInfo) {
        // eslint-disable-next-line prefer-destructuring
        appName = storedContainerInfo.appName;
        // eslint-disable-next-line prefer-destructuring
        singular = storedContainerInfo.singular;
        // eslint-disable-next-line prefer-destructuring
        appWrapperGetter = storedContainerInfo.appWrapperGetter;
        // eslint-disable-next-line prefer-destructuring
        dynamicStyleSheetElements = storedContainerInfo.dynamicStyleSheetElements;
        // eslint-disable-next-line prefer-destructuring
        proxy = storedContainerInfo.proxy;
      }

      const invokedByMicroApp = singular
        ? // check if the currently specified application is active
          // While we switch page from qiankun app to a normal react routing page, the normal one may load stylesheet dynamically while page rendering,
          // but the url change listener must to wait until the current call stack is flushed.
          // This scenario may cause we record the stylesheet from react routing page dynamic injection,
          // and remove them after the url change triggered and qiankun app is unmouting
          // see https://github.com/ReactTraining/history/blob/master/modules/createHashHistory.js#L222-L230
          checkActivityFunctions(window.location).some(name => name === appName)
        : // have storedContainerInfo means it invoked by a micro app in multiply mode
          !!storedContainerInfo;

      switch (element.tagName) {
        // link 和 style
        case LINK_TAG_NAME:
        case STYLE_TAG_NAME: {
          // 斷言,newChild 為 style 或者 link 標籤
          const stylesheetElement: HTMLLinkElement | HTMLStyleElement = newChild as any;
          // href 屬性
          const { href } = stylesheetElement as HTMLLinkElement;
          if (!invokedByMicroApp || (excludeAssetFilter && href && excludeAssetFilter(href))) {
            // 進來則說明,這個建立元素的動作不是微應用呼叫的,或者是一個特殊指定不希望被 qiankun 劫持的 link 標籤
            // 將其建立到主應用的下
            return rawDOMAppendOrInsertBefore.call(this, element, refChild) as T;
          }

          // 微應用容器 DOM
          const mountDOM = appWrapperGetter();

          // scoped css
          if (scopedCSS) {
            css.process(mountDOM, stylesheetElement, appName);
          }

          // 將該元素存到樣式表中
          // eslint-disable-next-line no-shadow
          dynamicStyleSheetElements.push(stylesheetElement);
          // 參考元素
          const referenceNode = mountDOM.contains(refChild) ? refChild : null;
          // 將該元素在微應用的空間中建立,這樣解除安裝微應用的時候就可以直接一起刪除了
          return rawDOMAppendOrInsertBefore.call(mountDOM, stylesheetElement, referenceNode);
        }

        // script 標籤
        case SCRIPT_TAG_NAME: {
          // 連結和文字
          const { src, text } = element as HTMLScriptElement;
          // some script like jsonp maybe not support cors which should't use execScripts
          if (!invokedByMicroApp || (excludeAssetFilter && src && excludeAssetFilter(src))) {
            // 同理,將該標籤建立到主應用下
            return rawDOMAppendOrInsertBefore.call(this, element, refChild) as T;
          }

          // 微應用容器 DOM
          const mountDOM = appWrapperGetter();
          // 使用者提供的 fetch 方法
          const { fetch } = frameworkConfiguration;
          // 參考節點
          const referenceNode = mountDOM.contains(refChild) ? refChild : null;

          // 如果 src 存在,則說明是一個外聯指令碼
          if (src) {
            // 執行遠端載入,將 proxy 設定為指令碼的全域性物件,來達到 JS 隔離的目的
            execScripts(null, [src], proxy, {
              fetch,
              strictGlobal: !singular,
              beforeExec: () => {
                Object.defineProperty(document, 'currentScript', {
                  get(): any {
                    return element;
                  },
                  configurable: true,
                });
              },
              success: () => {
                // we need to invoke the onload event manually to notify the event listener that the script was completed
                // here are the two typical ways of dynamic script loading
                // 1. element.onload callback way, which webpack and loadjs used, see https://github.com/muicss/loadjs/blob/master/src/loadjs.js#L138
                // 2. addEventListener way, which toast-loader used, see https://github.com/pyrsmk/toast/blob/master/src/Toast.ts#L64
                const loadEvent = new CustomEvent('load');
                if (isFunction(element.onload)) {
                  element.onload(patchCustomEvent(loadEvent, () => element));
                } else {
                  element.dispatchEvent(loadEvent);
                }

                element = null;
              },
              error: () => {
                const errorEvent = new CustomEvent('error');
                if (isFunction(element.onerror)) {
                  element.onerror(patchCustomEvent(errorEvent, () => element));
                } else {
                  element.dispatchEvent(errorEvent);
                }

                element = null;
              },
            });

            // 建立一個註釋元素,表示該 script 標籤被 qiankun 劫持處理了
            const dynamicScriptCommentElement = document.createComment(`dynamic script ${src} replaced by qiankun`);
            return rawDOMAppendOrInsertBefore.call(mountDOM, dynamicScriptCommentElement, referenceNode);
          }

          // 說明該 script 是一個內聯指令碼
          execScripts(null, [`<script>${text}</script>`], proxy, {
            strictGlobal: !singular,
            success: element.onload,
            error: element.onerror,
          });
          // 建立一個註釋元素,表示該 script 標籤被 qiankun 劫持處理了
          const dynamicInlineScriptCommentElement = document.createComment('dynamic inline script replaced by qiankun');
          return rawDOMAppendOrInsertBefore.call(mountDOM, dynamicInlineScriptCommentElement, referenceNode);
        }

        default:
          break;
      }
    }

    // 呼叫原始方法,插入元素
    return rawDOMAppendOrInsertBefore.call(this, element, refChild);
  };
}

getNewRemoveChild
/**
 * 增強 removeChild,讓其可以根據情況決定是從主應用中移除指定元素,還是從微應用中移除 script、style、link 元素
 * 如果是被劫持元素,則從微應用中移除,否則從主應用中移除
 * @param opts 
 */
function getNewRemoveChild(opts: {
  appWrapperGetter: CallableFunction;
  headOrBodyRemoveChild: typeof HTMLElement.prototype.removeChild;
}) {
  return function removeChild<T extends Node>(this: HTMLHeadElement | HTMLBodyElement, child: T) {
    // 原始的 removeChild
    const { headOrBodyRemoveChild } = opts;
    try {
      const { tagName } = child as any;
      // 當移除的元素是 script、link、style 之一時特殊處理
      if (isHijackingTag(tagName)) {
        // 微應用容器空間
        let { appWrapperGetter } = opts;

        // 新建時設定的,storedContainerInfo 包含了微應用的一些資訊,不過 storedContainerInfo 應該是始終為 undefeind,因為設定位置的程式碼似乎永遠不會被執行
        const storedContainerInfo = (child as any)[attachElementContainerSymbol];
        if (storedContainerInfo) {
          // eslint-disable-next-line prefer-destructuring
          // 微應用的包裹元素,也可以說微應用模版
          appWrapperGetter = storedContainerInfo.appWrapperGetter;
        }

        // 從微應用容器空間中移除該元素
        // container may had been removed while app unmounting if the removeChild action was async
        const container = appWrapperGetter();
        if (container.contains(child)) {
          return rawRemoveChild.call(container, child) as T;
        }
      }
    } catch (e) {
      console.warn(e);
    }

    // 從主應用中移除元素
    return headOrBodyRemoveChild.call(this, child) as T;
  };
}

patchAtMounting

在微應用的掛載階段會被呼叫,主要負責給各個全域性變數(方法)打 patch

export function patchAtMounting(
  appName: string,
  elementGetter: () => HTMLElement | ShadowRoot,
  sandbox: SandBox,
  singular: boolean,
  scopedCSS: boolean,
  excludeAssetFilter?: Function,
): Freer[] {
  const basePatchers = [
    // 定時器 patch
    () => patchInterval(sandbox.proxy),
    // 事件監聽 patch
    () => patchWindowListener(sandbox.proxy),
    // fix umi bug
    () => patchHistoryListener(),
    // 初始化階段時的那個 patch
    () => patchDynamicAppend(appName, elementGetter, sandbox.proxy, true, singular, scopedCSS, excludeAssetFilter),
  ];

  const patchersInSandbox = {
    [SandBoxType.LegacyProxy]: [...basePatchers],
    [SandBoxType.Proxy]: [...basePatchers],
    [SandBoxType.Snapshot]: basePatchers,
  };

  return patchersInSandbox[sandbox.type]?.map(patch => patch());
}

patch => patchInterval
/**
 * 定時器 patch,設定定時器時自動記錄定時器 id,清除定時器時自動刪除已清除的定時器 id,釋放 patch 時自動清除所有未被清除的定時器,並恢復定時器方法
 * @param global = windowProxy
 */
export default function patch(global: Window) {
  let intervals: number[] = [];

  // 清除定時器,並從 intervals 中清除已經清除的 定時器 id
  global.clearInterval = (intervalId: number) => {
    intervals = intervals.filter(id => id !== intervalId);
    return rawWindowClearInterval(intervalId);
  };

  // 設定定時器,並記錄定時器的 id
  global.setInterval = (handler: Function, timeout?: number, ...args: any[]) => {
    const intervalId = rawWindowInterval(handler, timeout, ...args);
    intervals = [...intervals, intervalId];
    return intervalId;
  };

  // 清除所有的定時器,並恢復定時器方法
  return function free() {
    intervals.forEach(id => global.clearInterval(id));
    global.setInterval = rawWindowInterval;
    global.clearInterval = rawWindowClearInterval;

    return noop;
  };
}

patch => patchWindowListener
/**
 * 監聽器 patch,新增事件監聽時自動記錄事件的回撥函式,移除時自動刪除事件的回撥函式,釋放 patch 時自動刪除所有的事件監聽,並恢復監聽函式
 * @param global windowProxy
 */
export default function patch(global: WindowProxy) {
  // 記錄各個事件的回撥函式
  const listenerMap = new Map<string, EventListenerOrEventListenerObject[]>();

  // 設定監聽器
  global.addEventListener = (
    type: string,
    listener: EventListenerOrEventListenerObject,
    options?: boolean | AddEventListenerOptions,
  ) => {
    // 從 listenerMap 中獲取已經存在的該事件的回撥函式
    const listeners = listenerMap.get(type) || [];
    // 儲存該事件的所有回撥函式
    listenerMap.set(type, [...listeners, listener]);
    // 設定監聽
    return rawAddEventListener.call(window, type, listener, options);
  };

  // 移除監聽器
  global.removeEventListener = (
    type: string,
    listener: EventListenerOrEventListenerObject,
    options?: boolean | AddEventListenerOptions,
  ) => {
    // 從 listenerMap 中移除該事件的指定回撥函式
    const storedTypeListeners = listenerMap.get(type);
    if (storedTypeListeners && storedTypeListeners.length && storedTypeListeners.indexOf(listener) !== -1) {
      storedTypeListeners.splice(storedTypeListeners.indexOf(listener), 1);
    }
    // 移除事件監聽
    return rawRemoveEventListener.call(window, type, listener, options);
  };

  // 釋放 patch,移除所有的事件監聽
  return function free() {
    // 移除所有的事件監聽
    listenerMap.forEach((listeners, type) =>
      [...listeners].forEach(listener => global.removeEventListener(type, listener)),
    );
    // 恢復監聽函式
    global.addEventListener = rawAddEventListener;
    global.removeEventListener = rawRemoveEventListener;

    return noop;
  };
}

連結

  • 微前端專欄

    • 微前端框架 之 qiankun 從入門到原始碼分析

    • HTML Entry 原始碼分析

    • 微前端框架 之 single-spa 從入門到精通

  • github

感謝各位的:點贊收藏評論,我們下期見。


當學習成為了習慣,知識也就變成了常識,掃碼關注微信公眾號,共同學習、進步。文章已收錄到 github,歡迎 Watch 和 Star。

微信公眾號

相關文章