整体核心流程

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.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. 微前端-李永宁的专栏

白边
215 声望42 粉丝

源码爱好者,已经完成vue2和vue3的源码解析+webpack5整体流程源码+vite4开发环境核心流程源码+koa2源码+qiankun源码