相关链接
本文是最近分析single-spa中的一篇,全部的文章如下:
- 深入分析single-spa——导航事件与reroute
- 深入分析single-spa——启动与应用管理
- 深入分析single-spa——事件机制
- 其他关于模块机制、生命周期、微前端类型的深入分析正在进行中
打开Github上的single-spa,它的About
里面就有这么一句:
The router for easy microfrontends.
sing-spa可以说是一款路由驱动式的微前端框架,那我们就先从Router——也就是我们常说的路由看起,再逐渐去分析single-spa内部的路由机制。
前置知识——关于前端路由
我们无论是使用React
、Vue
或者Angular
等开发spa应用,必然离不开Router. 在浏览器环境中,常见的Router分为两类:
- Browser Router
- Hash Router
Browser Router
在HTML5中,DOM的window
对象通过history
提供了对于浏览器会话历史的访问,允许在用户的浏览历史中进行向前和向后跳转。我们可以:
// 在history中向后跳转
window.history.back()
// 在history中向前跳转
window.history.forward()
// 或者使用go来载入会话历史中的某一个特定界面
window.history.go(-1) // 等同于back
window.history.go(1) // 等同于forward
此外,history API提供了pushState
/replaceState
/popState
事件,用来添加和修改历史记录中的条目。<BrowserRouter>
,就是使用了这些事件来保持UI和URL的一致性。
window.onpopstate = function(event) {
// ...
}
history.pushState({page: 1}, "title 1", "?page=1")
history.pushState({page: 2}, "title 2", "?page=2")
history.replaceState({page: 3}, "title 3", "?page=3")
Hash Router
Hash Router主要是通过监听hashchange
事件,根据location.hash
的变化来保持UI和URL的一致性。它也是我们经常会遇到的一种Router,具有很好的浏览器兼容性.
window.onhashchange = function (event) {
// ...
}
window.addEventListener('hashchange', function (event) {
// ...
})
关于这几种类型的事件,更多的信息可以参考History API和Window: hashchange event.
Single-spa中的路由机制
导航事件(navigation-events)
single-spa实现应用级别的路由导航,同时提供了对于Browser Router
和Hash Router
的支持。主要实现如下:
- 监听
hashchange
和popstate
事件,实现reroute - 重写
window.addEventListener
和window.removeEventListener
,实现对于自定义事件的劫持处理 - 对
pushState
和replaceState
事件进行自定义处理
基本流程
对于hashchange
和popstate
的监听处理
const capturedEventListeners = {
hashchange: [],
popstate: [],
};
// 需要监听的路由事件,也就是hashchange和popstate
export const routingEventsListeningTo = ["hashchange", "popstate"];
// 对于事件监听调用的实现
export function callCapturedEventListeners(eventArguments) {
if (eventArguments) {
const eventType = eventArguments[0].type;
if (routingEventsListeningTo.indexOf(eventType) >= 0) {
capturedEventListeners[eventType].forEach((listener) => {
try {
listener.apply(this, eventArguments);
} catch (e) {
setTimeout(() => {
throw e;
});
}
});
}
}
}
if (isInBrowser) {
// 注册对于hashchange和popstate事件的监听
window.addEventListener("hashchange", urlReroute);
window.addEventListener("popstate", urlReroute);
// ...
}
重写window.addEventListener
和window.removeEventListener
在注册对于hashchange和popstate事件的监听之后,又重写了addEventListener和removeEventListener这两个办法,具体实现如下:
if (isInBrowser) {
// ...
// 重写addEventListener和removeEventListener
const originalAddEventListener = window.addEventListener;
const originalRemoveEventListener = window.removeEventListener;
window.addEventListener = function (eventName, fn) {
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);
};
// ...
}
// urlReroute其实就是调用reroute来实现对应的路由导航操作
function urlReroute() {
reroute([], arguments);
}
对于pushState
和replaceState
的自定义处理
通过源码可以看到,single-spa通过实现了一个patchUpdateState
的方法,来对window.history上的pushState
和replaceState
事件添加一些自定义的逻辑:
if (isInBrowser) {
// ...
// 对pushState和replaceState事件,通过patchedUpdateState来添加一些自定义的逻辑
window.history.pushState = patchedUpdateState(
window.history.pushState,
"pushState"
);
window.history.replaceState = patchedUpdateState(
window.history.replaceState,
"replaceState"
);
// ...
}
在patchUpdateState
中,主要做了以下处理:
- 判断urlRerouteOnly或者url是否发生了变化
- 判断single-spa是否已经启动
- 如果single-spa已经启动,则会派发一个对应的事件
- 如果未启动,则会触发
reroute
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()) {
// 派发对应的事件
window.dispatchEvent(
createPopStateEvent(window.history.state, methodName)
);
} else {
// 调用reroute
reroute([]);
}
}
return result;
};
}
// cratePopStateEvent主要用来创建一个PopStateEvent,并添加singleSpa和singleSpaTrigger标识
function createPopStateEvent(state, originalMethodName) {
let evt;
try {
evt = new PopStateEvent("popstate", { state });
} catch (err) {
evt = document.createEvent("PopStateEvent");
evt.initPopStateEvent("popstate", false, false, state);
}
evt.singleSpa = true;
evt.singleSpaTrigger = originalMethodName;
return evt;
}
reroute
之前在navigation-events
的介绍里面,我们可以发现:
- 有声明过
callCapturedEventListeners
,但是并没有调用;真实的调用是在reroute.js
中触发的 - 在
patchUpdateState
的实现中,在single-spa没有启动的情况下,会触发reroute
- 对于
hashchange
和popstate
的监听,注册了一个urlReroute
的方法,这里也会触发reroute
那我们来看看reroute吧。
关于reroute
reroute 是 single-spa 的核心方法。该方法更新微应用的状态,触发微前端应用的生命周期函数,并发出一系列自定义事件。
触发时机
- 手动调用:在进行微前端应用注册和调用start方法的时候,触发reroute执行
- 自动触发:在navigation-events中监听路由事件发生变化,自动触发reroute执行
基本流程
- 判断appChangeUnderway,如果其为true,则存储reroute开始执行后的路由变化,待本次reroute执行后再进行处理
- 通过getAppChanges获取apps中各个应用的状态,并进行分类
- 判断single-spa是否已经启动,如果已经启动,则调用performAppChanges;否则调用loadApps
export function reroute(pendingPromises = [], eventArguments) {
/* 通过变量appChangeUnderway用来判断当前是否正在执行,其初始值为false;
* 如果其值为true,则通过peopleWaitingOnAppChange存放reroute开始执行后的路由变化;并返回一个Promise,待本次reroute执行完成后再进行处理
*/
if (appChangeUnderway) {
return new Promise((resolve, reject) => {
peopleWaitingOnAppChange.push({
resolve,
reject,
eventArguments,
});
});
}
// 通过getAppChanges获取微前端应用状态,并分为4类
const {
appsToUnload,
appsToUnmount,
appsToLoad,
appsToMount,
} = getAppChanges();
let appsThatChanged,
navigationIsCanceled = false,
oldUrl = currentUrl,
newUrl = (currentUrl = window.location.href);
// 判断single-spa是否已经启动
if (isStarted()) {
// appChangeUnderway 改为true,获取appThatChanged列表,并执行performAppChanges
appChangeUnderway = true;
appsThatChanged = appsToUnload.concat(
appsToLoad,
appsToUnmount,
appsToMount
);
return performAppChanges();
} else {
// 如果single-spa没有启动,则执行loadApps
appsThatChanged = appsToLoad;
return loadApps();
}
}
获取需要改变的微前端应用——getAppChanges
在之前的代码里面,我们可以看到在reroute
中调用了getAppChanges
方法来获取状态有变化的微前端应用。首先我们可以看下关于应用状态的定义:
// App statuses, 定义在app.helpers.js中,这里针对在getAppChanges中使用到的状态做了一些解释
// 初始状态,代表微前端应用的资源未加载
export const NOT_LOADED = "NOT_LOADED";
// 代表正在加载微前端应用的源代码
export const LOADING_SOURCE_CODE = "LOADING_SOURCE_CODE";
// NOT_LOADED的下一个状态,表示未初始化
export const NOT_BOOTSTRAPPED = "NOT_BOOTSTRAPPED";
export const BOOTSTRAPPING = "BOOTSTRAPPING";
// NOT_BOOTSTRAPPED的下一个状态,表示微前端应用相关代码未执行/未加载到界面上
export const NOT_MOUNTED = "NOT_MOUNTED";
export const MOUNTING = "MOUNTING";
// 表示微前端应用代码已经执行/已经加载到界面上
export const MOUNTED = "MOUNTED";
export const UPDATING = "UPDATING";
export const UNMOUNTING = "UNMOUNTING";
export const UNLOADING = "UNLOADING";
export const LOAD_ERROR = "LOAD_ERROR";
export const SKIP_BECAUSE_BROKEN = "SKIP_BECAUSE_BROKEN";
在getAppChanges中主要定义了4类应用,并进行了相应的分类:
- appsToUnload: 针对处于
NOT_BOOTSTRAPPED
和NOT_MOUNTED
状态,而且和当前url不匹配的应用 - appsToUnmount:针对处于
MOUNTED
状态,而且和当前url不匹配的应用 - appsToLoad:针对处于
NOT_LOADED
和LOADING_SOURCE_CODE
转台,而且和当前url匹配的应用 - appsToMount:与appsToUnload相对,针对处于
NOT_BOOTSTRAPPED
和NOT_MOUNTED
状态,而且和当前url匹配的应用
具体源码如下:
export function getAppChanges() {
const appsToUnload = [],
appsToUnmount = [],
appsToLoad = [],
appsToMount = [];
// We re-attempt to download applications in LOAD_ERROR after a timeout of 200 milliseconds
const currentTime = new Date().getTime();
apps.forEach((app) => {
const appShouldBeActive =
app.status !== SKIP_BECAUSE_BROKEN && shouldBeActive(app);
switch (app.status) {
case LOAD_ERROR:
if (appShouldBeActive && currentTime - app.loadErrorTime >= 200) {
appsToLoad.push(app);
}
break;
case NOT_LOADED:
case LOADING_SOURCE_CODE:
if (appShouldBeActive) {
appsToLoad.push(app);
}
break;
case NOT_BOOTSTRAPPED:
case NOT_MOUNTED:
if (!appShouldBeActive && getAppUnloadInfo(toName(app))) {
appsToUnload.push(app);
} else if (appShouldBeActive) {
appsToMount.push(app);
}
break;
case MOUNTED:
if (!appShouldBeActive) {
appsToUnmount.push(app);
}
break;
// all other statuses are ignored
}
});
return { appsToUnload, appsToUnmount, appsToLoad, appsToMount };
}
在获取到状态有变更的微前端应用之后,就需要去执行具体的操作了。
执行微前端应用的变化——performAppChanges
执行变化,主要是通过CustomEvent
进行自定义事件的派发,以及进行后续的处理:
- 根据appsThatChanged的数量,来派发
single-spa:before-no-app-change
或者single-spa:before-app-change
事件 - 派发
single-spa:before-routing-event
事件 - 如果导航已取消
- 派发
single-spa:before-mount-routing-event
事件 - 调用finishUpAndReturn
- 调用naviagteToUrl, 返回之前的url
- 派发
- 对各种状态的微前端应用进行处理
- 最终调用finishUpAndReturn
function performAppChanges() {
return Promise.resolve().then(() => {
// 根据appsThatChanged的数量,来派发自定义事件
window.dispatchEvent(
new CustomEvent(
appsThatChanged.length === 0
? "single-spa:before-no-app-change"
: "single-spa:before-app-change",
getCustomEventDetail(true)
)
);
// 派发single-spa:before-routing-event事件
window.dispatchEvent(
new CustomEvent(
"single-spa:before-routing-event",
getCustomEventDetail(true, { cancelNavigation })
)
);
// 针对导航取消的处理
if (navigationIsCanceled) {
window.dispatchEvent(
new CustomEvent(
"single-spa:before-mount-routing-event",
getCustomEventDetail(true)
)
);
finishUpAndReturn();
navigateToUrl(oldUrl);
return;
}
// 对各种状态的应用进行处理
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)
)
);
});
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(() => {
callAllEventListeners();
return Promise.all(loadThenMountPromises.concat(mountPromises))
.catch((err) => {
pendingPromises.forEach((promise) => promise.reject(err));
throw err;
})
.then(finishUpAndReturn); // 最终调用finishUpAndReturn
});
});
}
关于不同状态的微前端应用的处理,以及自定义事件,稍后会单独写一篇来分析,这里就先不展开了。
最后的处理——finishUpAndReturn
之前在reroute的流程分析中,还有两点:
- 记录了appChangeUnderway,初始值为false,在判断isStarted() === true后,将其设置为true
- 根据appChangeUnderway === true,将本次reroute后的路由变化记录到peopleWaitingOnAppChange中,等待后续处理
而finishUpAndReturn作为reroute的结束代码,并发出一些自定义的结束事件,对appChangeUnderway重新赋值,并处理之前记录在peopleWaitingOnAppChange中的路由事件,其返回值为mounted apps. 源码如下:
function finishUpAndReturn() {
// 获取mounted apps
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) {
setTimeout(() => {
throw err;
});
}
// 重置appChangeUnderway的值为false,以便在后续调用reroute
appChangeUnderway = false;
// 对之前记录在peopleWaitingOnAppChange中的记录,调用reroute进行处理
if (peopleWaitingOnAppChange.length > 0) {
const nextPendingPromises = peopleWaitingOnAppChange;
peopleWaitingOnAppChange = [];
reroute(nextPendingPromises);
}
// 返回之前获取到的mounted apps
return returnValue;
}