在很久之前,写过一篇初识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事件。
所以,监听路由变化也一定是监听了hashchange
或popstate
事件。
在src/navigation/navigation-events.js
中果然看到了类似的代码:
window.addEventListener("hashchange", urlReroute);
window.addEventListener("popstate", urlReroute);
function urlReroute() {
reroute([], arguments);
}
当路由变化触发上述事件时,就会按照第二点子应用状态流转
中说的去挂载应该挂载的子应用。但是我们也知道pushState|replaceState
方法不能
触发popstate
事件。
single-spa
是这样处理的,重写了pushState
和replaceState
方法。
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
事件,这样就能正常按照第二步子应用状态如何流转
进行子应用的mount
和unmount
。
四、副作用
-
第三步中我们分析,
pushState
和replaceState
不能触发popstate
事件,为了能正常挂载子应用,则当调用pushState
或replaceState
时,手动抛出一个popstate
事件。那么我们自己写的popstate
事件也会再非浏览器触发前进后退情况下被调用。但是也有一点好处,就是如果我们需要在子应用中跳转其他
子应用或者基座应用中的路由时,只需使用pushState | replaceState
就能完成跳转。 -
子应用和基座或其他子应用中可能存在
css样式覆盖
。 -
子应用和基座或其他子应用中可能存在
js变量覆盖
。
关于第一个副作用,可能在vue-router在低版本的环境中,存在已经unmount子应用将vue实例全部销毁的情况下,vue-router没有解除对popstate
事件监听,导致路由跳转混乱问题。大家可以点击「一个bug引发的关于vue-router原理分析」详细查看。
关于问题2、3是个比较系统的问题,会在下一篇详细讲述下目前的一些解决方案。