学习微前端框架之qiankun(一) 基于路由配置

1,217 阅读5分钟

主应用调用
1. registerMicroApps 注册微应用
2. start 启动
这两个api,微应用在入口提供生命周期回调函数

简要流程

qiankun流程图.png qiankun的底层还是调用了single-spa,它额外做了以下处理:

  1. 样式隔离
  2. JS沙箱
  3. 资源预加载
  4. HTML Entry接入方式
  5. 应用间通信

主要API

一、 registerMicroApps(apps, lifeCycles)

  1. 全局变量microApps用来存储所有已注册的微应用
  2. 从apps中找出未注册的微应用unregisteredApps
  3. 将unregisteredApps并入microApps
  4. 遍历unregisteredApps,调用single-sparegisterApplication 以下app会在触发微应用activeRule时执行
app: async () => {
    // 调用app.loader(loading 状态发生变化时会调用的方法。)
    loader(true);
    // 确保先启动了再执行这里(关注start api的末尾)
    await frameworkStartedDefer.promise;

    const { mount, ...otherMicroAppConfigs } = (
       // frameworkConfiguration:start 方法执行时设置的配置对象
      await loadApp({ name, props, ...appConfig }, frameworkConfiguration, lifeCycles)
    )();

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

以上loadApp加载微应用

  1. 获取微应用的入口 html 内容和脚本执行器(调用import-html-entry的importEntry)
const { template, execScripts, assetPublicPath } = await importEntry(entry, importEntryOpts);

以上即qiankun在single-spa基础上的优化:HTML Entry接入方式
解决single-spa需要自己提供加载微应用函数,需要将微应用打包成一个js文件,这样对微应用的修改成本太高,不易维护。

  1. single-spa 的限制,加载、初始化和卸载不能同时进行,必须等卸载完成以后才可以进行加载,这个 promise 会在微应用卸载完成后被 resolve
if (await validateSingularMode(singular, app)) {
    await (prevAppUnmountedDeferred && prevAppUnmountedDeferred.promise);
  }
  1. 用一个容器元素包裹微应用入口 html 模版, -> appContent
`<div id="`__qiankun_microapp_wrapper_for_${snakeCase(id)}__`" data-name="${name}" data-version="${version}">${tpl}</div>`
  1. 根据start参数选项中配置的sandbox
  • strictStyleIsolation:是否开启严格的样式隔离模式。这种模式下 qiankun 会为每个微应用的容器包裹上一个 shadow dom 节点,从而确保微应用的样式不会对全局造成影响。
  • experimentalStyleIsolation: 设置为 true 时,qiankun 会改写子应用所添加的样式,为所有样式规则增加一个特殊的选择器规则来限定其影响范围
    将appContent转换成dom节点initialAppWrapperElement
const appContent = getDefaultTplWrapper(appInstanceId, appName)(template);

  const strictStyleIsolation = typeof sandbox === 'object' && !!sandbox.strictStyleIsolation;

  const scopedCSS = isEnableScopedCSS(sandbox);
  let initialAppWrapperElement: HTMLElement | null = createElement(
    appContent,
    strictStyleIsolation,
    scopedCSS,
    appName,
  );

以上即qiankun在single-spa基础上的优化:样式隔离
有两种方式

shadow DOM: Web components 的一个重要属性是封装——可以将标记结构、样式和行为隐藏起来,并与页面上的其他代码相隔离,保证不同的部分不会混在一起,可使代码更加干净、整洁。其中,Shadow DOM 接口是关键所在,它可以将一个隐藏的、独立的 DOM 附加到一个元素上。

  1. 获取渲染函数
const render = getRender(appName, appContent, legacyRender);
  1. 执行render函数 调用app.render(后续版本会删除)或者将container清除后插入element
render({ element: initialAppWrapperElement, loading: true, container: initialContainer }, 'loading');
  1. 生成一个获取微应用dom(div id="__qiankun_microapp_wrapper_for_${snakeCase(id)}__")的方法
const initialAppWrapperGetter = getAppWrapperGetter(
    appName,
    appInstanceId,
    !!legacyRender,
    strictStyleIsolation,
    scopedCSS,
    () => initialAppWrapperElement,
  );
  1. 沙箱
let global = globalContext;
let mountSandbox = () => Promise.resolve();
let unmountSandbox = () => Promise.resolve();
const useLooseSandbox = typeof sandbox === 'object' && !!sandbox.loose;
let sandboxContainer;
if (sandbox) {
sandboxContainer = createSandboxContainer(
  appName,
  // FIXME should use a strict sandbox logic while remount, see https://github.com/umijs/qiankun/issues/518
  initialAppWrapperGetter,
  scopedCSS,
  useLooseSandbox,
  excludeAssetFilter,
  global,
);
// 用沙箱的代理对象作为接下来使用的全局对象
global = sandboxContainer.instance.proxy as typeof window;
mountSandbox = sandboxContainer.mount;
unmountSandbox = sandboxContainer.unmount;
}

三种模式:

  1. 不支持Proxy: SnapshotSandbox
  2. 支持Proxy单例:LegacySandbox
  3. 支持Proxy多例:ProxySandbox
  1. 合并生命周期
const {
    beforeUnmount = [],
    afterUnmount = [],
    afterMount = [],
    beforeMount = [],
    beforeLoad = [],
  } = mergeWith({}, getAddOns(global, assetPublicPath), lifeCycles, (v1, v2) => concat(v1 ?? [], v2 ?? []));
  1. 执行beforeLoad
await execHooksChain(toArray(beforeLoad), app, global);
  1. 获取微应用入口导出的生命周期函数
const scriptExports: any = await execScripts(global, sandbox && !useLooseSandbox);
const { bootstrap, mount, unmount, update } = getLifecyclesFromExports(
    scriptExports,
    appName,
    global,
    sandboxContainer?.instance?.latestSetProp,
  );
  1. 给微应用注册通信方法并返回通信方法,然后会将通信方法通过 props 注入到微应用
const { onGlobalStateChange, setGlobalState, offGlobalStateChange }: Record<string, CallableFunction> =
    getMicroAppStateActions(appInstanceId);

通信:设置全局对象globalState,调用initGlobalState可以对其进行初始化,微应用可以通过以上三个方法监听state修改,修改state,移除监听。

  1. 返回mount等single-spa的registerApplication的app需要return的promise<生命周期集合>
// FIXME temporary way
  const syncAppWrapperElement2Sandbox = (element: HTMLElement | null) => (initialAppWrapperElement = element);

  const parcelConfigGetter: ParcelConfigObjectGetter = (remountContainer = initialContainer) => {
    let appWrapperElement: HTMLElement | null;
    let appWrapperGetter: ReturnType<typeof getAppWrapperGetter>;

    const parcelConfig: ParcelConfigObject = {
      name: appInstanceId,
      bootstrap,
      mount: [ // 挂载阶段需要执行的一系列方法
        async () => {
          if (process.env.NODE_ENV === 'development') {
            const marks = performanceGetEntriesByName(markName, 'mark');
            // mark length is zero means the app is remounting
            if (marks && !marks.length) {
              performanceMark(markName);
            }
          }
        },
        async () => { // 单例模式需要等微应用卸载完成以后才能执行挂载任务,promise 会在微应用卸载完以后 resolve
          if ((await validateSingularMode(singular, app)) && prevAppUnmountedDeferred) {
            return prevAppUnmountedDeferred.promise;
          }

          return undefined;
        },
        // initial wrapper element before app mount/remount
        async () => {
          appWrapperElement = initialAppWrapperElement;
          appWrapperGetter = getAppWrapperGetter(
            appName,
            appInstanceId,
            !!legacyRender,
            strictStyleIsolation,
            scopedCSS,
            () => appWrapperElement,
          );
        },
        // 添加 mount hook, 确保每次应用加载前容器 dom 结构已经设置完毕
        async () => {
          const useNewContainer = remountContainer !== initialContainer;
          if (useNewContainer || !appWrapperElement) {
            // element will be destroyed after unmounted, we need to recreate it if it not exist
            // or we try to remount into a new container
            appWrapperElement = createElement(appContent, strictStyleIsolation, scopedCSS, appName);
            syncAppWrapperElement2Sandbox(appWrapperElement);
          }

          render({ element: appWrapperElement, loading: true, container: remountContainer }, 'mounting');
        },
        mountSandbox, // 运行时沙箱导出的 mount
        // exec the chain after rendering to keep the behavior with beforeLoad
        async () => execHooksChain(toArray(beforeMount), app, global),
        // 向微应用的 mount 生命周期函数传递参数,比如微应用中使用的 props.onGlobalStateChange 方法
        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),
        // initialize the unmount defer after app mounted and resolve the defer after it unmounted
        async () => {
          if (await validateSingularMode(singular, app)) {
            prevAppUnmountedDeferred = new Deferred<void>();
          }
        },
        async () => {
          if (process.env.NODE_ENV === 'development') {
            const measureName = `[qiankun] App ${appInstanceId} Loading Consuming`;
            performanceMeasure(measureName, markName);
          }
        },
      ],
      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);
          // for gc
          appWrapperElement = null;
          syncAppWrapperElement2Sandbox(appWrapperElement);
        },
        async () => {
          if ((await validateSingularMode(singular, app)) && prevAppUnmountedDeferred) {
            prevAppUnmountedDeferred.resolve();
          }
        },
      ],
    };

    if (typeof update === 'function') {
      parcelConfig.update = update;
    }

    return parcelConfig;
  };

二、start(opts)

  1. 设置默认值,解析opts
frameworkConfiguration = { prefetch: true, singular: true, sandbox: true, ...opts };
  const {
    prefetch,
    sandbox,
    singular,
    urlRerouteOnly = defaultUrlRerouteOnly,
    ...importEntryOpts
  } = frameworkConfiguration;
  1. 预加载
if (prefetch) {
    doPrefetchStrategy(microApps, prefetch, importEntryOpts);
  }
  • prefetch为数组,监听: single-spa:first-mount,预加载数组中的微应用
prefetchAfterFirstMounted(appsName2Apps(prefetchStrategy as string[]), importEntryOpts);
  • prefetch为函数,执行该函数,在预加载时机,加载首屏应用,次屏应用
(async () => {
      // critical rendering apps would be prefetch as earlier as possible
      const { criticalAppNames = [], minorAppsName = [] } = await prefetchStrategy(apps);
      prefetchImmediately(appsName2Apps(criticalAppNames), importEntryOpts);
      prefetchAfterFirstMounted(appsName2Apps(minorAppsName), importEntryOpts);
    })();
  • 其他-true
prefetchAfterFirstMounted(apps, importEntryOpts);
  • 其他-all
prefetchImmediately(apps, importEntryOpts);
  1. 降级,不支持Proxy特性的低版本浏览器强制使用单例模式
frameworkConfiguration = autoDowngradeForLowVersionBrowser(frameworkConfiguration);
  1. 执行single-spa的start
startSingleSpa({ urlRerouteOnly });
  1. 全局设置标识值started,用于在手动调用loadMicroApp时判断是否已启动
started = true;
  1. 全局设置,用于在执行single-spa的app方法时先等待start执行完再loadApp
frameworkStartedDefer.resolve();

调用的singl-spa API

registerApplication(appNameOrConfig, appOrLoadApp, activeWhen, customProps)

  1. 解析、验证参数
const registration = sanitizeArguments(
    appNameOrConfig,
    appOrLoadApp,
    activeWhen,
    customProps
  );
  1. apps为全局变量,存储已注册的微应用。如果当前应用已注册则抛出错误
if (getAppNames().indexOf(registration.name) !== -1)
    throw Error(
      formatErrorMessage(
        21,
        __DEV__ &&
          `There is already an app registered with name ${registration.name}`,
        registration.name
      )
    );
  1. 将当前应用存入apps,状态为NOT_LOADED
apps.push(
    assign(
      {
        loadErrorTime: null,
        status: NOT_LOADED,
        parcels: {},
        devtools: {
          overlays: {
            options: {},
            selectors: [],
          },
        },
      },
      registration
    )
  );
  1. 确保兼容jQuery,添加补丁;执行主要逻辑reroute
if (isInBrowser) {
    ensureJQuerySupport();
    reroute();
  }

以上reroute执行场景:

  • 注册微应用
  • 启动
  • 路由变化
  1. 等待切换
if (appChangeUnderway) {
    return new Promise((resolve, reject) => {
      peopleWaitingOnAppChange.push({
        resolve,
        reject,
        eventArguments,
      });
    });
  }
  1. 根据状态将apps整理归类到以下4个数组中
const {
    appsToUnload,
    appsToUnmount,
    appsToLoad,
    appsToMount,
  } = getAppChanges();
  1. 如果已经执行过start方法,则performAppChanges
if (isStarted()) {
    appChangeUnderway = true;
    appsThatChanged = appsToUnload.concat(
      appsToLoad,
      appsToUnmount,
      appsToMount
    );
    return performAppChanges();
  }
  1. 如果没有执行过start方法,则loadApps
else {
    appsThatChanged = appsToLoad;
    return loadApps();
  }

以上loadApps:加载微应用
重要逻辑: toLoadPromise: 执行registerApplication的第二个参数app,将得到的生命周期函数注入每个微应用,此时状态是 NOT_BOOTSTRAPPED
-》 callAllEventListeners:监听浏览器路由事件

return Promise.resolve().then(() => {
      const loadPromises = appsToLoad.map(toLoadPromise);

      return (
        Promise.all(loadPromises)
          .then(callAllEventListeners)
          // there are no mounted apps, before start() is called, so we always return []
          .then(() => [])
          .catch((err) => {
            callAllEventListeners();
            throw err;
          })
      );
    });

以上performAppChanges:执行改变

  1. 触发状态变更事件
window.dispatchEvent(
        new CustomEvent(
          appsThatChanged.length === 0
            ? "single-spa:before-no-app-change"
            : "single-spa:before-app-change",
          getCustomEventDetail(true)
        )
      );

      window.dispatchEvent(
        new CustomEvent(
          "single-spa:before-routing-event",
          getCustomEventDetail(true, { cancelNavigation })
        )
      );
  1. 针对导航取消的处理 finishUpAndReturn: appChangeUnderway置为false,继续reroute peopleWaitingOnAppChange
    navigateToUrl: 跳转到oldUrl
if (navigationIsCanceled) {
        window.dispatchEvent(
          new CustomEvent(
            "single-spa:before-mount-routing-event",
            getCustomEventDetail(true)
          )
        );
        finishUpAndReturn();
        navigateToUrl(oldUrl);
        return;
      }
  1. 移除
const unloadPromises = appsToUnload.map(toUnloadPromise);

      const unmountUnloadPromises = appsToUnmount
        .map(toUnmountPromise)
        .map((unmountPromise) => unmountPromise.then(toUnloadPromise));
  1. 卸载
const allUnmountPromises = unmountUnloadPromises.concat(unloadPromises);

      const unmountAllPromise = Promise.all(allUnmountPromises);

      unmountAllPromise.then(() => {
        window.dispatchEvent(
          new CustomEvent(
            "single-spa:before-mount-routing-event",
            getCustomEventDetail(true)
          )
        );
      });
  1. 加载
const loadThenMountPromises = appsToLoad.map((app) => {
        return toLoadPromise(app).then((app) =>
          tryToBootstrapAndMount(app, unmountAllPromise)
        );
      });
  1. 挂载
const mountPromises = appsToMount
        .filter((appToMount) => appsToLoad.indexOf(appToMount) < 0)
        .map((appToMount) => {
          return tryToBootstrapAndMount(appToMount, unmountAllPromise);
        });

start(opts)

started = true;
  if (opts && opts.urlRerouteOnly) {
    setUrlRerouteOnly(opts.urlRerouteOnly);
  }
  if (isInBrowser) {
    reroute();
  }