qiankun源码分析-4.start-1.md

start

在使用qiankun时,我们最后调用start方法,完成子应用的加载,那么start函数具体做了什么,我们今天深入源于分析下,源码如下:

ts 复制代码
const defaultUrlRerouteOnly = true;
export let frameworkConfiguration: FrameworkConfiguration = {};
let started = false;
export function start(opts: FrameworkConfiguration = {}) {
  // 设置全局默认配置,
  frameworkConfiguration = { prefetch: true, singular: true, sandbox: true, ...opts };
  // 获取配置中的  prefetch 以及 urlRerouteOnly, 默认都为true
  const { prefetch, urlRerouteOnly = defaultUrlRerouteOnly, ...importEntryOpts } = frameworkConfiguration;

  // 如果配置了 prefetch, 则执行预加载策略
  if (prefetch) {
    doPrefetchStrategy(microApps, prefetch, importEntryOpts);
  }

  // 为低版本的浏览器自动降级
  frameworkConfiguration = autoDowngradeForLowVersionBrowser(frameworkConfiguration);

  // 执行single-spa中的start函数
  startSingleSpa({ urlRerouteOnly });
  started = true;

  frameworkStartedDefer.resolve();
}

当前我们的start函数的入参为空,所以这里的opts为空对象,然后做了以下两件事情:

  1. 如果配置了 prefetch, 则配置预加载策略
  2. 执行single-spa中的start函数,设置started为true

1. 预加载策略

我们首先分析下预加载策略,这里的预加载策略是在doPrefetchStrategy函数中实现的,源码如下:

ts 复制代码
/**
 * 执行预加载策略
 * @param apps app列表
 * @param prefetchStrategy 预加载策略,可选 boolean | 'all' | string[] | function, 默认为 true
 * @param importEntryOpts import-html-entry 配置项,稍后分析
 */
export function doPrefetchStrategy(
  apps: AppMetadata[],
  prefetchStrategy: PrefetchStrategy,
  importEntryOpts?: ImportEntryOpts,
) {
  // 定义函数:将app name转换为app metadata
  const appsName2Apps = (names: string[]): AppMetadata[] => apps.filter((app) => names.includes(app.name));

  // 根据预加载策略,执行预加载
  if (Array.isArray(prefetchStrategy)) {
    prefetchAfterFirstMounted(appsName2Apps(prefetchStrategy as string[]), importEntryOpts);
  } else if (isFunction(prefetchStrategy)) {
    (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);
    })();
  } else {
    switch (prefetchStrategy) {
      // 默认为true 会执行这里的逻辑
      case true:
        // 在mounted之后 预加载所有app
        prefetchAfterFirstMounted(apps, importEntryOpts);
        break;

      case 'all':
        prefetchImmediately(apps, importEntryOpts);
        break;

      default:
        break;
    }
  }
}

首先看下入参,apps为应用列表,prefetchStrategy为预加载策略, 当前为 true,importEntryOptsimport-html-entry的配置项,当前为{singular: true, sandbox: true}。 我们继续分析doPrefetchStrategy函数,首先定义了一个函数appsName2Apps,该函数的作用是将app name转换为app metadata,然后根据预加载策略,执行预加载。 因为prefetchStrategy为true,所以会执行prefetchAfterFirstMounted函数,该函数的作用是在mounted之后 预加载所有app,源码如下:

ts 复制代码
// 在第一次mounted之后,预加载未加载的app
function prefetchAfterFirstMounted(apps: AppMetadata[], opts?: ImportEntryOpts): void {
  window.addEventListener('single-spa:first-mount', function listener() {
    // 获取未加载的app
    const notLoadedApps = apps.filter((app) => getAppStatus(app.name) === NOT_LOADED);

    if (process.env.NODE_ENV === 'development') {
      const mountedApps = getMountedApps();
      console.log(`[qiankun] prefetch starting after ${mountedApps} mounted...`, notLoadedApps);
    }

    // 预加载未加载的app
    notLoadedApps.forEach(({ entry }) => prefetch(entry, opts));

    window.removeEventListener('single-spa:first-mount', listener);
  });
}

该函数的实现很简单,就是监听single-spa:first-mount事件,当第一次应用被挂载时,预加载未加载的app,这里我们随后再来分析事件是如何被触发的。

暂时总结下:doPrefetchStrategy是用来注册预加载策略的,即在第一次应用被挂载时,预加载未加载的app。

2.frameworkConfiguration

我们回过头继续分析start函数,当前已经执行完预加载策略,接下来有一行代码

ts 复制代码
frameworkConfiguration = autoDowngradeForLowVersionBrowser(frameworkConfiguration);

该函数的作用重新修改了下frameworkConfiguration的配置,为低版本的浏览器自动降级,源码如下:

ts 复制代码
const autoDowngradeForLowVersionBrowser = (configuration: FrameworkConfiguration): FrameworkConfiguration => {
  const { sandbox = true, singular } = configuration;
  if (sandbox) {
    if (!window.Proxy) {
      // 不支持proxy, 使用快照沙箱
      console.warn('[qiankun] Missing window.Proxy, proxySandbox will degenerate into snapshotSandbox');

      if (singular === false) {
        console.warn(
          '[qiankun] Setting singular as false may cause unexpected behavior while your browser not support window.Proxy',
        );
      }

      return { ...configuration, sandbox: typeof sandbox === 'object' ? { ...sandbox, loose: true } : { loose: true } };
    }

    // 如果不支持结构赋值,将关闭快速模式,快速模式作用是什么?
    if (
      !isConstDestructAssignmentSupported() &&
      (sandbox === true || (typeof sandbox === 'object' && sandbox.speedy !== false))
    ) {
      console.warn(
        '[qiankun] Speedy mode will turn off as const destruct assignment not supported in current browser!',
      );

      return {
        ...configuration,
        sandbox: typeof sandbox === 'object' ? { ...sandbox, speedy: false } : { speedy: false },
      };
    }
  }

  return configuration;
};

函数入参为frameworkConfiguration,首先获取了frameworkConfiguration中的sandboxsingular, 这里我们的实参为{prefetch: true, singular: true, sandbox: true},然后判断是否支持window.Proxy,如果不支持window.Proxy,则使用快照沙箱,那么我们的函数return的值为:{prefetch: true, singular: true, sandbox: { loose: true }}, 如果支持window.Proxy,则判断是否支持结构赋值,如果不支持结构赋值,则关闭快速模式,那么我们的函数return的值为:{prefetch: true, singular: true, sandbox: { speedy: false }}

小结

autoDowngradeForLowVersionBrowser函数的作用是为低版本的浏览器自动降级,如果不支持window.Proxy,则使用快照沙箱,如果支持window.Proxy,则判断是否支持结构赋值,如果不支持结构赋值,则关闭快速模式。具体sandbox的不同值对应的逻辑是什么我们随后再分析

3. startSingleSpa

我们继续分析start函数,当预加载策略配置完,且做完自动降级的配置后,会执行我们很重要的一个函数startSingleSpa,该函数的作用是执行single-spa中的start函数,源码如下:

ts 复制代码
started = false;
export function start(opts) {
  started = true;
  if (opts && opts.urlRerouteOnly) {
    setUrlRerouteOnly(opts.urlRerouteOnly);
  }
  if (isInBrowser) {
    reroute();
  }
}

该函数的入参为{urlRerouteOnly: true},然后设置了started为true,然后执行setUrlRerouteOnly,该函数的作用是设置urlRerouteOnly,源码如下:

ts 复制代码
let urlRerouteOnly;

export function setUrlRerouteOnly(val) {
  urlRerouteOnly = val;
}

这里很简单,我们就不分析了,我们回过头继续分析start,接下来会执行reroute函数,源码如下:

ts 复制代码
export function reroute(pendingPromises = [], eventArguments) {
  const {
    appsToUnload,
    appsToUnmount,
    appsToLoad,
    appsToMount,
  } = getAppChanges();
  let appsThatChanged,
    navigationIsCanceled = false,
    oldUrl = currentUrl,
    newUrl = (currentUrl = window.location.href);

  // 是否已经执行start方法
  if (isStarted()) {
    appChangeUnderway = true;
    appsThatChanged = appsToUnload.concat(
      appsToLoad,
      appsToUnmount,
      appsToMount
    );
    return performAppChanges();
  } else {
    appsThatChanged = appsToLoad;
    return loadApps();
  }

  function cancelNavigation() {...}

  function loadApps() {...}

  function performAppChanges() {...}

  function finishUpAndReturn() {...}

  /* We need to call all event listeners that have been delayed because they were
   * waiting on single-spa. This includes haschange and popstate events for both
   * the current run of performAppChanges(), but also all of the queued event listeners.
   * We want to call the listeners in the same order as if they had not been delayed by
   * single-spa, which means queued ones first and then the most recent one.
   */
  function callAllEventListeners() {...}

  function getCustomEventDetail(isBeforeChanges = false, extraProperties) {...}
}

我们只保留了主要的逻辑,当我们调用isStarted函数时,会返回true,所以会执行performAppChanges函数,该函数的作用是执行应用程序的变更,源码如下:

ts 复制代码
 function performAppChanges() {
  return Promise.resolve().then(() => {
    // https://github.com/single-spa/single-spa/issues/545
    
    // ...

    const unloadPromises = appsToUnload.map(toUnloadPromise);

    const unmountUnloadPromises = appsToUnmount
      .map(toUnmountPromise)
      .map((unmountPromise) => unmountPromise.then(toUnloadPromise));

    const allUnmountPromises = unmountUnloadPromises.concat(unloadPromises);

    const unmountAllPromise = Promise.all(allUnmountPromises);

    unmountAllPromise.then(() => {
      window.dispatchEvent(
        new CustomEvent(
          "single-spa:before-mount-routing-event",
          getCustomEventDetail(true)
        )
      );
    });

    /* We load and bootstrap apps while other apps are unmounting, but we
     * wait to mount the app until all apps are finishing unmounting
     */
    const loadThenMountPromises = appsToLoad.map((app) => {
      return toLoadPromise(app).then((app) =>
        tryToBootstrapAndMount(app, unmountAllPromise)
      );
    });

    /* These are the apps that are already bootstrapped and just need
     * to be mounted. They each wait for all unmounting apps to finish up
     * before they mount.
     */
    const mountPromises = appsToMount
      .filter((appToMount) => appsToLoad.indexOf(appToMount) < 0)
      .map((appToMount) => {
        return tryToBootstrapAndMount(appToMount, unmountAllPromise);
      });
    return unmountAllPromise
      .catch((err) => {
        callAllEventListeners();
        throw err;
      })
      .then(() => {
        /* Now that the apps that needed to be unmounted are unmounted, their DOM navigation
         * events (like hashchange or popstate) should have been cleaned up. So it's safe
         * to let the remaining captured event listeners to handle about the DOM event.
         */
        callAllEventListeners();

        return Promise.all(loadThenMountPromises.concat(mountPromises))
          .catch((err) => {
            pendingPromises.forEach((promise) => promise.reject(err));
            throw err;
          })
          .then(finishUpAndReturn);
      });
  });
}

这里的promise嵌套有点多,我们不防从return出发,看看做了什么?首先是卸载应用unmountAllPromise,然后是挂载应用loadThenMountPromises,最后是执行finishUpAndReturn函数,对于我们当下的场景,待卸载应用和待挂载应用都是空,所以我们可以略过细节,去看最后的finishUpAndReturn函数,源码如下:

ts 复制代码
function finishUpAndReturn() {
  const returnValue = getMountedApps();
  pendingPromises.forEach((promise) => promise.resolve(returnValue));

  try {
    const appChangeEventName =
      appsThatChanged.length === 0
        ? "single-spa:no-app-change"
        : "single-spa:app-change";
    window.dispatchEvent(
      new CustomEvent(appChangeEventName, getCustomEventDetail())
    );
    window.dispatchEvent(
      new CustomEvent("single-spa:routing-event", getCustomEventDetail())
    );
  } catch (err) {
    /* We use a setTimeout because if someone else's event handler throws an error, single-spa
     * needs to carry on. If a listener to the event throws an error, it's their own fault, not
     * single-spa's.
     */
    setTimeout(() => {
      throw err;
    });
  }
// ...

  return returnValue;
}

刚刚我们提到了,没有挂载和卸载的应用,那么appsThatChanged自然为空,那么最后会触发事件single-spa:no-app-change,还记得我们在setDefaultMountApp函数中监听了single-spa:no-app-change事件吗?当没有应用被挂载时,跳转到默认应用,是不是串起来了?我们回过头看下setDefaultMountApp函数,源码如下:

ts 复制代码
export function setDefaultMountApp(defaultAppLink: string) {
  // can not use addEventListener once option for ie support
  window.addEventListener('single-spa:no-app-change', function listener() {
    const mountedApps = getMountedApps();
    if (!mountedApps.length) {
      navigateToUrl(defaultAppLink);
    }

    window.removeEventListener('single-spa:no-app-change', listener);
  });
}

这里mountedApps为空,所以会执行navigateToUrl函数,该函数的作用是跳转到默认应用,源码如下:

ts 复制代码
export function navigateToUrl(obj) {
  let url;
  if (typeof obj === "string") {
    url = obj;
  }
  // ...

  const current = parseUri(window.location.href);
  const destination = parseUri(url);

  if (url.indexOf("#") === 0) {
    window.location.hash = destination.hash;
  } else if (current.host !== destination.host && destination.host) {
    if (process.env.BABEL_ENV === "test") {
      return { wouldHaveReloadedThePage: true };
    } else {
      window.location.href = url;
    }
  } else if (
    destination.pathname === current.pathname &&
    destination.search === current.search
  ) {
    window.location.hash = destination.hash;
  } else {
    // different path, host, or query params
    window.history.pushState(null, null, url);
  }
}

在我们的场景下,defaultAppLink/react16,最终进入最后一个else逻辑window.history.pushState(null, null, url),这里是个重点 , 我们都知道pushState会改变浏览器的历史记录,地址栏会变成塞入的url,但是页面不会改变,那么问题来了,我们是如何跳转到/react16的呢?我们回过头看下reroute函数,源码如下:

ts 复制代码
// single-spa/src/navigation/navigation-events.js

window.history.pushState = patchedUpdateState(
    window.history.pushState,
    "pushState"
  );
window.history.replaceState = patchedUpdateState(
  window.history.replaceState,
  "replaceState"
);
function patchedUpdateState(updateState, methodName) {
  return function () {
    const urlBefore = window.location.href;
    const result = updateState.apply(this, arguments);
    const urlAfter = window.location.href;

    if (!urlRerouteOnly || urlBefore !== urlAfter) {
      if (isStarted()) {
        // fire an artificial popstate event once single-spa is started,
        // so that single-spa applications know about routing that
        // occurs in a different application
        window.dispatchEvent(
          createPopStateEvent(window.history.state, methodName)
        );
      } else {
        // do not fire an artificial popstate event before single-spa is started,
        // since no single-spa applications need to know about routing events
        // outside of their own router.
        reroute([]);
      }
    }

    return result;
  };
}

window.addEventListener("hashchange", urlReroute);
window.addEventListener("popstate", urlReroute);

上面这段代码,是single-spa的一段全局代码,可以看到,我们对window.history.pushStatewindow.history.replaceState进行了重写, 重写的逻辑是:如果urlRerouteOnlyfalse,或者urlBeforeurlAfter不相等,则触发popstate事件,然后执行reroute函数,这里我们的urlRerouteOnlytrue,但是before和after不同,所以会触发事件,我们看下createPopStateEvent函数,源码如下:

ts 复制代码
function createPopStateEvent(state, originalMethodName) {
  let evt;
  try {
    evt = new PopStateEvent("popstate", { state });
  } catch (err) {
    // IE 11 compatibility https://github.com/single-spa/single-spa/issues/299
    // https://docs.microsoft.com/en-us/openspecs/ie_standards/ms-html5e/bd560f47-b349-4d2c-baa8-f1560fb489dd
    evt = document.createEvent("PopStateEvent");
    evt.initPopStateEvent("popstate", false, false, state);
  }
  evt.singleSpa = true;
  evt.singleSpaTrigger = originalMethodName;
  return evt;
}

这里我们可以看到,我们触发的是popstate事件,然后执行urlReroute函数,源码如下:

ts 复制代码
function urlReroute() {
  reroute([], arguments);
}

可以发现,最后还是调用了reroute函数,这里的argumentspopstate事件的参数,reroute最后会调用performAppChanges函数,那么当再次调用这个函数的时候, 我们的appsToLoad就不为空了,我们回过头看下performAppChanges函数,其中的逻辑我们在分析single-spa的时候已经分析过了,这里我们就看一下关键逻辑:

ts 复制代码
const loadThenMountPromises = appsToLoad.map((app) => {
        return toLoadPromise(app).then((app) =>
          tryToBootstrapAndMount(app, unmountAllPromise)
        );
      });

这个函数的作用是加载并挂载应用,加载和挂载这里的大致逻辑其实我们在分析single-spa的时候已经分析过了,但它是如何和qiankun结合起来的呢?子应用的钩子什么时候调用的?沙箱如何实现的?我们下一篇文章详细分析加载和挂载的过程。

这里再放上一张流程图加深下理解:

4. 总结

最后我们总结下start的流程,大白话解释就是:qiankun借助single-spa的能力加载我们的子应用,说的晦涩一点就是:

  1. qiankun:配置预加载策略与兼容性处理
  2. single-spa:调用single-spa的start方法
  3. single-spa:调用reroute方法,最后调用finishUpAndReturn方法,在这里会触发single-spa:no-app-change事件,
  4. qiankun:调用single-spa:no-app-change事件的回调函数setDefaultMountApp
  5. qiankun:调用setDefaultMountApp函数中的navigateToUrl函数,调用window.history.pushState,修改浏览器历史记录
  6. single-spa:触发popstate事件,调用urlReroute函数,最后调用performAppChanges函数,加载并挂载应用
相关推荐
aPurpleBerry7 分钟前
JS常用数组方法 reduce filter find forEach
javascript
GIS程序媛—椰子35 分钟前
【Vue 全家桶】7、Vue UI组件库(更新中)
前端·vue.js
DogEgg_00142 分钟前
前端八股文(一)HTML 持续更新中。。。
前端·html
ZL不懂前端1 小时前
Content Security Policy (CSP)
前端·javascript·面试
乐闻x1 小时前
ESLint 使用教程(一):从零配置 ESLint
javascript·eslint
木舟10091 小时前
ffmpeg重复回听音频流,时长叠加问题
前端
王大锤43911 小时前
golang通用后台管理系统07(后台与若依前端对接)
开发语言·前端·golang
我血条子呢1 小时前
[Vue]防止路由重复跳转
前端·javascript·vue.js
黎金安1 小时前
前端第二次作业
前端·css·css3
啦啦右一1 小时前
前端 | MYTED单篇TED词汇学习功能优化
前端·学习