Skip to content
目录

06 · single-spa 集成、loadApp 与 loadMicroApp

registerMicroApps:与 single-spa 对齐

registerMicroApps 对每个应用调用 registerApplicationapp 异步函数里 await frameworkStartedDefer.promise,确保宿主 start() 已执行后再 loadApp

25:38:packages/qiankun/src/apis/registerMicroApps.ts
registerApplication({
      name,
      app: async () => {
        loader(true);
        await frameworkStartedDefer.promise;

        const { mount, ...otherMicroAppConfigs } = (
          await loadApp({ name, entry, container, props }, frameworkConfiguration, lifeCycles)
        )(container);

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

原因:single-spa 在 start 前也可能触发应用加载;Defer 把「框架就绪」与「应用加载」解耦,避免时序洞。

v3 中注释掉的 prefetch 等说明部分 2.x 策略尚未完全迁回门面层,以当前源码为准。

loadApp:返回 ParcelConfigObjectGetter

loadApp 先完成 一次 loadEntry(首次加载),返回函数 (mountContainer) => parcelConfig,以便 single-spa 在 mount 阶段传入真实容器。

remount:二次进入时手动 loadEntry

mountTimes > 1,会清空容器、拉无 script 的 HTML 字符串loadEntry,避免脚本重复执行:

139:148:packages/qiankun/src/core/loadApp.ts
if (mountTimes > 1) {
            initContainer(mountContainer, appName, { sandboxCfg: sandbox, mountTimes, instanceId });
            // html scripts should be removed to avoid repeatedly execute
            const htmlString = await getPureHTMLStringWithoutScripts(entry, enhancedFetch);
            await loadEntry(
              { url: entry, res: new Response(htmlString, { status: 200, statusText: 'OK' }) },
              mountContainer,
              containerOpts,
            );
          }

mount 链顺序要点

  1. 更新 microAppDOMContainer 引用;
  2. remount 时重新加载入口;
  3. mountSandbox(mountContainer)
  4. beforeMount hooks;
  5. 子应用 mount
  6. afterMount
  7. mountTimes++

unmount 链

beforeUnmount → 子应用 unmountunmountSandboxafterUnmountclearContainer

原因:先让业务卸载再撤沙箱与 DOM,避免沙箱销毁后仍有业务异步逻辑触碰伪全局。

生命周期导出:三种 fallback

getLifecyclesFromExports 顺序:

  1. 入口脚本导出对象(validateExportLifecycle);
  2. 沙箱 latestSetProp 指向的全局属性;
  3. 开发环境 warning 后尝试 globalContext[appName]
246:269:packages/qiankun/src/core/loadApp.ts
if (globalLatestSetProp) {
    const lifecycles = (globalContext as unknown as ObjectType)[globalLatestSetProp as never] as MicroAppLifeCycles;
    if (validateExportLifecycle(lifecycles)) {
      return lifecycles;
    }
  }
  ...
  throw new QiankunError(
    `You need to export lifecycle functions in ${appName} entry as neither globalLatestSetProp ${String(
      globalLatestSetProp,
    )} nor window['${appName}'] export correctly`,
  );

loadMicroApp:按容器 XPath 记忆加载

loadMicroApp 在开头计算 containerXPath,同一 name + xpath 复用 parcelConfigGetterPromise,避免重复执行入口脚本(注释说明):

58:61:packages/qiankun/src/apis/loadMicroApp.ts
/**
   * using name + container xpath as the micro app instance id,
   * it means if you're rendering a micro app to a dom which have been rendered before,
   * the micro app would not load and evaluate its lifecycles again
   */

同一容器多次 loadMicroApp 时,会在 mount 链前 await 前一个实例的 unmountPromise,避免并发挂载冲突(同文件 31-41 行)。

Webpack 多实例:data-webpack

同屏多实例同名应用时移除子容器内 script[data-webpack],注释指向 webpack runtime 用该属性判断「脚本已加载」从而跳过执行:

284:287:packages/qiankun/src/core/loadApp.ts
/**
 * To prevent webpack from skipping reload logic and causing the js not to re-execute when a micro app is loaded multiple times on the same viewport,
 * the data-webpack attribute of the script must be removed.
 * see https://github.com/webpack/webpack/blob/1f13ff9fe587e094df59d660b4611b1bd19aed4c/lib/runtime/LoadScriptRuntimeModule.js#L131-L136
 */

beforeLoad 时序 FIXME

loadAppawait loadEntry 之前execHooksChain(beforeLoad),而 loadEntry 内部会同步插入 DOM 并可能执行内联脚本,存在 hook 与真实 DOM/脚本执行顺序 难以严格保证的问题:

107:110:packages/qiankun/src/core/loadApp.ts
// FIXME Due to the asynchronous execution of loadEntry, the DOM of the sub-app is inserted synchronously through appendChild, and inline scripts are also executed synchronously. Therefore, the beforeLoad may need to rely on transformer configuration to coordinate and ensure the order of asynchronous operations.
  await execHooksChain(toArray(beforeLoad), app, global);

  const lifecycles = await lifecyclesPromise;

可简述为:理解 beforeLoad 时不要假设「子应用 DOM 尚未出现」;若强依赖顺序,需在 transformer/构建侧协调或接受当前限制。

Addon:runtimePublicPath

getAddOns 合并 runtimePublicPath 等,在 beforeLoad/beforeMount 写入 global.__INJECTED_PUBLIC_PATH_BY_QIANKUN__,供 webpack 子应用 publicPath 动态修正(packages/qiankun/src/addons/runtimePublicPath.ts)。

下一节

07-性能优化与预取.md

MIT License.