【微前端】qiankun v2.10.16(流程图)源码解析

整体核心流程

源码分析

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()

当用户触发 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 标签样式隔离

用一个

包裹 importEntry 拿到的微应用的 HTML 模板数据,同时处理模板中的