single-spa
在 MPA 模式下可以通过浏览器来实现应用状态和周期事件的处理,但是在 SPA 模式下默认无法做到技术无关的应用切换。大部分的 Web 框架(例如 React、Vue 和 Svelte 等)提供了根据路由来切换框架组件的能力,并没有提供根据路由来切换技术无关的应用的能力。为此,single-spa 提供了根据路由来切换应用的能力,并且在内部实现了应用的状态转换和周期事件,从而实现了类似于 MPA 模式下浏览器的应用切换能力。

single-spa 原理解析
整个 single-spa 源码可以被分为几个主要部分,如下所示:
applications:注册微应用并解析微应用的注册参数start:启动微应用的生命周期函数执行navigation:处理导航事件、根据导航变化计算和执行微应用的变化lifecycles:异步执行微应用的生命周期函数以及相应的错误处理
applications
reroute
该函数主要是在微应用需要发生变化时触发,它会通过 getAppChanges 判断需要变化的微应用列表,然后根据外部是否调用了 start 函数来判断执行微应用的批量加载 loadApps 还是执行所有微应用的变化
export function reroute(pendingPromises = [], eventArguments) {
// eslint-disable-next-line no-console
console.log("[navigation/reroute.js - reroute]: reroute 函数开始执行...");
// 如果当前正在执行 performAppChanges 处理应用变化,
// 则将 eventArguments 存储到 peopleWaitingOnAppChange 数组中
// 如果 performAppChanges 函数还未执行完毕,
// 但是再次调用了 reroute 函数,
// 那么会等待 performAppChanges 函数执行完毕
// 在 performAppChanges 函数执行完毕后,
// 会调用 finishUpAndReturn 函数,
// 如果 peopleWaitingOnAppChange 数组中有数据,
// 则会再次执行 reroute 函数
// 因此这里主要用于延迟执行 reroute 函数
if (appChangeUnderway) {
return new Promise((resolve, reject) => {
// 将 resolve、reject、eventArguments 存储到 peopleWaitingOnAppChange 数组中
// 当 performAppChanges 函数执行完毕后,
// 会调用 finishUpAndReturn 函数,
// 如果 peopleWaitingOnAppChange 数组中有数据,
// 则会再次执行 reroute 函数
peopleWaitingOnAppChange.push({
resolve,
reject,
eventArguments,
});
});
}
// 获取当前应用的变化情况
// 1. appsToUnload: 需要彻底卸载的应用
// 2. appsToUnmount: 需要卸载的应用
// 3. appsToLoad: 需要加载的应用
// 4. appsToMount: 需要挂载的应用
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
);
console.log(
"[navigation/reroute.js - reroute]: appsToUnload 数据: ",
appsToUnload
);
console.log(
"[navigation/reroute.js - reroute]: appsToUnmount 数据: ",
appsToUnmount
);
console.log(
"[navigation/reroute.js - reroute]: appsToLoad 数据: ",
appsToLoad
);
console.log(
"[navigation/reroute.js - reroute]: appsToMount 数据: ",
appsToMount
);
console.log(
"[navigation/reroute.js - reroute]: 准备执行 performAppChanges..."
);
return performAppChanges();
// 如果还没有启动
// 调用 singleSpa.registerApplication() 方法后,
// 还没有调用 singleSpa.start() 方法之前会走到这里
} else {
appsThatChanged = appsToLoad;
console.log("[navigation/reroute.js - reroute]: 准备执行 loadApps...");
return loadApps();
}lifecycles
toLoadPromise 解析
toLoadPromise 本质上不是微应用提供的生命周期函数,而是注册加载 API 时需要提供的激活加载函数,它可以在 single-spa 调用 start 函数后加载,也可以在初始化微应用的路由被激活时触发加载:
export function toLoadPromise(app) {
console.log(
"[lifecycles/load.js - toLoadPromise]: toLoadPromise 函数开始执行…",
app.name,
app.status
);
// 开启微任务,异步执行微应用的加载函数
return Promise.resolve().then(() => {
console.log(
"[lifecycles/load.js - toLoadPromise]: toLoadPromise Promise.resolve 开始执行, 开始检测 app.loadPromise 是否已经执行过。",
app.name,
app.status
);
// 如果 app.loadPromise 存在,
// 直接返回 app.loadPromise
// 这里可以确保同一个 app 只会执行一次 loadApp 方法
// 例如注册微应用时会调用 loadApps 方法,
// 会执行微应用的 toLoadPromise,
// 此时会缓存 app.loadPromise
// 而启动 start 函数最终调用 performAppChanges 时还会执行微应用的 toLoadPromise
// 为了避免重复执行 app.loadPromise 方法,
// 这里会直接返回 app.loadPromise(Promise 对象)
if (app.loadPromise) {
console.log(
"[lifecycles/load.js - toLoadPromise]: 已经执行过 app.loadPromise,直接返回对应的 Promise 结果。",
app.name,
app.status
);
return app.loadPromise;
}
// 如果 app.status 不是 NOT_LOADED 和 LOAD_ERROR,直接返回 app
if (app.status !== NOT_LOADED && app.status !== LOAD_ERROR) {
return app;
}
// 将 app.status 设置为 LOADING_SOURCE_CODE
app.status = LOADING_SOURCE_CODE;
let appOpts, isUserErr;
console.log(
"[lifecycles/load.js - toLoadPromise]: app.loadPromise 开始执行…",
app.name,
app.status
);
// 使用 app.loadPromise 缓存 app 的加载,
// 避免在 loadApps 以及 performAppChanges 时重复加载
return (app.loadPromise = Promise.resolve()
.then(() => {
console.log(
"[lifecycles/load.js - toLoadPromise]: 在 Promise.resolve 中开始执行 app.loadPromise…",
app.name,
app.status
);
console.log(
"[lifecycles/load.js - toLoadPromise]: 准备执行 app.loadApp(registerApplication 的第二个参数 app)…",
app.name,
app.status
);
// 这里的 loadApp 其实就是 registerApplication 的第二个参数
// 在主应用中使用 window.fetch 获取子应用的资源,
// 执行后需要返回 Promise,
// 并且返回的是子应用的生命周期函数对象
const loadPromise = app.loadApp(getProps(app));
// 如果 loadPromise 不是 Promise 对象,抛出异常
if (!smellsLikeAPromise(loadPromise)) {
// The name of the app will be prepended to this error message inside of the handleAppError function
// app.loadApp 返回的不是 Promise 对象,抛出异常
isUserErr = true;
throw Error(
formatErrorMessage(
33,
__DEV__ &&
`single-spa loading function did not return a promise. Check the second argument to registerApplication('${toName(
app
)}', loadingFunction, activityFunction)`,
toName(app)
)
);
}
// 这里的 val 其实就是 loadApp 的 Promise 返回值
// 也就是 registerApplication 的第二个参数 app 的返回值
// 例如:() => import("react-micro-app"),
// 返回的是一个 Promise
// 例如:() => Promise.resolve({ bootstrap: async () => {}, mount, unmount }),
// 返回的是一个 Promise
// 所以 val 就是各个子应用的生命周期函数组成的对象,
// 例如:{ bootstrap: async () => {}, mount, unmount }
return loadPromise.then((val) => {
console.log(
"[lifecycles/load.js - toLoadPromise]: app.loadApp 执行完成,开始检测子应用的生命周期函数是否符合要求…",
app.name,
app.status
);
app.loadErrorTime = null;
appOpts = val;
let validationErrMessage, validationErrCode;
if (typeof appOpts !== "object") {
validationErrCode = 34;
if (__DEV__) {
validationErrMessage = `does not export anything`;
}
}
// 判断 appOpts 的 bootstrap、mount、unmount 是否符合要求
if (
// ES Modules don't have the Object prototype
// ES 模块没有 Object 原型
Object.prototype.hasOwnProperty.call(appOpts, "bootstrap") &&
!validLifecycleFn(appOpts.bootstrap)
) {
validationErrCode = 35;
if (__DEV__) {
validationErrMessage = `does not export a valid bootstrap function or array of functions`;
}
}
if (!validLifecycleFn(appOpts.mount)) {
validationErrCode = 36;
if (__DEV__) {
validationErrMessage = `does not export a mount function or array of functions`;
}
}
if (!validLifecycleFn(appOpts.unmount)) {
validationErrCode = 37;
if (__DEV__) {
validationErrMessage = `does not export a unmount function or array of functions`;
}
}
const type = objectType(appOpts);
if (validationErrCode) {
let appOptsStr;
try {
appOptsStr = JSON.stringify(appOpts);
} catch {}
console.error(
formatErrorMessage(
validationErrCode,
__DEV__ &&
`The loading function for single-spa ${type} '${toName(
app
)}' resolved with the following, which does not have bootstrap, mount, and unmount functions`,
type,
toName(app),
appOptsStr
),
appOpts
);
handleAppError(validationErrMessage, app, SKIP_BECAUSE_BROKEN);
return app;
}
if (appOpts.devtools && appOpts.devtools.overlays) {
app.devtools.overlays = assign(
{},
app.devtools.overlays,
appOpts.devtools.overlays
);
}
// 设置 app 的状态为 NOT_BOOTSTRAPPED
app.status = NOT_BOOTSTRAPPED;
// 将 appOpts 中的周期函数扁平化
app.bootstrap = flattenFnArray(appOpts, "bootstrap");
app.mount = flattenFnArray(appOpts, "mount");
app.unmount = flattenFnArray(appOpts, "unmount");
app.unload = flattenFnArray(appOpts, "unload");
app.timeouts = ensureValidAppTimeouts(appOpts.timeouts);
// 删除 app.loadPromise,表明 app.loadPromise 已经执行完成
// 下一次执行 toLoadPromise 时会重新执行 app.loadPromise
delete app.loadPromise;
console.log(
"[lifecycles/load.js - toLoadPromise]: app.loadApp 返回的周期函数解析完成,所有周期函数符合要求。",
app.name,
app.status
);
return app;
});
})
.catch((err) => {
delete app.loadPromise;
let newStatus;
if (isUserErr) {
// 经常会在微前端中出现这种情况,执行 app.loadApp 时返回的不是 Promise 对象
// 这里会将 app 的状态设置为 SKIP_BECAUSE_BROKEN
newStatus = SKIP_BECAUSE_BROKEN;
} else {
// 如果 app.loadApp 执行失败,将 app 的状态设置为 LOAD_ERROR
newStatus = LOAD_ERROR;
app.loadErrorTime = new Date().getTime();
}
handleAppError(err, app, newStatus);
return app;
}));
});
}single-spa 的所有生命周期函数执行都是在微任务中执行,这可以确保主应用框架路由的代码先执行完毕,然后处理微应用的加载和卸载。
整体流程

在主应用中使用 registerApplication 注册微应用的执行流程如下所示,需要注意每注册一个微应用以下流程都会执行一遍:

微应用注册完成后需要调用 single-spa 的 start 函数启动,它的执行流程如下所示:

从上述流程可以发现,通过 registerApplication 注册微应用时只会执行微应用的加载逻辑(执行注册参数 app),但是调用 start 函数启动后,single-spa 会调用 performAppChanges 批量处理所有需要变化的微应用,除了会对激活的微应用批量进行异步加载,还会批量异步执行微应用的生命周期函数,包括 bootstrap、mount、unmount 以及 unload。