Stories

Detail Return Return

【微前端】qiankun v2.10.16(流程圖)源碼解析 - Stories Detail

整體核心流程

qiankun

源碼分析

single-spa 存在以下主要的缺點

  • 路由狀態管理不足:無法保持路由狀態,頁面刷新後路由狀態丟失
  • 父子應用間的路由交互以來 postMessage 等方式,開發體驗差
  • 未提供原生的 CSS 和 JS 沙箱隔離,可能導致樣式污染或者全局變量衝突
  • 默認以來 webpack 的構建配置,其他構建工具需要改造後才能兼容
  • 版本兼容性差,如果使用不同的 Vue 版本,可能引發衝突
  • 僅提供路由核心能力,缺乏多實例並行等微前端所需要的完整功能
  • 子應用需要遵循特定的生命週期函數,對於一些非標準化的頁面支持較弱,改造成本較高

qiankun 基於 single-spa 進行二次封裝修正了一些缺點,主要包括:

  • 降低侵入性:single-spa 對主應用和子應用的改造要求較高,而 qiankun 通過封裝減少了代碼侵入性,提供了更簡潔的 API 和基於 HTML Entry 的接入方式,降低了接入複雜度
  • 隔離機制:single-spa 未內置完善的隔離方案,可能導致子應用的樣式、全局變量衝突。qiankun 通過沙箱機制(如 CSS Modules、Proxy 代理等)實現了子應用的樣式和作用域隔離,提升安全性
  • 優化開發體驗:qiankun 提供了更貼近實際開發需求的功能,例如子應用的動態加載、預加載策略,以及基於發佈-訂閲模式的通信機制,彌補了 single-spa 在工程化實踐中的不足

1. registerMicroApps() 和 start()

1.1 registerMicroApps()

registerMicroApps() 的邏輯非常簡單:

  • 防止微應用重複註冊
  • 遍歷 unregisteredApps 調用 single-spa 的 registerApplication() 進行微應用的註冊
function registerMicroApps(apps, lifeCycles) {
  const unregisteredApps = apps.filter(
    (app) => !microApps.some((registeredApp) => registeredApp.name === app.name)
  );
  microApps = [...microApps, ...unregisteredApps];

  unregisteredApps.forEach((app) => {
    const { name, activeRule, loader = noop, props, ...appConfig } = app;
    registerApplication({
      name,
      app: async () => {
        //...
        const { mount, ...otherMicroAppConfigs } = (
          await loadApp(
            { name, props, ...appConfig },
            frameworkConfiguration,
            lifeCycles
          )
        )();

        return {
          mount: [
            async () => loader(true),
            ...toArray(mount),
            async () => loader(false),
          ],
          ...otherMicroAppConfigs,
        };
      },
      activeWhen: activeRule,
      customProps: props,
    });
  });
}

1.2 start()

start() 的邏輯也非常簡單:

  • prefetch:預加載觸發 doPrefetchStrategy()
  • 兼容舊的瀏覽器版本autoDowngradeForLowVersionBrowser()改變配置參數frameworkConfiguration
  • 觸發 single-spa 的 start()
function start(opts: FrameworkConfiguration = {}) {
  frameworkConfiguration = {
    prefetch: true,
    singular: true,
    sandbox: true,
    ...opts,
  };
  const {
    prefetch,
    urlRerouteOnly = defaultUrlRerouteOnly,
    ...importEntryOpts
  } = frameworkConfiguration;

  if (prefetch) {
    doPrefetchStrategy(microApps, prefetch, importEntryOpts);
  }

  frameworkConfiguration = autoDowngradeForLowVersionBrowser(
    frameworkConfiguration
  );

  startSingleSpa({ urlRerouteOnly });
  started = true;

  frameworkStartedDefer.resolve(); // frameworkStartedDefer本質就是一個promise
}

2. 預加載

支持傳入預加載的策略,如果不傳則默認為 true,即默認會觸發 prefetchAfterFirstMounted()

function doPrefetchStrategy(apps, prefetchStrategy, importEntryOpts) {
  const appsName2Apps = (names) =>
    apps.filter((app) => names.includes(app.name));
  if (Array.isArray(prefetchStrategy)) {
    prefetchAfterFirstMounted(appsName2Apps(prefetchStrategy), importEntryOpts);
  } else if (isFunction(prefetchStrategy)) {
    (async () => {
      const { criticalAppNames = [], minorAppsName = [] } =
        await prefetchStrategy(apps);
      prefetchImmediately(appsName2Apps(criticalAppNames), importEntryOpts);
      prefetchAfterFirstMounted(appsName2Apps(minorAppsName), importEntryOpts);
    })();
  } else {
    switch (prefetchStrategy) {
      case true:
        prefetchAfterFirstMounted(apps, importEntryOpts);
        break;
      case "all":
        prefetchImmediately(apps, importEntryOpts);
        break;
    }
  }
}

通過 requestIdleCallback() 控制瀏覽器空閒時進行

  • importEntry() 獲取所有微應用的 entry 資源
  • 然後再觸發對應 getExternalStyleSheets() 獲取外部的 styles 數據 + getExternalScripts() 獲取外部的 js 數據
function prefetchAfterFirstMounted(apps, opts) {
  window.addEventListener("single-spa:first-mount", function listener() {
    const notLoadedApps = apps.filter(
      (app) => getAppStatus(app.name) === NOT_LOADED
    );
    notLoadedApps.forEach(({ entry }) => prefetch(entry, opts));
    window.removeEventListener("single-spa:first-mount", listener);
  });
}

function prefetch(entry, opts) {
  if (!navigator.onLine || isSlowNetwork) {
    return;
  }
  requestIdleCallback(async () => {
    const { getExternalScripts, getExternalStyleSheets } = await importEntry(
      entry,
      opts
    );
    requestIdleCallback(getExternalStyleSheets);
    requestIdleCallback(getExternalScripts);
  });
}

3. start()後觸發微應用 mount() 和 unmount()

qiankun

當用户觸發 start() 後,我們從上面流程圖可以知道,會觸發多個生命週期,比如 app.unmount()app.bootstrap()app.mount()

app.unmount()app.bootstrap()app.mount()這三個方法的獲取是從微應用註冊時聲明的,從 single-spa 的源碼分析可以知道,是registerApplication()傳入的 app

從下面的代碼可以知道, qiankun 封裝了傳入的 app() 方法,從 loadApp()中獲取 bootstrapmountunmount三個方法然後再傳入 registerApplication()

function registerMicroApps(apps, lifeCycles) {
  const unregisteredApps = apps.filter(
    (app) => !microApps.some((registeredApp) => registeredApp.name === app.name)
  );
  microApps = [...microApps, ...unregisteredApps];

  unregisteredApps.forEach((app) => {
    const { name, activeRule, loader = noop, props, ...appConfig } = app;
    registerApplication({
      name,
      app: async () => {
        //...
        const { mount, ...otherMicroAppConfigs } = (
          await loadApp(
            { name, props, ...appConfig },
            frameworkConfiguration,
            lifeCycles
          )
        )();

        return {
          mount: [
            async () => loader(true),
            ...toArray(mount),
            async () => loader(false),
          ],
          ...otherMicroAppConfigs,
        };
      },
      activeWhen: activeRule,
      customProps: props,
    });
  });
}

3.1 核心方法 loadApp()

代碼較為冗長,下面將針對每一個小點進行分析
3.1.1 初始化階段

根據註冊的 name 生成唯一的 appInstanceId

const { entry, name: appName } = app;
const appInstanceId = genAppInstanceIdByName(appName);
const markName = `[qiankun] App ${appInstanceId} Loading`;
3.1.2 初始化配置 & importEntry

初始化配置項

  • singular:單例模式
  • sandbox:沙箱模式
  • excludeAssetFilter:資源過濾

然後使用第三方庫 importEntry 加載微應用的各種數據,包括

  • template:link 替換為 style 後的 HTML 數據
  • getExternalScripts:需要另外加載的 JS 代碼
  • execScripts:執行 getExternalScripts() 下載 scripts,然後調用 geval() 生成沙箱代碼並執行,確保 JS 在代理的上下文中運行,避免全局污染
  • assetPublicPath:靜態資源地址
const {
  singular = false,
  sandbox = true,
  excludeAssetFilter,
  globalContext = window,
  ...importEntryOpts
} = configuration;
const {
  template,
  execScripts: execScripts2,
  assetPublicPath,
  getExternalScripts,
} = await importEntry(entry, importEntryOpts);
await getExternalScripts();

然後執行 getExternalScripts() 下載 scripts

通過上面的 importEntry() 內部已經觸發了外部 styles 的下載並且替換到 template
3.1.3 校驗單例模式

如果開啓了單例模式,需要等待前一個應用卸載完成後再加載當前的新應用

if (await validateSingularMode(singular, app)) {
  await (prevAppUnmountedDeferred && prevAppUnmountedDeferred.promise);
}
3.1.4 DOM 根容器的創建 & 處理 style 標籤樣式隔離

用一個 <div id=xxx></div> 包裹 importEntry 拿到的微應用的 HTML 模板數據,同時處理模板中的 <style>數據,保證樣式作用域隔離

在接下來的小點中再着重分析樣式隔離的相關邏輯
const appContent = getDefaultTplWrapper(appInstanceId, sandbox)(template);
const strictStyleIsolation =
  typeof sandbox === "object" && !!sandbox.strictStyleIsolation;
const scopedCSS = isEnableScopedCSS(sandbox);
let initialAppWrapperElement = createElement(
  appContent,
  strictStyleIsolation,
  scopedCSS,
  appInstanceId
);

function getDefaultTplWrapper(name, sandboxOpts) {
  return (tpl) => {
    let tplWithSimulatedHead;
    if (tpl.indexOf("<head>") !== -1) {
      tplWithSimulatedHead = tpl
        .replace("<head>", `<${qiankunHeadTagName}>`)
        .replace("</head>", `</${qiankunHeadTagName}>`);
    } else {
      tplWithSimulatedHead = `<${qiankunHeadTagName}></${qiankunHeadTagName}>${tpl}`;
    }
    return `<div id="${getWrapperId(
      name
    )}" data-name="${name}" data-version="${version}" data-sandbox-cfg=${JSON.stringify(
      sandboxOpts
    )}>${tplWithSimulatedHead}</div>`;
  };
}
function createElement(
  appContent,
  strictStyleIsolation,
  scopedCSS,
  appInstanceId
) {
  const containerElement = document.createElement("div");
  containerElement.innerHTML = appContent;
  const appElement = containerElement.firstChild;
  if (strictStyleIsolation) {
    if (!supportShadowDOM) {
    } else {
      const { innerHTML } = appElement;
      appElement.innerHTML = "";
      let shadow;
      if (appElement.attachShadow) {
        shadow = appElement.attachShadow({ mode: "open" });
      } else {
        shadow = appElement.createShadowRoot();
      }
      shadow.innerHTML = innerHTML;
    }
  }
  if (scopedCSS) {
    const attr = appElement.getAttribute(QiankunCSSRewriteAttr);
    if (!attr) {
      appElement.setAttribute(QiankunCSSRewriteAttr, appInstanceId);
    }
    const styleNodes = appElement.querySelectorAll("style") || [];
    forEach(styleNodes, (stylesheetElement) => {
      process$1(appElement, stylesheetElement, appInstanceId);
    });
  }
  return appElement;
}
3.1.5 渲染函數 render

定義 DOM 的掛載方法,本質就是 dom.appendChild() 這一套邏輯

const render = getRender(appInstanceId, appContent, legacyRender);
render(
  {
    element: initialAppWrapperElement,
    loading: true,
    container: initialContainer,
  },
  "loading"
);

觸發 render() 進行 DOM 的掛載,如下圖所示

qiankun

3.1.6 沙箱容器的創建

創建對應的 sandbox 容器,構建出對應的

  • sandboxContainer.mount
  • sandboxContainer.unmount
在接下來的小點中再着重分析沙箱的相關邏輯
if (sandbox) {
  sandboxContainer = createSandboxContainer(
    appInstanceId,
    initialAppWrapperGetter,
    scopedCSS,
    useLooseSandbox,
    excludeAssetFilter,
    global,
    speedySandbox
  );
  global = sandboxContainer.instance.proxy;
  mountSandbox = sandboxContainer.mount;
  unmountSandbox = sandboxContainer.unmount;
}
3.1.7 生命週期鈎子方法的處理

執行 beforeLoad 生命週期的方法

執行 importEntry 拿到的微應用的 execScripts 代碼,注入全局變量global並執行微應用的腳本

global = sandboxContainer.instance.proxy

最終通過微應用的 execScripts 代碼執行拿到對應的聲明週期方法:

  • bootstarp
  • mount
  • unmount
await execHooksChain(toArray(beforeLoad), app, global);
const scriptExports = await execScripts2(global, sandbox && !useLooseSandbox, {
  scopedGlobalVariables: speedySandbox ? cachedGlobals : [],
});
const { bootstrap, mount, unmount, update } = getLifecyclesFromExports(
  scriptExports,
  appName,
  global,
  (_a = sandboxContainer == null ? void 0 : sandboxContainer.instance) == null
    ? void 0
    : _a.latestSetProp
);
3.1.8 返回 mount、unmount 的對象數據

其中 mount 依次執行

  • 初始化容器 DOM
  • 檢查容器 DOM ,如果還沒有設置則觸發 createElement() 確保容器 DOM 構建完成,進入 mounting 狀態
  • 沙箱激活:運行沙箱導出的 mount() 方法
  • 執行生命週期鈎子方法:beforeMount
  • 觸發微應用的 mount() 方法,並且傳遞對應的參數,比如 setGlobalStateonGlobalStateChange
  • 進入 mounted 狀態,執行 mounted 掛載成功相關的生命週期方法
  • 執行生命週期鈎子方法:afterMount
  • 檢測單例模式下的相關邏輯

unmount 依次執行

  • 執行生命週期鈎子方法:beforeUnmount
  • 觸發微應用的 unmount() 方法
  • 沙箱銷燬:運行沙箱導出的 unmount() 方法
  • 執行生命週期鈎子方法:afterUnmount
  • 觸發 render 進行 真實 DOM 的 卸載
  • 檢測單例模式下的相關邏輯
const parcelConfigGetter = (remountContainer = initialContainer) => {
  let appWrapperElement;
  let appWrapperGetter;
  const parcelConfig = {
    name: appInstanceId,
    bootstrap,
    mount: [
      // initial wrapper element before app mount/remount
      async () => {
        appWrapperElement = initialAppWrapperElement;
        appWrapperGetter = getAppWrapperGetter(
          appInstanceId,
          !!legacyRender,
          strictStyleIsolation,
          scopedCSS,
          () => appWrapperElement
        );
      },
      // 添加 mount hook, 確保每次應用加載前容器 dom 結構已經設置完畢
      async () => {
        const useNewContainer = remountContainer !== initialContainer;
        if (useNewContainer || !appWrapperElement) {
          appWrapperElement = createElement(
            appContent,
            strictStyleIsolation,
            scopedCSS,
            appInstanceId
          );
          syncAppWrapperElement2Sandbox(appWrapperElement);
        }
        render(
          {
            element: appWrapperElement,
            loading: true,
            container: remountContainer,
          },
          "mounting"
        );
      },
      mountSandbox,
      // exec the chain after rendering to keep the behavior with beforeLoad
      async () => execHooksChain(toArray(beforeMount), app, global),
      async (props) =>
        mount({
          ...props,
          container: appWrapperGetter(),
          setGlobalState,
          onGlobalStateChange,
        }),
      // finish loading after app mounted
      async () =>
        render(
          {
            element: appWrapperElement,
            loading: false,
            container: remountContainer,
          },
          "mounted"
        ),
      async () => execHooksChain(toArray(afterMount), app, global),
    ],
    unmount: [
      async () => execHooksChain(toArray(beforeUnmount), app, global),
      async (props) => unmount({ ...props, container: appWrapperGetter() }),
      unmountSandbox,
      async () => execHooksChain(toArray(afterUnmount), app, global),
      async () => {
        render(
          { element: null, loading: false, container: remountContainer },
          "unmounted"
        );
        offGlobalStateChange(appInstanceId);
        appWrapperElement = null;
        syncAppWrapperElement2Sandbox(appWrapperElement);
      },
      async () => {
        if (
          (await validateSingularMode(singular, app)) &&
          prevAppUnmountedDeferred
        ) {
          prevAppUnmountedDeferred.resolve();
        }
      },
    ],
  };
  if (typeof update === "function") {
    parcelConfig.update = update;
  }
  return parcelConfig;
};
return parcelConfigGetter;

4. 監聽路由變化觸發 reroute()

本質就是觸發 loadApp() 進行應用具體邏輯的加載

當加載 single-spa 的代碼後,會直接監聽路由的變化,當路由發生變化時,會觸發reroute(),從而觸發 performAppChanges()

single-spa.performAppChanges() 進行舊的路由的卸載以及新的路由的加載

本質就是觸發

  • app.unmount()觸發微應用的卸載
  • app.bootstrap() -> app.mount()觸發微應用的加載

5. 樣式隔離

在上面的DOM 根容器的創建 & 處理 style 標籤樣式隔離分析中

const appContent = getDefaultTplWrapper(appInstanceId, sandbox)(template);
const strictStyleIsolation =
  typeof sandbox === "object" && !!sandbox.strictStyleIsolation;
const scopedCSS = isEnableScopedCSS(sandbox);
let initialAppWrapperElement = createElement(
  appContent,
  strictStyleIsolation,
  scopedCSS,
  appInstanceId
);

如果我們在 qiankun.start({sandbox: {}}) 傳入一個 sandbox 的配置對象數據,那麼我們就可以開啓

  • 嚴格隔離模式 strictStyleIsolation=true
  • 實驗性的樣式隔離模式 experimentalStyleIsolation=true

sandbox - boolean | { strictStyleIsolation?: boolean, experimentalStyleIsolation?: boolean } - 可選,是否開啓沙箱,默認為 true

默認情況下沙箱可以確保單實例場景子應用之間的樣式隔離,但是無法確保主應用跟子應用、或者多實例場景的子應用樣式隔離。當配置為 { strictStyleIsolation: true } 時表示開啓嚴格的樣式隔離模式。這種模式下 qiankun 會為每個微應用的容器包裹上一個 shadow dom 節點,從而確保微應用的樣式不會對全局造成影響

注:上面兩種模式是互斥,不能同時存在

const scopedCSS = isEnableScopedCSS(sandbox);
function isEnableScopedCSS(sandbox) {
  if (typeof sandbox !== "object") {
    return false;
  }
  if (sandbox.strictStyleIsolation) {
    return false;
  }
  return !!sandbox.experimentalStyleIsolation;
}
const strictStyleIsolation =
  typeof sandbox === "object" && !!sandbox.strictStyleIsolation;

如果開啓了嚴格樣式隔離strictStyleIsolation,則創建一個 Shadow 包裹 importEntry 加載微應用得到的 HTML 模板數據

function createElement(
  appContent,
  strictStyleIsolation,
  scopedCSS,
  appInstanceId
) {
  const containerElement = document.createElement("div");
  containerElement.innerHTML = appContent;
  const appElement = containerElement.firstChild;
  if (strictStyleIsolation) {
    if (!supportShadowDOM) {
      console.warn(
        "[qiankun]: As current browser not support shadow dom, your strictStyleIsolation configuration will be ignored!"
      );
    } else {
      const { innerHTML } = appElement;
      appElement.innerHTML = "";
      let shadow;
      if (appElement.attachShadow) {
        shadow = appElement.attachShadow({ mode: "open" });
      } else {
        shadow = appElement.createShadowRoot();
      }
      shadow.innerHTML = innerHTML;
    }
  }
  if (scopedCSS) {
    const attr = appElement.getAttribute(QiankunCSSRewriteAttr);
    if (!attr) {
      appElement.setAttribute(QiankunCSSRewriteAttr, appInstanceId);
    }
    const styleNodes = appElement.querySelectorAll("style") || [];
    forEach(styleNodes, (stylesheetElement) => {
      process$1(appElement, stylesheetElement, appInstanceId);
    });
  }
  return appElement;
}

如果開啓了experimentalStyleIsolation,則使用

  • processor = new ScopedCSS()
  • 使用 processor.process() 進行樣式前綴的重寫
const process$1 = (appWrapper, stylesheetElement, appName) => {
  if (!processor) {
    processor = new ScopedCSS();
  }
  if (stylesheetElement.tagName === "LINK") {
    console.warn(
      "Feature: sandbox.experimentalStyleIsolation is not support for link element yet."
    );
  }
  const mountDOM = appWrapper;
  if (!mountDOM) {
    return;
  }
  const tag = (mountDOM.tagName || "").toLowerCase();
  if (tag && stylesheetElement.tagName === "STYLE") {
    const prefix = `${tag}[${QiankunCSSRewriteAttr}="${appName}"]`;
    processor.process(stylesheetElement, prefix);
  }
};

process()中,主要進行

  • 調用 rewrite(rules, prefix) 重寫規則,生成帶前綴的 CSS 文本
  • styleNode 內容為空,則通過 MutationObserver 監聽動態添加的子節點,確保異步加載的樣式也能被處理

rewrite(rules, prefix) 主要分為 3 種情況進行處理:

  • ruleStyle(rule, prefix):處理普通 CSS 規則
  • ruleMedia(rule, prefix):遞歸處理媒體查詢規則
  • ruleSupport(rule, prefix):遞歸處理 @supports 條件規則
rewrite(rules, prefix = "") {
  let css = "";
  rules.forEach((rule) => {
    switch (rule.type) {
      case 1:
        css += this.ruleStyle(rule, prefix);
        break;
      case 4:
        css += this.ruleMedia(rule, prefix);
        break;
      case 12:
        css += this.ruleSupport(rule, prefix);
        break;
      default:
        if (typeof rule.cssText === "string") {
          css += `${rule.cssText}`;
        }
        break;
    }
  });
  return css;
}

5.1 ruleStyle()

通過正則匹配 rootSelectorRErootCombinationRE,匹配htmlbody:root 等全局選擇器以及匹配 html 後跟隨其他選擇器的組合(如 html .class

ruleStyle(rule, prefix) {
    const rootSelectorRE = /((?:[^\w\-.#]|^)(body|html|:root))/gm;
    const rootCombinationRE = /(html[^\w{[]+)/gm;
    const selector = rule.selectorText.trim();
    let cssText = "";
    if (typeof rule.cssText === "string") {
      cssText = rule.cssText;
    }
    if (selector === "html" || selector === "body" || selector === ":root") {
      return cssText.replace(rootSelectorRE, prefix);
    }
    if (rootCombinationRE.test(rule.selectorText)) {
      const siblingSelectorRE = /(html[^\w{]+)(\+|~)/gm;
      if (!siblingSelectorRE.test(rule.selectorText)) {
        cssText = cssText.replace(rootCombinationRE, "");
      }
    }
    cssText = cssText.replace(
      /^[\s\S]+{/,
      (selectors) => selectors.replace(/(^|,\n?)([^,]+)/g, (item, p, s) => {
        if (rootSelectorRE.test(item)) {
          return item.replace(rootSelectorRE, (m) => {
            const whitePrevChars = [",", "("];
            if (m && whitePrevChars.includes(m[0])) {
              return `${m[0]}${prefix}`;
            }
            return prefix;
          });
        }
        return `${p}${prefix} ${s.replace(/^ */, "")}`;
      })
    );
    return cssText;
}
  • 如果匹配到htmlbody:root,則新增前面的作用域 [data-qiankun="app"]
  • 如果匹配到html 後跟隨其他選擇器的組合,則移除html+ 新增前面的作用域 [data-qiankun="app"]
  • 如果匹配到其他選擇器,直接新增前面的作用域 [data-qiankun="app"]
/* 原始 CSS */
body { background: blue; }
.my-class { color: red; }
html .header { font-size: 20px; }

/* 處理後 CSS(假設 prefix 為 [data-qiankun="app"]) */
[data-qiankun="app"] { background: blue; }
[data-qiankun="app"] .my-class { color: red; }
[data-qiankun="app"] .header { font-size: 20px; }

5.2 ruleMedia()

遞歸調用 rewrite() 處理媒體查詢內部的規則,保持媒體查詢條件不變

ruleMedia(rule, prefix) {
  const css = this.rewrite(arrayify(rule.cssRules), prefix);
  return `@media ${rule.conditionText || rule.media.mediaText} {${css}}`;
}
/* 原始 CSS */
@media screen and (max-width: 600px) {
  .box { width: 100%; }
}

/* 處理後 CSS */
@media screen and (max-width: 600px) {
  [data-qiankun="app"] .box { width: 100%; }
}

5.3 ruleSupport()

遞歸調用 rewrite 處理 @supports 條件內部的規則,保持條件不變

ruleSupport(rule, prefix) {
  const css = this.rewrite(arrayify(rule.cssRules), prefix);
  return `@supports ${rule.conditionText || rule.cssText.split("{")[0]} {${css}}`;
}
/* 原始 CSS */
@supports (display: grid) {
  .grid { display: grid; }
}

/* 處理後 CSS */
@supports (display: grid) {
  [data-qiankun="app"] .grid { display: grid; }
}

6. 沙箱機制

沙箱機制主要是用來隔離微應用之間的全局變量副作用,防止基座和微應用以及微應用和微應用之間相互干擾

qiankun 使用了三種沙箱實現:

  • ProxySandbox:支持Proxy的現代瀏覽器環境 並且 註冊微應用傳入 {sandbox: {loose: true}}
  • LegacySandbox:支持Proxy的現代瀏覽器環境,默認使用的沙箱
  • SnapshotSandbox:不支持Proxy的舊瀏覽器環境
const useLooseSandbox = typeof sandbox === "object" && !!sandbox.loose;
function createSandboxContainer(...) {
  let sandbox;
  if (window.Proxy) {
    sandbox = useLooseSandbox ? new LegacySandbox(appName, globalContext) :
      new ProxySandbox(appName, globalContext, { speedy: !!speedySandBox });
  } else {
    sandbox = new SnapshotSandbox(appName);
  }
}

6.1 ProxySandbox

class ProxySandbox {
  constructor() {
    const { fakeWindow, propertiesWithGetter } = createFakeWindow(
      globalContext,
      !!speedy
    );
    const proxy = new Proxy(fakeWindow, {
        set: () => {},
        get: ()=> {}
        //...
    });
  }
  active()
  inactive()
}
6.1.1 createFackWindow()

使用 createFackWindow() 構建一個模擬的 window 全局對象

傳入參數:

  • 傳入globalContext = window
  • speedy:微應用註冊時聲明,用於某些屬性的優化處理,為了解決某些情況下 with 導致的卡頓問題

獲取全局對象所有的屬性名(即 window 的所有屬性),然後篩選出不可配置的屬性(configurable = false

這些不可配置的屬性一般都是原生屬性或者不可刪除的屬性

然後遍歷這些屬性 p,獲取屬性描述符 descriptor

對一些特殊屬性先進行處理:

  • 瀏覽器安全相關的屬性,需允許沙箱內修改:topparentselfwindow
  • 在性能優化模式下speedydocument 屬性

對上面這些屬性,先更改為可配置 configurable = true;如果沒有getter,則設置為可寫模式writeable = true

對於所有的全局屬性

  • 對於有 getter的屬性,添加到 propertiesWithGetter 對象中(後續在 Proxy 中攔截這些屬性時,直接返回原始值,避免代理破壞原生行為)
  • 然後在 fakeWindow 上定義這些屬性

最終返回全局對象 fakeWindow 和 特殊屬性記錄對象 propertiesWithGetter

const speedySandbox =
  typeof sandbox === "object" ? sandbox.speedy !== false : true;
function createFakeWindow(globalContext, speedy) {
  const propertiesWithGetter = /* @__PURE__ */ new Map();
  const fakeWindow = {};
  Object.getOwnPropertyNames(globalContext)
    .filter((p) => {
      const descriptor = Object.getOwnPropertyDescriptor(globalContext, p);
      return !(descriptor == null ? void 0 : descriptor.configurable);
    })
    .forEach((p) => {
      const descriptor = Object.getOwnPropertyDescriptor(globalContext, p);
      if (descriptor) {
        const hasGetter = Object.prototype.hasOwnProperty.call(
          descriptor,
          "get"
        );
        if (
          p === "top" ||
          p === "parent" ||
          p === "self" ||
          p === "window" || // window.document is overwriting in speedy mode
          (p === "document" && speedy) ||
          (inTest && (p === mockTop || p === mockSafariTop))
        ) {
          descriptor.configurable = true;
          if (!hasGetter) {
            descriptor.writable = true;
          }
        }
        if (hasGetter) propertiesWithGetter.set(p, true);
        rawObjectDefineProperty(fakeWindow, p, Object.freeze(descriptor));
      }
    });
  return {
    fakeWindow,
    propertiesWithGetter,
  };
}
6.1.2 new Proxy(fakeWindow)

使用 ProxyfakeWindow 進行劫持

const proxy = new Proxy(fakeWindow, {
  set: ()=> {}
  get: ()=> {}
  //...
}

沙箱運行時(sandboxRunning = true),記錄修改的屬性到 upatedValueSet(無論是白名單還是非白名單屬性)

  • 白名單屬性(比如 System__cjsWrapper、React 調試鈎子)同步到 全局對象globalContext
  • 非白名單屬性則寫入 fakeWindow,如果 全局對象globalContext 存在該屬性而 fakeWindow不存在該屬性,則調整 writable:true 兼容之前的設置

沙箱非運行狀態則直接返回 true

set: (target, p, value): boolean => {
  if (this.sandboxRunning) {
    this.registerRunningApp(name, proxy);
    // 白名單屬性同步到全局
    if (typeof p === 'string' && globalVariableWhiteList.includes(p)) {
      this.globalWhitelistPrevDescriptor[p] = Object.getOwnPropertyDescriptor(globalContext, p);
      globalContext[p] = value;
    } else {
      // 非白名單屬性寫入 fakeWindow
      if (!target.hasOwnProperty(p) && globalContext.hasOwnProperty(p)) {
        // 處理全局已存在的屬性(修正描述符)
        const descriptor = Object.getOwnPropertyDescriptor(globalContext, p);
        const { writable, configurable, enumerable, set } = descriptor!;
        if (writable || set) {
          Object.defineProperty(target, p, { configurable, enumerable, writable: true, value });
        }
      } else {
        target[p] = value;
      }
    }
    updatedValueSet.add(p);
    this.latestSetProp = p;
    return true;
  }
  // 沙箱非活躍時警告
  if (process.env.NODE_ENV === 'development') {
    console.warn(`[qiankun] Set window.${p.toString()} while sandbox destroyed or inactive in ${name}!`);
  }
  return true;
}

對各種情況進行處理

  • 防止逃逸:全局對象windowselfglobalThis代理,直接返回代理proxy
  • 特殊屬性:

    • top/parent 如果你的主應用程序處於 iframe 上下文中,允許屬性逃逸,返回 globalContext,否則返回 proxy
    • document 返回沙箱自己構建的 document
    • eval 返回原生 eval
  • 白名單屬性處理:直接返回全局對象 globalContext[p]
  • 凍結屬性處理:對於一些configurable=false&writable=false的屬性,嘗試從globalContext->target進行判斷獲取
  • 原生 API 修正:對於 fetch 需要綁定原生上下文的方法進行重新綁定並且返回值

其它屬性(不是需要重新綁定的屬性 + 非凍結屬性),從globalContext[p]->target[p]進行判斷獲取

export const nativeGlobal = new Function('return this')();
const useNativeWindowForBindingsProps = new Map<PropertyKey, boolean>([
  ['fetch', true],
  ['mockDomAPIInBlackList', process.env.NODE_ENV === 'test'],
]);
const cachedGlobalsInBrowser = array2TruthyObject(
  globalsInBrowser.concat(process.env.NODE_ENV === 'test' ? ['mockNativeWindowFunction'] : []),
);
function isNativeGlobalProp(prop: string): boolean {
  return prop in cachedGlobalsInBrowser;
}
get: (target, p) => {
  this.registerRunningApp(name, proxy);
  if (p === Symbol.unscopables) return unscopables;
  // 代理全局對象(防止逃逸)
  if (p === "window" || p === "self" || p === "globalThis") {
    return proxy;
  }
  // 特殊屬性處理
  if (p === "top" || p === "parent") {
    // 如果你的主應用程序處於 iframe 上下文中,請允許這些屬性逃離沙盒
    return globalContext === globalContext.parent ? proxy : globalContext[p];
  }
  if (p === "document") return this.document;
  if (p === "eval") return eval;
  // 白名單屬性直接返回全局值
  if (globalVariableWhiteList.includes(p)) return globalContext[p];
  // 凍結屬性直接返回(避免重綁定)
  const actualTarget = propertiesWithGetter.has(p)
    ? globalContext
    : p in target
    ? target
    : globalContext;
  if (isPropertyFrozen(actualTarget, p)) return actualTarget[p];
  // 原生屬性綁定到原生上下文(如 fetch.bind(window))
  if (isNativeGlobalProp(p) || useNativeWindowForBindingsProps.has(p)) {
    const boundTarget = useNativeWindowForBindingsProps.get(p)
      ? nativeGlobal
      : globalContext;
    return rebindTarget2Fn(boundTarget, actualTarget[p]);
  }
  return actualTarget[p];
};
  • has:從cachedGlobalObjectstargetglobalContext 檢查是否具有該屬性
  • getOwnPropertyDescriptor:優先 fakeWindow,如果不存在,則從 globalContext 中獲取 descriptor並且標記為可配置 configurable=true
  • ownKeys:合併 fakeWindowglobalContext 的 key
  • deleteProperty:從 fakeWindow 刪除屬性 + updatedValueSet 刪除對應的記錄
has: (target, p) =>
  p in cachedGlobalObjects || p in target || p in globalContext;
getOwnPropertyDescriptor: (target, p) => {
  if (target.hasOwnProperty(p)) {
    descriptorTargetMap.set(p, "target");
    return Object.getOwnPropertyDescriptor(target, p);
  }
  if (globalContext.hasOwnProperty(p)) {
    const descriptor = Object.getOwnPropertyDescriptor(globalContext, p);
    descriptorTargetMap.set(p, "globalContext");
    if (descriptor && !descriptor.configurable) descriptor.configurable = true; // 兼容性調整
    return descriptor;
  }
  return undefined;
};
ownKeys: (target) =>
  uniq(Reflect.ownKeys(globalContext).concat(Reflect.ownKeys(target)));
deleteProperty: (target, p) => {
  if (target.hasOwnProperty(p)) {
    delete target[p];
    updatedValueSet.delete(p);
  }
  return true;
};
6.1.3 active() & inactive()
  • active():激活沙箱,activeSandboxCount++
  • inactive():在沙箱停用時恢復全局白名單屬性的原始值(在 new Proxy 的 set() 已經存儲到 globalWhitelistPrevDescriptor 中),否則直接在原生 globalContext 中刪除該屬性
active() {
    if (!this.sandboxRunning) activeSandboxCount++;
    this.sandboxRunning = true;
}

inactive() {
    if (inTest || --activeSandboxCount === 0) {
        // reset the global value to the prev value
        Object.keys(this.globalWhitelistPrevDescriptor).forEach((p) => {
          const descriptor = this.globalWhitelistPrevDescriptor[p];
          if (descriptor) {
              Object.defineProperty(this.globalContext, p, descriptor);
          } else {
              // @ts-ignore
              delete this.globalContext[p];
          }
        });
    }

    this.sandboxRunning = false;
}

6.2 LegacySandbox

為了兼容性 singular 模式下依舊使用該沙箱,等新沙箱穩定之後再切換

ProxySandbox一樣,也是基於 Proxy 實現的沙箱

但是這個沙箱只考慮單例模式,直接操作原生的 window對象,記錄原始值然後實現卸載時恢復原生 window對象

6.2.1 Proxy.set()

set()

  • 如果原生window對象不存在該屬性,則添加到 addedPropsMapInSandbox
  • 如果原生window對象存在該屬性,則添加到 modifiedPropsOriginalValueMapInSandbox

並且使用 currentUpdatedPropsValueMap 進行該屬性的存儲,同時改變原生window對應的屬性

const setTrap = (p, value, originalValue, sync2Window = true) => {
  if (this.sandboxRunning) {
    if (!rawWindow.hasOwnProperty(p)) {
      addedPropsMapInSandbox.set(p, value);
    } else if (!modifiedPropsOriginalValueMapInSandbox.has(p)) {
      modifiedPropsOriginalValueMapInSandbox.set(p, originalValue);
    }
    currentUpdatedPropsValueMap.set(p, value);
    if (sync2Window) {
      rawWindow[p] = value;
    }
    this.latestSetProp = p;
    return true;
  }
  return true;
};
6.2.2 inactive()

在卸載時,使用之前記錄的 addedPropsMapInSandboxmodifiedPropsOriginalValueMapInSandbox 恢復 原生window對象

inactive() {
    this.modifiedPropsOriginalValueMapInSandbox.forEach((v, p) => this.setWindowProp(p, v));
    this.addedPropsMapInSandbox.forEach((_, p) => this.setWindowProp(p, void 0, true));
    this.sandboxRunning = false;
}
6.2.3 get()

get() 中,如果是特殊屬性,直接返回當前代理全局對象proxy,否則返回 rawWindow[p](因為原生的 window 已經被改變)

get(_, p) {
    if (p === "top" || p === "parent" || p === "window" || p === "self") {
        return proxy;
    }
    const value = rawWindow[p];
    return rebindTarget2Fn(rawWindow, value);
},

僅處理

  • fn是可調用函數
  • fn未被綁定過
  • fn不是構造函數
如 window.console、window.atob 這類

然後將fnthis綁定到target,創建出新的綁定函數boundValue,複製fn的所有屬性到新創建的函數boundValue上 + 處理原型保證信息一致

重寫原來的fn.toString方法,如果新的函數boundValue沒有toString,則調用原來fn.toString()方法,如果有,則觸發boundValuetoString()

function rebindTarget2Fn(target, fn) {
  if (isCallable(fn) && !isBoundedFunction(fn) && !isConstructable(fn)) {
    const cachedBoundFunction = functionBoundedValueMap.get(fn);
    if (cachedBoundFunction) {
      return cachedBoundFunction;
    }
    const boundValue = Function.prototype.bind.call(fn, target);
    Object.getOwnPropertyNames(fn).forEach((key) => {
      if (!boundValue.hasOwnProperty(key)) {
        Object.defineProperty(
          boundValue,
          key,
          Object.getOwnPropertyDescriptor(fn, key)
        );
      }
    });
    if (
      fn.hasOwnProperty("prototype") &&
      !boundValue.hasOwnProperty("prototype")
    ) {
      Object.defineProperty(boundValue, "prototype", {
        value: fn.prototype,
        enumerable: false,
        writable: true,
      });
    }
    if (typeof fn.toString === "function") {
      const valueHasInstanceToString =
        fn.hasOwnProperty("toString") && !boundValue.hasOwnProperty("toString");
      const boundValueHasPrototypeToString =
        boundValue.toString === Function.prototype.toString;
      if (valueHasInstanceToString || boundValueHasPrototypeToString) {
        const originToStringDescriptor = Object.getOwnPropertyDescriptor(
          valueHasInstanceToString ? fn : Function.prototype,
          "toString"
        );
        Object.defineProperty(
          boundValue,
          "toString",
          Object.assign(
            {},
            originToStringDescriptor,
            (
              originToStringDescriptor == null
                ? void 0
                : originToStringDescriptor.get
            )
              ? null
              : { value: () => fn.toString() }
          )
        );
      }
    }
    functionBoundedValueMap.set(fn, boundValue);
    return boundValue;
  }
  return fn;
}

通過上面的處理,確保綁定後的函數在沙箱內外行為一致,避免因為上下文切換導致報錯(微應用中調用時會拋出 Illegal invocation 異常)

6.2.4 active()

在恢復沙箱時,會從之前set() 存儲的currentUpdatedPropsValueMap中進行 window 對象屬性值的恢復

active() {
    if (!this.sandboxRunning) {
        this.currentUpdatedPropsValueMap.forEach((v, p) => this.setWindowProp(p, v));
    }
    this.sandboxRunning = true;
}
private setWindowProp(prop: PropertyKey, value: any, toDelete?: boolean) {
  if (value === undefined && toDelete) {
    // eslint-disable-next-line no-param-reassign
    delete (this.globalContext as any)[prop];
  } else if (isPropConfigurable(this.globalContext, prop) && typeof prop !== 'symbol') {
    Object.defineProperty(this.globalContext, prop, { writable: true, configurable: true });
    // eslint-disable-next-line no-param-reassign
    (this.globalContext as any)[prop] = value;
  }
}

6.3 SnapshotSandbox

  • active()時,使用一個windowSnapshot保存原生 window 對象的所有屬性,然後恢復之前的modifyPropsMap所有修改的屬性到 window 對象上
  • inactive()時,將目前所有的修改都存放到modifyPropsMap上去,然後使用windowSnapshot進行原生 window 對象的屬性恢復
class SnapshotSandbox {
  constructor(name) {
    //...
    this.name = name;
    this.proxy = window;
    this.type = SandBoxType.Snapshot;
  }
  active() {
    this.windowSnapshot = {};
    iter(window, (prop) => {
      this.windowSnapshot[prop] = window[prop];
    });
    Object.keys(this.modifyPropsMap).forEach((p) => {
      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;
  }
  patchDocument() {}
}

6.4 mount() & unmount()

unmount()時,會

  • 執行 patchAtBootstrapping()patchAtMounting() 拿到的 free() 方法,然後拿到 free()執行完畢後返回的 rebuild()存儲到 sideEffectsRebuilders
  • 觸發 sandbox.inactive()

在進行mount()時,會按照順序執行

  • sandbox.active()
  • 執行上一次 patchAtBootstrapping() 卸載時執行的 free() 返回的 rebuild()
  • patchAtMounting()
  • 執行上一次 patchAtMounting() 卸載時執行的 free() 返回的 rebuild()
  • 清除所有 rebuild()
const bootstrappingFreers = patchAtBootstrapping(
  appName,
  elementGetter,
  sandbox,
  scopedCSS,
  excludeAssetFilter,
  speedySandBox
);
return {
  instance: sandbox,
  async mount() {
    sandbox.active();
    const sideEffectsRebuildersAtBootstrapping = sideEffectsRebuilders.slice(
      0,
      bootstrappingFreers.length
    );
    const sideEffectsRebuildersAtMounting = sideEffectsRebuilders.slice(
      bootstrappingFreers.length
    );
    if (sideEffectsRebuildersAtBootstrapping.length) {
      sideEffectsRebuildersAtBootstrapping.forEach((rebuild) => rebuild());
    }
    mountingFreers = patchAtMounting(
      appName,
      elementGetter,
      sandbox,
      scopedCSS,
      excludeAssetFilter,
      speedySandBox
    );
    if (sideEffectsRebuildersAtMounting.length) {
      sideEffectsRebuildersAtMounting.forEach((rebuild) => rebuild());
    }
    sideEffectsRebuilders = [];
  },
  async unmount() {
    sideEffectsRebuilders = [...bootstrappingFreers, ...mountingFreers].map(
      (free) => free()
    );
    sandbox.inactive();
  },
};

從下面代碼可以看出,

  • patchAtBootstrapping():順序執行的是 patchLooseSandbox() / patchStrictSandbox(),然後拿到對應的 free()
  • patchAtMounting():順序執行的是 patchInterval() -> patchWindowListener() -> patchHistoryListener() -> patchLooseSandbox() / patchStrictSandbox(),然後拿到對應的 free()
function patchAtBootstrapping(
  appName,
  elementGetter,
  sandbox,
  scopedCSS,
  excludeAssetFilter,
  speedySandBox
) {
  const patchersInSandbox = {
    [SandBoxType.LegacyProxy]: [
      () =>
        patchLooseSandbox(
          appName,
          elementGetter,
          sandbox,
          false,
          scopedCSS,
          excludeAssetFilter
        ),
    ],
    [SandBoxType.Proxy]: [
      () =>
        patchStrictSandbox(
          appName,
          elementGetter,
          sandbox,
          false,
          scopedCSS,
          excludeAssetFilter,
          speedySandBox
        ),
    ],
    [SandBoxType.Snapshot]: [
      () =>
        patchLooseSandbox(
          appName,
          elementGetter,
          sandbox,
          false,
          scopedCSS,
          excludeAssetFilter
        ),
    ],
  };
  return patchersInSandbox[sandbox.type]?.map((patch) => patch());
}
export function patchAtMounting() {
  const basePatchers = [
    () => patchInterval(sandbox.proxy),
    () => patchWindowListener(sandbox.proxy),
    () => patchHistoryListener(),
  ];
  const patchersInSandbox = {
    [SandBoxType.LegacyProxy]: [
      ...basePatchers,
      () =>
        patchLooseSandbox(
          appName,
          elementGetter,
          sandbox,
          true,
          scopedCSS,
          excludeAssetFilter
        ),
    ],
    [SandBoxType.Proxy]: [
      ...basePatchers,
      () =>
        patchStrictSandbox(
          appName,
          elementGetter,
          sandbox,
          true,
          scopedCSS,
          excludeAssetFilter,
          speedySandBox
        ),
    ],
    [SandBoxType.Snapshot]: [
      ...basePatchers,
      () =>
        patchLooseSandbox(
          appName,
          elementGetter,
          sandbox,
          true,
          scopedCSS,
          excludeAssetFilter
        ),
    ],
  };
  return patchersInSandbox[sandbox.type]?.map((patch) => patch());
}
6.4.1 patchInterval()

在微應用內部調用 setInterval() 或者 clearInterval() 時,會直接觸發原生的 window.setInterval()window.clearInterval()

free() 中,會直接將所有註冊的 intervals 全部進行 clearInterval(),然後恢復全局方法 window.setIntervalwindow.clearInterval

重寫 setInterval()clearInterval() 只是為了在 free() 的時候能夠移除所有的定時器
const rawWindowInterval = window.setInterval;
const rawWindowClearInterval = window.clearInterval;

function patch(global: Window) {
  let intervals: number[] = [];

  global.clearInterval = (intervalId: number) => {
    intervals = intervals.filter((id) => id !== intervalId);
    return rawWindowClearInterval.call(window, intervalId as any);
  };
  global.setInterval = (handler: CallableFunction, 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;
  };
}
6.4.2 patchWindowListener()

patchInterval() 類似,這裏對 window.addEventListenerwindow.removeEventListener 進行重寫,然後在 free() 時進行所有事件的移除和以及原生方法恢復

6.4.3 patchHistoryListener()

修復 UmiJS 相關的路由功能

patchInterval()patchWindowListener() 類似,對window.g_history 進行重寫,然後在 free() 時進行所有 window.g_history.listen監聽的移除

這裏比較特殊的是:rebuild() 必須使用 window.g_history.listen 的方式重新綁定 listener,從而能保證 rebuild 這部分也能被捕獲到,否則在應用卸載後無法正確的移除這部分副作用
function patch() {
  let rawHistoryListen = (_) => noop;
  const historyListeners = [];
  const historyUnListens = [];
  if (window.g_history && isFunction(window.g_history.listen)) {
    rawHistoryListen = window.g_history.listen.bind(window.g_history);
    window.g_history.listen = (listener) => {
      historyListeners.push(listener);
      const unListen = rawHistoryListen(listener);
      historyUnListens.push(unListen);
      return () => {
        unListen();
        historyUnListens.splice(historyUnListens.indexOf(unListen), 1);
        historyListeners.splice(historyListeners.indexOf(listener), 1);
      };
    };
  }
  return function free() {
    let rebuild = noop;
    if (historyListeners.length) {
      rebuild = () => {
        historyListeners.forEach((listener) =>
          window.g_history.listen(listener)
        );
      };
    }
    historyUnListens.forEach((unListen) => unListen());
    if (window.g_history && isFunction(window.g_history.listen)) {
      window.g_history.listen = rawHistoryListen;
    }
    return rebuild;
  };
}
6.4.4 patchLooseSandbox()

從下面代碼可以看出,主要邏輯集中在下面幾個方法中:

  • patchHTMLDynamicAppendPrototypeFunctions()
  • unpatchDynamicAppendPrototypeFunctions()
  • recordStyledComponentsCSSRules()
  • rebuildCSSRules()
function patchLooseSandbox() {
  const { proxy } = sandbox;
  let dynamicStyleSheetElements = [];
  const unpatchDynamicAppendPrototypeFunctions =
    patchHTMLDynamicAppendPrototypeFunctions(
      () =>
        checkActivityFunctions(window.location).some(
          (name) => name === appName
        ),
      () => ({
        appName,
        appWrapperGetter,
        proxy,
        strictGlobal: false,
        speedySandbox: false,
        scopedCSS,
        dynamicStyleSheetElements,
        excludeAssetFilter,
      })
    );

  return function free() {
    if (isAllAppsUnmounted()) unpatchDynamicAppendPrototypeFunctions();
    recordStyledComponentsCSSRules(dynamicStyleSheetElements);
    return function rebuild() {
      rebuildCSSRules(dynamicStyleSheetElements, (stylesheetElement) => {
        const appWrapper = appWrapperGetter();
        if (!appWrapper.contains(stylesheetElement)) {
          document.head.appendChild.call(appWrapper, stylesheetElement);
          return true;
        }
        return false;
      });
      if (mounting) {
        dynamicStyleSheetElements = [];
      }
    };
  };
}
6.4.4.1 patchHTMLDynamicAppendPrototypeFunctions()

劫持並重寫 appendChildinsertBeforeremoveChild等 DOM 操作方法,實現對微應用動態場景的 <style><link><script>標籤的隔離和管理,實現:

  • 隔離微應用資源:將樣式轉化為內聯樣式插入到微應用中,將 JS 代碼轉化為沙箱代碼進行隔離,防止微應用的 CSS 和 JS 污染基座
  • 動態資源跟蹤:記錄微應用動態創建的資源,便於後續微應用 unmount 時移除資源 + 重新激活微應用時恢復資源
function patchHTMLDynamicAppendPrototypeFunctions(
  isInvokedByMicroApp,
  containerConfigGetter
) {
  const rawHeadAppendChild2 = HTMLHeadElement.prototype.appendChild;
  //...
  if (
    rawHeadAppendChild2[overwrittenSymbol] !== true &&
    rawBodyAppendChild[overwrittenSymbol] !== true &&
    rawHeadInsertBefore2[overwrittenSymbol] !== true
  ) {
    HTMLHeadElement.prototype.appendChild =
      getOverwrittenAppendChildOrInsertBefore({
        rawDOMAppendOrInsertBefore: rawHeadAppendChild2,
        containerConfigGetter,
        isInvokedByMicroApp,
        target: "head",
      });
    //...
  }
  const rawHeadRemoveChild = HTMLHeadElement.prototype.removeChild;
  //...
  if (
    rawHeadRemoveChild[overwrittenSymbol] !== true &&
    rawBodyRemoveChild[overwrittenSymbol] !== true
  ) {
    HTMLHeadElement.prototype.removeChild = getNewRemoveChild(
      rawHeadRemoveChild,
      containerConfigGetter,
      "head",
      isInvokedByMicroApp
    );
    //...
  }
  return function unpatch() {
    HTMLHeadElement.prototype.appendChild = rawHeadAppendChild2;
    HTMLHeadElement.prototype.removeChild = rawHeadRemoveChild;
    HTMLBodyElement.prototype.appendChild = rawBodyAppendChild;
    HTMLBodyElement.prototype.removeChild = rawBodyRemoveChild;
    HTMLHeadElement.prototype.insertBefore = rawHeadInsertBefore2;
  };
}

從上面代碼可以知道,劫持重寫主要涉及到兩個方法

  • getOverwrittenAppendChildOrInsertBefore() 重寫 appendChildinsertBefore
  • getNewRemoveChild() 重寫 removeChild
6.4.4.1.1 getOverwrittenAppendChildOrInsertBefore()
function getOverwrittenAppendChildOrInsertBefore(opts) {
  function appendChildOrInsertBefore(newChild, refChild = null) {
    // opts參數:原始方法、是否由子應用調用、容器配置、目標容器(head/body)
    const {
      rawDOMAppendOrInsertBefore,
      isInvokedByMicroApp,
      containerConfigGetter,
      target = "body",
    } = opts;
    let element = newChild;
    // 非劫持標籤(非 script/style/link)或非子應用調用時,直接調用原始方法
    if (!isHijackingTag(element.tagName) || !isInvokedByMicroApp(element)) {
      return rawDOMAppendOrInsertBefore.call(this, element, refChild);
    }
    // ...
  }
  appendChildOrInsertBefore[overwrittenSymbol] = true; // 標記為已重寫
  return appendChildOrInsertBefore;
}
  • isHijackingTag(): <style><link><script>元素
  • isInvokedByMicroApp():檢測當前的路由是否是 active 路由

patchLooseSandbox() 中,isInvokedByMicroApp()僅僅檢測當前的路由是否是 active 路由

但是在 patchStrictSandbox() 中,isInvokedByMicroApp() 是檢測當前的 element 是否是微應用動態創建的元素

// single-spa的checkActivityFunctions
function checkActivityFunctions() {
  var location = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : window.location;
  return apps.filter(function (app) {
    return app.activeWhen(location);
  }).map(toName);
}
// isInvokedByMicroApp = () => checkActivityFunctions(window.location).some((name) => name === appName)
if (!isHijackingTag(element.tagName) || !isInvokedByMicroApp(element)) {
  return rawDOMAppendOrInsertBefore.call(this, element, refChild) as T;
}

function getOverwrittenAppendChildOrInsertBefore(opts) {
  function appendChildOrInsertBefore(newChild, refChild = null) {
    // opts參數:原始方法、是否由子應用調用、容器配置、目標容器(head/body)
    const {
      rawDOMAppendOrInsertBefore,
      isInvokedByMicroApp,
      containerConfigGetter,
      target = "body",
    } = opts;
    let element = newChild;
    // 非劫持標籤(非 script/style/link)或非子應用調用時,直接調用原始方法
    if (!isHijackingTag(element.tagName) || !isInvokedByMicroApp(element)) {
      return rawDOMAppendOrInsertBefore.call(this, element, refChild);
    }

    switch (element.tagName) {
      case LINK_TAG_NAME:
      case STYLE_TAG_NAME: {
        //...
      }
      case SCRIPT_TAG_NAME: {
        //...
      }
    }
    // ...
  }
  appendChildOrInsertBefore[overwrittenSymbol] = true; // 標記為已重寫
  return appendChildOrInsertBefore;
}

劫持appendChildinsertBefore進行重寫後,如果插入的是

  • linkstyle 標籤

    • 如果命中excludeAssetFilter,則直接插入
    • 否則將 link 轉化為 style 標籤進行內聯,並且通過 ScopedCSS 重寫該內聯 CSS 規則,添加前綴的命名空間,實現樣式隔離 + 記錄樣式到 dynamicStyleSheetElements
case LINK_TAG_NAME:
case STYLE_TAG_NAME: {
  const { href, rel } = element;
  // 如果資源被排除過濾器(excludeAssetFilter)捕獲,直接插入
  if (excludeAssetFilter && href && excludeAssetFilter(href)) {
    return rawDOMAppendOrInsertBefore.call(this, element, refChild);
  }
  // 標記元素的目標容器(head/body),並記錄到 appWrapper 中
  defineNonEnumerableProperty(stylesheetElement, styleElementTargetSymbol, target);
  const appWrapper = appWrapperGetter(); // 獲取子應用容器
  if (scopedCSS) { // 啓用 CSS 作用域隔離時
    if (element.tagName === "LINK" && rel === "stylesheet" && href) {
      // 將 link 轉換為 style 標籤,並內聯 CSS 內容(防止全局污染)
      stylesheetElement = convertLinkAsStyle(...);
    }
    // 通過 ScopedCSS 類重寫 CSS 規則,添加命名空間前綴
    const scopedCSSInstance = new _ScopedCSS();
    scopedCSSInstance.process(stylesheetElement, prefix);
  }
  // 插入到子應用容器的指定位置
  const mountDOM = target === "head" ? getAppWrapperHeadElement(appWrapper) : appWrapper;
  const result = rawDOMAppendOrInsertBefore.call(mountDOM, stylesheetElement, referenceNode);
  dynamicStyleSheetElements.push(stylesheetElement); // 記錄動態樣式表
  return result;
}
  • script 標籤

    • 如果命中excludeAssetFilter,則直接插入,不做任何處理
    • 如果 elemnet.src 存在,則進行 js 文件的下載並存入緩存中,然後對每一個 JS 都調用 geval(scriptSrc, inlineScript) 外層進行沙箱代碼的包裹(with(window) { ...srcipt })並且執行,然後將動態插入的內容轉化為註釋節點
    • 如果elemnet.src 不存在,説明是內聯 js,對每一個 JS 都調用 geval(scriptSrc, inlineScript) 外層進行沙箱代碼的包裹(with(window) { ...srcipt })並且執行,然後替換為註釋節點,然後將動態插入的內容轉化為註釋節點
case SCRIPT_TAG_NAME: {
  const { src, text } = element;
  // 如果資源被排除或非可執行腳本類型,直接插入
  if (excludeAssetFilter && src && excludeAssetFilter(src) || !isExecutableScriptType(element)) {
    return rawDOMAppendOrInsertBefore.call(this, element, refChild);
  }
  if(src) {
    // 外部js:執行子應用腳本
    execScripts(null, [src], proxy, {fetch: fetch2, ...})
    // 替換為註釋節點,防止重複執行
    const dynamicScriptCommentElement = document.createComment(`dynamic script ${src} replaced by qiankun`);
    dynamicScriptAttachedCommentMap.set(element, dynamicScriptCommentElement);
    return rawDOMAppendOrInsertBefore.call(mountDOM, dynamicScriptCommentElement, referenceNode);
  } else {
    // 內聯js:執行子應用腳本
    execScripts(proxy, [`<script>${text}</script>`], { strictGlobal, scopedGlobalVariables });
    // 替換為註釋節點,防止重複執行
    const dynamicInlineScriptCommentElement = document.createComment("dynamic inline script replaced by qiankun");
    return rawDOMAppendOrInsertBefore.call(mountDOM, dynamicInlineScriptCommentElement, refChild);
  }
}
6.4.4.1.2 getNewRemoveChild()

getOverwrittenAppendChildOrInsertBefore() 類似,在兩個條件的判斷後:

  • isHijackingTag(): <style><link><script>元素
  • isInvokedByMicroApp():檢測當前的路由是否是 active 路由

才會觸發 removeChild() 的處理

也是針對兩種類型進行處理

  • linkstyle 標籤:

    • dynamicLinkAttachedInlineStyleMap獲取對應的 style 標籤(如果不存在,則還是 child),然後調用原生的 removeChild 進行刪除
    • 移除動態樣式dynamicStyleSheetElements的數據對應的 child 數據,防止恢復微應用激活狀態時錯誤將它恢復
case STYLE_TAG_NAME:
case LINK_TAG_NAME: {
  attachedElement = dynamicLinkAttachedInlineStyleMap.get(child) || child;
  const dynamicElementIndex = dynamicStyleSheetElements.indexOf(attachedElement);
  if (dynamicElementIndex !== -1) {
    dynamicStyleSheetElements.splice(dynamicElementIndex, 1);
  }
  break;
}
const appWrapper = appWrapperGetter();
const container = target === "head" ? getAppWrapperHeadElement(appWrapper) : appWrapper;
if (container.contains(attachedElement)) {
  return rawRemoveChild2.call(attachedElement.parentNode, attachedElement);
}
  • script 標籤:dynamicScriptAttachedCommentMap獲取對應的 註釋節點 js(如果不存在,則還是 child),然後調用原生的 removeChild 進行刪除
case SCRIPT_TAG_NAME: {
  attachedElement = dynamicScriptAttachedCommentMap.get(child) || child;
  break;
}
const appWrapper = appWrapperGetter();
const container = target === "head" ? getAppWrapperHeadElement(appWrapper) : appWrapper;
if (container.contains(attachedElement)) {
  return rawRemoveChild2.call(attachedElement.parentNode, attachedElement);
}
6.4.4.2 unpatchDynamicAppendPrototypeFunctions()

在觸發 free() 時,首先會觸發unpatchDynamicAppendPrototypeFunctions(),恢復劫持的 appendChildremoveChildinsertBefore 為原生方法

function patchHTMLDynamicAppendPrototypeFunctions() {
  return function unpatch() {
    HTMLHeadElement.prototype.appendChild = rawHeadAppendChild2;
    HTMLHeadElement.prototype.removeChild = rawHeadRemoveChild;
    HTMLBodyElement.prototype.appendChild = rawBodyAppendChild;
    HTMLBodyElement.prototype.removeChild = rawBodyRemoveChild;
    HTMLHeadElement.prototype.insertBefore = rawHeadInsertBefore2;
  };
}
const unpatchDynamicAppendPrototypeFunctions =
  patchHTMLDynamicAppendPrototypeFunctions();
6.4.4.3 recordStyledComponentsCSSRules()

將動態添加的樣式添加到styledComponentCSSRulesMap進行存儲,等待下一次激活時觸發 rebuild() 時使用

function recordStyledComponentsCSSRules(styleElements) {
  styleElements.forEach((styleElement) => {
    if (
      styleElement instanceof HTMLStyleElement &&
      isStyledComponentsLike(styleElement)
    ) {
      if (styleElement.sheet) {
        styledComponentCSSRulesMap.set(
          styleElement,
          styleElement.sheet.cssRules
        );
      }
    }
  });
}
6.4.4.4 rebuildCSSRules()

在微應用重新掛載時,重建動態樣式表,從之前 free() 時記錄的 styledComponentCSSRulesMap 獲取對應的 cssRules,然後不斷調用 insertRule() 恢復樣式

防止在微應用切換時,之前動態插入的樣式丟失
return function rebuild() {
  rebuildCSSRules(dynamicStyleSheetElements, (stylesheetElement) => {
    const appWrapper = appWrapperGetter();
    if (!appWrapper.contains(stylesheetElement)) {
      document.head.appendChild.call(appWrapper, stylesheetElement);
      return true;
    }
    return false;
  });
  if (mounting) {
    dynamicStyleSheetElements = [];
  }
};
function rebuildCSSRules(styleSheetElements, reAppendElement) {
  styleSheetElements.forEach((stylesheetElement) => {
    const appendSuccess = reAppendElement(stylesheetElement);
    if (appendSuccess) {
      if (
        stylesheetElement instanceof HTMLStyleElement &&
        isStyledComponentsLike(stylesheetElement)
      ) {
        const cssRules = getStyledElementCSSRules(stylesheetElement);
        if (cssRules) {
          for (let i = 0; i < cssRules.length; i++) {
            const cssRule = cssRules[i];
            const cssStyleSheetElement = stylesheetElement.sheet;
            cssStyleSheetElement.insertRule(
              cssRule.cssText,
              cssStyleSheetElement.cssRules.length
            );
          }
        }
      }
    }
  });
}
6.4.4.5 總結

通過 patchHTMLDynamicAppendPrototypeFunctions() 劫持並重寫 appendChildinsertBeforeremoveChild等 DOM 操作方法,實現對微應用動態場景的 <style><link><script>標籤的隔離和管理,實現:

  • 隔離微應用資源:將樣式轉化為內聯樣式插入到微應用中,將 JS 代碼轉化為沙箱代碼進行隔離,防止微應用的 CSS 和 JS 污染基座
  • 動態資源跟蹤:記錄微應用動態創建的資源,便於後續微應用 unmount 時移除資源 + 重新激活微應用時恢復資源

並且通過 patchHTMLDynamicAppendPrototypeFunctions() 拿到對應的 free() 方法:恢復appendChildinsertBeforeremoveChild為原生方法

這裏的 free() 方法是針對 patchHTMLDynamicAppendPrototypeFunctions() 的!

然後暴露出去 patchLooseSandboxfree(),執行 free() 後可以獲取 rebuild() 方法

  • free()patchHTMLDynamicAppendPrototypeFunctions()free() 恢復多個劫持方法為原生方法 + 記錄動態插入的樣式 dynamicStyleSheetElements
  • rebuild():利用 free() 記錄的 dynamicStyleSheetElements 進行樣式的重建( <style> 插入到 DOM)
function patchLooseSandbox() {
  const { proxy } = sandbox;
  let dynamicStyleSheetElements = [];
  const unpatchDynamicAppendPrototypeFunctions =
    patchHTMLDynamicAppendPrototypeFunctions(
      () =>
        checkActivityFunctions(window.location).some(
          (name) => name === appName
        ),
      () => ({
        appName,
        appWrapperGetter,
        proxy,
        strictGlobal: false,
        speedySandbox: false,
        scopedCSS,
        dynamicStyleSheetElements,
        excludeAssetFilter,
      })
    );

  return function free() {
    if (isAllAppsUnmounted()) unpatchDynamicAppendPrototypeFunctions();
    recordStyledComponentsCSSRules(dynamicStyleSheetElements);
    return function rebuild() {
      rebuildCSSRules(dynamicStyleSheetElements, (stylesheetElement) => {
        const appWrapper = appWrapperGetter();
        if (!appWrapper.contains(stylesheetElement)) {
          document.head.appendChild.call(appWrapper, stylesheetElement);
          return true;
        }
        return false;
      });
      if (mounting) {
        dynamicStyleSheetElements = [];
      }
    };
  };
}
6.4.5 patchStrictSandbox()

patchLooseSandbox() 相比較,由於沙箱可以是多個,因此會使用經過一系列邏輯得到當前的沙箱配置 containerConfig

然後還是使用 patchHTMLDynamicAppendPrototypeFunctions() 劫持並重寫 appendChildinsertBeforeremoveChild等 DOM 操作方法,實現對微應用動態場景的 <style><link><script>標籤的隔離和管理

function patchStrictSandbox() {
  const { proxy } = sandbox;
  let containerConfig = proxyAttachContainerConfigMap.get(proxy);
  //...
  const { dynamicStyleSheetElements } = containerConfig;
  const unpatchDynamicAppendPrototypeFunctions =
    patchHTMLDynamicAppendPrototypeFunctions(
      (element) => elementAttachContainerConfigMap.has(element),
      (element) => elementAttachContainerConfigMap.get(element)
    );
  const unpatchDocument = patchDocument({ sandbox, speedy: speedySandbox });
  return function free() {
    if (isAllAppsUnmounted()) {
      unpatchDynamicAppendPrototypeFunctions();
      unpatchDocument();
    }
    recordStyledComponentsCSSRules(dynamicStyleSheetElements);
    return function rebuild() {
      rebuildCSSRules(dynamicStyleSheetElements, (stylesheetElement) => {
        const appWrapper = appWrapperGetter();
        const mountDom =
          stylesheetElement[styleElementTargetSymbol] === "head"
            ? getAppWrapperHeadElement(appWrapper)
            : appWrapper;
        if (typeof refNo === "number" && refNo !== -1) {
          rawHeadInsertBefore.call(mountDom, stylesheetElement, refNode);
        } else {
          rawHeadAppendChild.call(mountDom, stylesheetElement);
        }
      });
    };
  };
}

patchLooseSandbox() 相比較,getOverwrittenAppendChildOrInsertBefore() 傳入的參數是不同的,這裏 isInvokedByMicroApp() 是使用 elementAttachContainerConfigMap 進行判斷,即如果不是當前沙箱創建的元素,則不劫持和重寫!

function getOverwrittenAppendChildOrInsertBefore(opts) {
  function appendChildOrInsertBefore(newChild, refChild = null) {
    // opts參數:原始方法、是否由子應用調用、容器配置、目標容器(head/body)
    const {
      rawDOMAppendOrInsertBefore,
      isInvokedByMicroApp,
      containerConfigGetter,
      target = "body",
    } = opts;
    let element = newChild;
    // 非劫持標籤(非 script/style/link)或非子應用調用時,直接調用原始方法
    if (!isHijackingTag(element.tagName) || !isInvokedByMicroApp(element)) {
      return rawDOMAppendOrInsertBefore.call(this, element, refChild);
    }
    // ...
  }
  appendChildOrInsertBefore[overwrittenSymbol] = true; // 標記為已重寫
  return appendChildOrInsertBefore;
}

回到 patchStrictSandbox() 的分析,可以發現除了增加 patchDocument(),其他的邏輯基本是一致的,也就是:

通過 patchHTMLDynamicAppendPrototypeFunctions() 拿到對應的 free() 方法:恢復appendChildinsertBeforeremoveChild為原生方法

然後暴露出去 patchStrictSandbox()free()rebuild() 方法

  • free():恢復appendChildinsertBeforeremoveChild為原生方法 + 記錄動態插入的樣式 dynamicStyleSheetElements,然後增加了 unpatchDocument() 的處理
  • rebuild():利用 free() 記錄的 dynamicStyleSheetElements 進行樣式的重建( <style> 插入到 DOM)
下面我們將針對 patchDocument()unpatchDocument() 展開分析
6.4.5.1 patchDocument()

顧名思義,就是對 document 進行劫持重寫

暫時不考慮 speedy 的情況,從下面代碼可以知道,本質就是劫持 document.createElement,然後遇到 script/style/link 時,也就是 document.createELement("script") 觸發 attachElementToProxy(),將動態創建的元素加入到 elementAttachContainerConfigMap

然後提供對應的 unpatch() 恢復 document.createElement

function patchDocument(cfg) {
  const { sandbox, speedy } = cfg;
  const attachElementToProxy = (element, proxy) => {
    const proxyContainerConfig = proxyAttachContainerConfigMap.get(proxy);
    if (proxyContainerConfig) {
      elementAttachContainerConfigMap.set(element, proxyContainerConfig);
    }
  };
  if (speedy) {...}
  const docCreateElementFnBeforeOverwrite = docCreatePatchedMap.get(
    document.createElement
  );
  if (!docCreateElementFnBeforeOverwrite) {
    const rawDocumentCreateElement = document.createElement;
    Document.prototype.createElement = function createElement2(
      tagName,
      options
    ) {
      const element = rawDocumentCreateElement.call(this, tagName, options);
      if (isHijackingTag(tagName)) {
        const { window: currentRunningSandboxProxy } =
          getCurrentRunningApp() || {};
        if (currentRunningSandboxProxy) {
          attachElementToProxy(element, currentRunningSandboxProxy);
        }
      }
      return element;
    };
    if (document.hasOwnProperty("createElement")) {
      document.createElement = Document.prototype.createElement;
    }
    docCreatePatchedMap.set(
      Document.prototype.createElement,
      rawDocumentCreateElement
    );
  }
  return function unpatch() {
    if (docCreateElementFnBeforeOverwrite) {
      Document.prototype.createElement = docCreateElementFnBeforeOverwrite;
      document.createElement = docCreateElementFnBeforeOverwrite;
    }
  };
}

elementAttachContainerConfigMap 就是上面 patchHTMLDynamicAppendPrototypeFunctions() 中傳入的 isInvokedByMicroApp() ,也就是説如果 script/style/link 不是當前微應用創建的元素,則不進行劫持重寫 appendChildinsertBeforeremoveChild等方法

比如當前微應用創建了 <script>,那麼它觸發 script.appendChild(A) 就會被劫持並且記錄 A 這個資源
const unpatchDynamicAppendPrototypeFunctions =
  patchHTMLDynamicAppendPrototypeFunctions(
    (element) => elementAttachContainerConfigMap.has(element),
    (element) => elementAttachContainerConfigMap.get(element)
  );

當考慮 speedy 時,我們知道這個模式是為了劫持 document從而提高性能

https://github.com/umijs/qiankun/pull/2271

patchDocument()的邏輯主要分為 3 個部分:

  • 使用 Proxy 劫持 documentcreateElement()querySelector()

    • createElement() 觸發 attachElementToProxy() 將當前的元素加入到 elementAttachContainerConfigMap
    • querySelector() 將直接使用微應用專用的 qiankunHead,避免直接插入到基座的 head 元素中
const modifications = {};
const proxyDocument = new Proxy(document, {
  /
   * Read and write must be paired, otherwise the write operation will leak to the global
   */
  set: (target, p, value) => {
    switch (p) {
      case "createElement": {
        modifications.createElement = value;
        break;
      }
      case "querySelector": {
        modifications.querySelector = value;
        break;
      }
      default:
        target[p] = value;
        break;
    }
    return true;
  },
  get: (target, p, receiver) => {
    switch (p) {
      case "createElement": {
        const targetCreateElement =
          modifications.createElement || target.createElement;
        return function createElement2(...args) {
          if (!nativeGlobal.__currentLockingSandbox__) {
            nativeGlobal.__currentLockingSandbox__ = sandbox.name;
          }
          const element = targetCreateElement.call(target, ...args);
          if (nativeGlobal.__currentLockingSandbox__ === sandbox.name) {
            attachElementToProxy(element, sandbox.proxy);
            delete nativeGlobal.__currentLockingSandbox__;
          }
          return element;
        };
      }
      case "querySelector": {
        const targetQuerySelector =
          modifications.querySelector || target.querySelector;
        return function querySelector(...args) {
          const selector = args[0];
          switch (selector) {
            case "head": {
              const containerConfig = proxyAttachContainerConfigMap.get(
                sandbox.proxy
              );
              if (containerConfig) {
                const qiankunHead = getAppWrapperHeadElement(
                  containerConfig.appWrapperGetter()
                );
                qiankunHead.appendChild = HTMLHeadElement.prototype.appendChild;
                qiankunHead.insertBefore =
                  HTMLHeadElement.prototype.insertBefore;
                qiankunHead.removeChild = HTMLHeadElement.prototype.removeChild;
                return qiankunHead;
              }
              break;
            }
          }
          return targetQuerySelector.call(target, ...args);
        };
      }
    }
    const value = target[p];
    if (isCallable(value) && !isBoundedFunction(value)) {
      return function proxyFunction(...args) {
        return value.call(
          target,
          ...args.map((arg) => (arg === receiver ? target : arg))
        );
      };
    }
    return value;
  },
});
  • sandbox.patchDocument(proxyDocument);:將沙箱內部持有的 document 改變為 new Proxy() 代理的 document
class ProxySandbox {
  public patchDocument(doc: Document) {
    this.document = doc;
  }
}
  • 修復部分原型方法

    • MutationObserver.prototype.observeNode.prototype.compareDocumentPosition直接將 target 改為基座的 document,避免報錯
    • Node.prototype.parentNode 子應用可能判斷 document === html.parentNode,但代理 document 會導致結果為 false
  • MutationObserver.prototype.observeNode.prototype.compareDocumentPosition 可以參考 https://github.com/umijs/qiankun/issues/2406 的描述
  • Node.prototype.parentNode 可以參考https://github.com/umijs/qiankun/issues/2408 的描述
MutationObserver.prototype.observe = function observe(target, options) {
  const realTarget = target instanceof Document ? nativeDocument : target;
  return nativeMutationObserverObserveFn.call(this, realTarget, options);
};
Node.prototype.compareDocumentPosition = function compareDocumentPosition(
  node
) {
  const realNode = node instanceof Document ? nativeDocument : node;
  return prevCompareDocumentPosition.call(this, realNode);
};
Object.defineProperty(Node.prototype, "parentNode", {
  get() {
    const parentNode = parentNodeGetter.call(this);
    if (parentNode instanceof Document) {
      const proxy = getCurrentRunningApp()?.window;
      if (proxy) return proxy.document;
    }
    return parentNode;
  },
});

參考

  1. 微前端-李永寧的專欄
user avatar tianmiaogongzuoshi_5ca47d59bef41 Avatar haoqidewukong Avatar nihaojob Avatar jingdongkeji Avatar aqiongbei Avatar longlong688 Avatar huajianketang Avatar inslog Avatar banana_god Avatar Dream-new Avatar xiaoxxuejishu Avatar zero_dev Avatar
Favorites 149 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.