斗胆分析下single-spa源码

307 阅读5分钟

在很久之前,写过一篇初识single-spa得文章,那篇文章写的相对比较简单,只是大概介绍了一下single-spa的用法,在git上上传了一个使用demo。一年多过去了,是对上篇的延续,也是对这一年多使用的一个总结。下面我就斗胆给朋友们分析下single-spa的源码,让大家对single-spa有个更深入的认识。

如果对single-spa不是很熟悉,这里是上一篇 初识single-spa传送门,大家可以自行点击查看。

假设先打开基座应用,然后点击对应的路由挂载子应用。

文中说的app处于激活态的含义是:当前路由满足registerApplication函数入参activeWhen设置的条件。

下面主要从以下几个方面分析:

1. registerApplication都做了什么

2. 注册的子应用的状态是如何流转的

3. 是如何监听路由变化,变更不同子应用的状态的

4. 副作用

一、registerApplication都做了什么

1. 格式化参数

官网和源码都可以看到,registerApplication函数是支持两种调用方式的,一种是registerApplication(name, applicationOrLoadingFn,activeWhen,customProps),另一种是registerApplication({ name, app,activeWhen,customProps })。所以registerApplication函数第一步就是格式化传进来的参数,统一成一种格式。

2. 将注册的app状态初始化为NOT_LOADED,并存储到全局变量 apps数组中(会根据appName去重)。

3. 判断是否在浏览器环境,在浏览器环境中则执行reroute函数。

详细代码大家可以查看:src/applications/apps.js => registerApplication()

二、子应用状态如何流转

执行完registerApplication函数,我们会执行start函数。此时,整个子应用注册完成,但是由于目前路由没有激活任何子应用,我们现在随机点击一个需要挂载子应用的路由,此时会重新执行reroute函数。

1. 获取当前注册的app的状态,并分状态进行存储

  • appToLoad

    app处于激活状态并且app的状态为LOAD_ERROR | NOT_LOADED | LOADING_SOURCE_CODE

  • appToUnload

    app处于非激活状态并且需要去unload并且状态为NOT_BOOTSTRAPPED | NOT_MOUNTED

  • appToMount

    app处于激活态并且app状态为NOT_BOOTSTRAPPED | NOT_MOUNTED

  • appToUnmount

    app处于非激活态并且app状态为MOUNTED

由于我们是首次挂载当前路由需要的子应用,所以当前app的状态应该是在registerApplication注册时被初始化的NOT_LOADED状态,所以在appToLoad数组中会有当前需要挂载的app,其余的数组应该都是空数组。

2. 检测当前app状态,当前app状态如果为NOT_LOADED | LOAD_ERROR,则将app的状态置为LOADING_SOURCE_CODE

3. 加载挂载app所需的静态文件,并且获取子应用入口文件的生命周期钩子,成功获取将app的状态置为NOT_BOOTSTRAPPED

4. 判断当前app状态为NOT_BOOTSTRAPPED状态,将app状态置为BOOTSTRAPPING

5. 执行子应用抛出的bootstrap钩子,执行成功之后将状态置为NOT_MOUNTED

6. 以上都执行完之后(都是在异步中执行的),判断当前app是否还处于激活状态,如果处于激活状态则执行子应用抛出的mount钩子,并将状态置为MOUNTED

7. 在点击其他子应用路由,则重复上述1-6过程。

8. 对于已经挂载的子应用,当需要挂载其他子应用时,当前子应用会执行unmount钩子,然后将状态置为NOT_MOUNTED。当再需要挂载该子应用时,则重复执行步骤6

至此,子应用的主要状态大致流转完毕。这里需要注意的是,子应用只有第一次挂载的时候才会挂在子应用的静态文件,也就是步骤3,当重复激活子应用时,则不会重复挂载静态文件,只会重复执行mount钩子。

下图为子应用状态刘庄示意图:

子应用状态流转图

三、是如何监听路由变化,变更不同子应用的状态的

我们知道当点击a标签、触发浏览器的前进后退、调用histroy.back()、histroy.go()等会触发hashchange或者popstate事件。

所以,监听路由变化也一定是监听了hashchangepopstate事件。

src/navigation/navigation-events.js中果然看到了类似的代码:

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

function urlReroute() {
  reroute([], arguments);
}

当路由变化触发上述事件时,就会按照第二点子应用状态流转中说的去挂载应该挂载的子应用。但是我们也知道pushState|replaceState方法不能触发popstate事件。

single-spa是这样处理的,重写了pushStatereplaceState方法。

window.history.pushState = patchedUpdateState(
 window.history.pushState,
 "pushState"
);
window.history.replaceState = patchedUpdateState(
 window.history.replaceState,
 "replaceState"
);

function patchedUpdateState(updateState, methodName) {
  return function () {
    // 示意代码
    window.dispatchEvent(
       createPopStateEvent(window.history.state, methodName)
     );
  };
}

function createPopStateEvent(state, originalMethodName) {
  // https://github.com/single-spa/single-spa/issues/224 and https://github.com/single-spa/single-spa-angular/issues/49
  // We need a popstate event even though the browser doesn't do one by default when you call replaceState, so that
  // all the applications can reroute. We explicitly identify this extraneous event by setting singleSpa=true and
  // singleSpaTrigger=<pushState|replaceState> on the event instance.
  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事件,这样就能正常按照第二步子应用状态如何流转进行子应用的mountunmount

四、副作用

  1. 第三步中我们分析,pushStatereplaceState不能触发popstate事件,为了能正常挂载子应用,则当调用pushStatereplaceState时,手动抛出一个popstate事件。那么我们自己写的popstate事件也会再非浏览器触发前进后退情况下被调用。但是也有一点好处,就是如果我们需要在子应用中跳转其他子应用或者基座应用中的路由时,只需使用pushState | replaceState就能完成跳转。

  2. 子应用和基座或其他子应用中可能存在css样式覆盖

  3. 子应用和基座或其他子应用中可能存在js变量覆盖

关于第一个副作用,可能在vue-router在低版本的环境中,存在已经unmount子应用将vue实例全部销毁的情况下,vue-router没有解除对popstate事件监听,导致路由跳转混乱问题。大家可以点击「一个bug引发的关于vue-router原理分析」详细查看。

关于问题2、3是个比较系统的问题,会在下一篇详细讲述下目前的一些解决方案。