「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!」
前言
就目前来讲,谈及微前端不可避免的都会涉及到 single-spa 这个库,对于他在微前端中扮演的角色、所做的工作很多人却还是一知半解,本文想带你解开它的神秘面纱
阅读本文需要对 single-spa的基本使用有所了解,还没用过的同学可以阅读我之前的文章()[]
带着问题阅读
如果用一句话总结 single-spa 的作用,我认为应该是:
single-spa 为微前端提供了应用的生命周期管理
我一般都是带着问题去阅读源码
下面将围绕下面两个问题,对single-spa 进行解刨
这也是single-spa主要做的两件事情
- 1,路由是怎么处理?
- 2,应用的生命周期是怎么管理的?
路由是怎么处理?
在我们使用 registerApplication 注册应用时,第三个参数可以用于做子应用的路由匹配,自定义路由匹配规则,当路由发生变更的时候,singleSpa 会调用这个方法,根据window.location的变化对应用进行 mount 和 unmount
这里就会有两个问题
1, 在我们主动调用 history.pustState 或者 history.replaceState 中是不会触发路由变化的事件 popstate 和 hashchange的,那么 singleSpa 是怎么监听路由变化的?
2,我们知道popstate 和 hashchange事件是可以被多个监听者监听的,但是在微前端的场景中,为了保证应用能正常加载避免和其他监听者的冲突, single-spa 的执行权应该是最高的,那么 single-spa 是怎么拿到第一个执行权的呢?
如何监听路由变化,确保第一执行权
前面说到,在我们主动调用 history.pustState 或者 history.replaceState 中是不会触发路由变化的事件 popstate 和 hashchange的
single-spa 的解决方案就是
1,对原生的
pushState和replaceState进行拦截,当他被调用的时候,主动调用window.dispatchEvent去触发事件 2,拦截window.addEventListener和window.removeEventListener的pushState和replaceState事件,自定义其触发时机
我们可以在源码中的src/navigation/navigation-events.js看到这个逻辑
if (isInBrowser){
// 省略代码
// 对原生方法进行拦截
window.history.pushState = patchedUpdateState(
window.history.pushState,
"pushState"
);
window.history.replaceState = patchedUpdateState(
window.history.replaceState,
"replaceState"
);
}
// 对原生的 window.history.pushState 和 window.history.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()) {
// 手动发送一个 hashchange 、popstate 事件
window.dispatchEvent(
createPopStateEvent(window.history.state, methodName)
);
} else {
reroute([]);
}
}
return result;
};
}
window.addEventListener = function (eventName, fn) {
// 拦截 ["hashchange", "popstate"] 事件
if (typeof fn === "function") {
if (
routingEventsListeningTo.indexOf(eventName) >= 0 &&
!find(capturedEventListeners[eventName], (listener) => listener === fn)
) {
capturedEventListeners[eventName].push(fn);
return;
}
}
return originalAddEventListener.apply(this, arguments);
};
window.removeEventListener = function (eventName, listenerFn) {
if (typeof listenerFn === "function") {
if (routingEventsListeningTo.indexOf(eventName) >= 0) {
capturedEventListeners[eventName] = capturedEventListeners[
eventName
].filter((fn) => fn !== listenerFn);
return;
}
}
return originalRemoveEventListener.apply(this, arguments);
};
这样就确保了能触发路由变化事件,且 single-spa 是第一个处理路由的
在路由变化的时候, single-spa 会调用 rerouter 方法,这个很重要,应用的生命周期就在这个方法进行执行,我们这里只需先记住在 hashchange 和 popstate 事件中会触发 reroute方法即可
function urlReroute() {
reroute([], arguments);
}
window.addEventListener("hashchange", urlReroute);
window.addEventListener("popstate", urlReroute);
应用的生命周期是怎么管理的?
registerApplication
我们先来看看registerApplication 具体做了啥,
很简单,就是把参数包装一下,然后创建一个状态为 NOT_LOADED 的应用,最后调用
reroute方法,除了之前说的 popstate 和 hashchange , 又多了一个调用 reroute的地方
export function registerApplication(
appNameOrConfig,
appOrLoadApp,
activeWhen
customProps
) {
// 对传入的参数进行验证、包装处理
const registration = sanitizeArguments(
appNameOrConfig,
appOrLoadApp,
activeWhen,
customProps
);
// 省略代码
// 注册应用,应用状态为 NOT_LOADED
apps.push(
assign(
{
loadErrorTime: null,
status: NOT_LOADED,
parcels: {},
devtools: {
overlays: {
options: {},
selectors: [],
},
},
},
registration
)
);
// 调用 reroute
if (isInBrowser) {
ensureJQuerySupport();
reroute();
}
}
除了 registerApplication ,我们在基座中必不可少的就是调用
这个方法也很简单,将 started设置为 true ,然后调用的reroute 方法
start方法start方法,
export function start(opts) {
started = true;
if (opts && opts.urlRerouteOnly) {
setUrlRerouteOnly(opts.urlRerouteOnly);
}
if (isInBrowser) {
reroute();
}
}
到目前为止,触发reroute一共有三种方式
- start 方法
- registerApplication 方法
- hashchange、popstate 事件
除此之外 single-spa 还提供了一个主动触发的方法triggerAppChange
可见 reroute 多么重要
在这个方法中,管理了整个应用的生命周期,应用状态的流转
应用的状态
为了方便后续了解,我将应用的状态总结如下
| 应用状态 | 描述 | |
|---|---|---|
| NOT_LOADED | 应用还没加载,默认状态 | |
| LOADING_SOURCE_CODE | 加载中,调用了注册应用时的第二个参数 | |
| NOT_BOOTSTRAPPED | 加载完成,但是还没用调用应用的 bootstrap 函数 | |
| BOOTSTRAPPING | 正在调用应用的 bootstrap 函数 | |
| NOT_MOUNTED | 调用 bootstrap 成功,还没调用 mount | |
| MOUNTED | 应用的 mount 生命周期函数执行成功,应用已成功挂载 | |
| UNLOADING | 卸载没有进行过 mount 的应用,调用 unload 方法 | |
| UNMOUNTING | 调用应用 unmount 生命周期方法,调用成功后,状态变为 NOT_MOUNTED | |
| SKIP_BECAUSE_BROKEN | 应用变更状态失败了,不会再进行下个状态的变更 | |
| LOAD_ERROR | 应用加载失败,在下次进行reroute 时,如果超过 200 毫秒,会重新加载 | 状态失败 |
reroute 执行流程
下面我们分析下 reroute的执行过程
export function reroute(pendingPromises = [], eventArguments) {
/**
* 如果有应用正在处于状态变更的状态,将此次触发暂存起来。
* performAppChanges 执行完后,这个变量为 false
*/
if (appChangeUnderway) {
return new Promise((resolve, reject) => {
peopleWaitingOnAppChange.push({
resolve,
reject,
eventArguments,
});
});
}
// appsToUnload: 当前状态为 NOT_BOOTSTRAPPED 或 NOT_MOUNTED, 且当前路由不匹配,就是应用还没 mount ,路由就已经发生了改变
// appsToUnmount: 当前状态为 MOUNTED ,且路由不匹配
// appsToLoad: 当前路由匹配,状态为 NOT_LOADED、 LOADING_SOURCE_CODE 、 LOAD_ERROR。如果是 LOAD_ERROR,需要满足距离上一次加载失败时间超过200毫秒
// appsToMount: 当前状态为 NOT_BOOTSTRAPPED 或 NOT_MOUNTED,且当前路由匹配
const {
appsToUnload,
appsToUnmount,
appsToLoad,
appsToMount,
} = getAppChanges();
let appsThatChanged,
navigationIsCanceled = false,
oldUrl = currentUrl,
newUrl = (currentUrl = window.location.href);
const stared = isStarted();
if (stared) {
appChangeUnderway = true;
appsThatChanged = appsToUnload.concat(
appsToLoad,
appsToUnmount,
appsToMount
);
return performAppChanges();
} else {
appsThatChanged = appsToLoad;
// 还没启动,去加载应用,这里不会执行挂载,只到 NOT_BOOTSTRAPPED 状态
return loadApps();
}
// 省略代码
可以看到,reroute 有个队列机制,同一时间只会执行一次应用状态的变更,应用状态变量的具体逻辑在
performAppChanges 中
看看performAppChanges 方法做了啥
```js
function performAppChanges() {
return Promise.resolve().then(() => {
// https://github.com/single-spa/single-spa/issues/545
// 触发一些自定义事件,省略代码...、
// 两种需要卸载的, 执行一些清理动作
// 1, 加载完资源,还没有进行 mount, 会执行 unload 生命周期函数
// 2, 已经 mounted,会执行 Unmount 生命周期函数
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);
/**
* 执行 bootstrap 和 mount
*/
const loadThenMountPromises = appsToLoad.map((app) => {
return toLoadPromise(app).then((app) =>
tryToBootstrapAndMount(app, unmountAllPromise)
);
});
const mountPromises = appsToMount
.filter((appToMount) => appsToLoad.indexOf(appToMount) < 0)
.map((appToMount) => {
return tryToBootstrapAndMount(appToMount, unmountAllPromise);
});
return unmountAllPromise
.catch((err) => {
callAllEventListeners();
throw err;
})
.then(() => {
// 这里会去触发使用 window.addEventListener 注册的事件,为了保证不影响 single-spa 的执行,会使用一个 try/cash 包裹
callAllEventListeners();
return Promise.all(loadThenMountPromises.concat(mountPromises))
.catch((err) => {
pendingPromises.forEach((promise) => promise.reject(err));
throw err;
})
/**
* 在 finishUpAndReturn 中,会去检查在本次状态更变过程中,有没有触发reroute
* 如果有,再次执行 reroute
*/
.then(finishUpAndReturn);
});
});
}