内容介绍
本篇文章内容较长,涵盖了single-spa框架的注册(registerApplication
),开始(start
),加载(loadApps
),切换(performAppChanges
),以及生命周期,同时也有我对源码一些细节上的问题和答案,同时在源码之间也穿插着我的解释,相信会对大家理解有所帮助。有什么问题可以在评论区交流,谢谢大家!
参考
single-spa框架api概览以及详解
整个 single-spa 源码可以被分为几个主要部分,如下所示:
-
applications
:注册微应用并解析微应用的注册参数 -
start
:启动微应用的生命周期函数执行 -
reroute
:在微应用需要发生变化时触发 -
loadApps
:未调用start时候调用reroute会调用loadApps加载微应用 -
performAppChanges
:start函数已经被调用过触发reroute时会调用performAppChanges处理微应用变化 -
navigation
:处理导航事件、根据导航变化计算和执行微应用的变化 -
lifecycles
:异步执行微应用的生命周期函数以及相应的错误处理
single-spa 全部状态
ini
// App statuses 12
export const NOT_LOADED = "NOT_LOADED";//single-spa应用注册了,还未加载。
export const LOADING_SOURCE_CODE = "LOADING_SOURCE_CODE";//应用代码正在被拉取。
export const NOT_BOOTSTRAPPED = "NOT_BOOTSTRAPPED";//应用已经加载,还未初始化。
export const BOOTSTRAPPING = "BOOTSTRAPPING";//初始化中
export const NOT_MOUNTED = "NOT_MOUNTED";//应用已经加载和初始化,还未挂载。
export const MOUNTING = "MOUNTING";//应用正在被挂载,还未结束。
export const MOUNTED = "MOUNTED";//应用目前处于激活状态,已经挂载到DOM元素上。
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";//跳过加载
registerApplication
解析
registerApplication
是 single-spa 对外提供的注册 API,它可以通过对象或者函数两种方式进行调用,在主应用中的使用如下所示:
javascript
// single-spa-config.js
// 引入 single-spa 的 NPM 库包
import { registerApplication, start } from 'single-spa';
// 函数调用方式,需要提供四个参数
registerApplication(
// 参数 1:微应用名称标识
'app2',
// 参数 2:微应用加载逻辑 / 微应用对象,必须返回 Promise
() => import('src/app2/main.js'),
// 参数 3:微应用的激活条件
(location) => location.pathname.startsWith('/app2'),
// 参数 4:传递给微应用的 props 数据
{ some: 'value' }
);
// 对象调用方式,只需要一个对象参数
// 更加清晰,易于阅读和维护,无须记住参数的顺序
registerApplication({
// name 参数
name: 'app1',
// app 参数,必须返回 Promise
app: () => import('src/app1/main.js'),
// activeWhen 参数
activeWhen: '/app1',
// customProps 参数
customProps: {
some: 'value',
}
});
registerApplication函数解析:注册应用,处理参数,触发reroute
scss
export function registerApplication(
appNameOrConfig,
appOrLoadApp,
activeWhen,
customProps
) {
// registerApplication的入参形式多样,sanitizeArguments函数是
// registerApplication的入参进行格式化,返回一个统一的格式
// 同时还会对参数的格式进行校验,不符合规范将会报错
// const registration = {
// name: null, 注册的app的name
// loadApp: null, 注册的app的加载方式
// activeWhen: null, app加载的判断条件
// customProps: null, 用户自定义参数
// };
const registration = sanitizeArguments(
appNameOrConfig,
appOrLoadApp,
activeWhen,
customProps
);
// 校验是否调用了start
if (!isStarted() && !startWarningInitialized) {
startWarningInitialized = true;
setTimeout(() => {
if (!isStarted()) {
console.warn(
formatErrorMessage(
1,
__DEV__ &&
`在加载 singleSpa 5000 毫秒后,尚未调用 singleSpa.start()。在调用 start() 之前,可以声明和加载(loaded)应用程序,但不能引导(bootstrapped)或挂载(mounted)。`
)
);
}
}, 5000);
}
//重名校验,重名了会抛出错误
if (getAppNames().indexOf(registration.name) !== -1)
throw Error(
formatErrorMessage(
21,
__DEV__ &&
`There is already an app registered with name ${registration.name}`,
registration.name
)
);
//apps是一个全局变量,用于存储注册的被处理过的app列表
apps.push(
assign(
{
loadErrorTime: null, // 加载错误的时间
status: NOT_LOADED,// 加载的app初始状态
parcels: {},
devtools: {
overlays: {
options: {},
selectors: [],
},
},
},
// 格式化后的注册应用参数
registration
)
);
if (isInBrowser) {
// 支持 jQuery 的路由事件监听
ensureJQuerySupport();
reroute();
}
}
sanitizeArguments函数解析:格式化微应用入参
ini
function sanitizeArguments(
appNameOrConfig,
appOrLoadApp,
activeWhen,
customProps
) {
const usingObjectAPI = typeof appNameOrConfig === "object";
const registration = {
name: null,
loadApp: null,
activeWhen: null,
customProps: null,
};
if (usingObjectAPI) {
validateRegisterWithConfig(appNameOrConfig);
registration.name = appNameOrConfig.name;
registration.loadApp = appNameOrConfig.app;
registration.activeWhen = appNameOrConfig.activeWhen;
registration.customProps = appNameOrConfig.customProps;
} else {
validateRegisterWithArguments(
appNameOrConfig,
appOrLoadApp,
activeWhen,
customProps
);
registration.name = appNameOrConfig;
registration.loadApp = appOrLoadApp;
registration.activeWhen = activeWhen;
registration.customProps = customProps;
}
registration.loadApp = sanitizeLoadApp(registration.loadApp);
registration.customProps = sanitizeCustomProps(registration.customProps);
registration.activeWhen = sanitizeActiveWhen(registration.activeWhen);
return registration;
}
function sanitizeLoadApp(loadApp) {
if (typeof loadApp !== "function") {
return () => Promise.resolve(loadApp);
}
return loadApp;
}
function sanitizeCustomProps(customProps) {
return customProps ? customProps : {};
}
function sanitizeActiveWhen(activeWhen) {
let activeWhenArray = Array.isArray(activeWhen) ? activeWhen : [activeWhen];
activeWhenArray = activeWhenArray.map((activeWhenOrPath) =>
typeof activeWhenOrPath === "function"
? activeWhenOrPath
: pathToActiveWhen(activeWhenOrPath)
);
return (location) =>
activeWhenArray.some((activeWhen) => activeWhen(location));
}
export function pathToActiveWhen(path, exactMatch) {
const regex = toDynamicPathValidatorRegex(path, exactMatch);
return (location) => {
// compatible with IE10
let origin = location.origin;
if (!origin) {
origin = `${location.protocol}//${location.host}`;
}
const route = location.href
.replace(origin, "")
.replace(location.search, "")
.split("?")[0];
return regex.test(route);
};
}
function toDynamicPathValidatorRegex(path, exactMatch) {
let lastIndex = 0,
inDynamic = false,
regexStr = "^";
if (path[0] !== "/") {
path = "/" + path;
}
for (let charIndex = 0; charIndex < path.length; charIndex++) {
const char = path[charIndex];
const startOfDynamic = !inDynamic && char === ":";
const endOfDynamic = inDynamic && char === "/";
if (startOfDynamic || endOfDynamic) {
appendToRegex(charIndex);
}
}
appendToRegex(path.length);
return new RegExp(regexStr, "i");
function appendToRegex(index) {
const anyCharMaybeTrailingSlashRegex = "[^/]+/?";
const commonStringSubPath = escapeStrRegex(path.slice(lastIndex, index));
regexStr += inDynamic
? anyCharMaybeTrailingSlashRegex
: commonStringSubPath;
if (index === path.length) {
if (inDynamic) {
if (exactMatch) {
// Ensure exact match paths that end in a dynamic portion don't match
// urls with characters after a slash after the dynamic portion.
regexStr += "$";
}
} else {
// For exact matches, expect no more characters. Otherwise, allow
// any characters.
const suffix = exactMatch ? "" : ".*";
regexStr =
// use charAt instead as we could not use es6 method endsWith
regexStr.charAt(regexStr.length - 1) === "/"
? `${regexStr}${suffix}$`
: `${regexStr}(/${suffix})?(#.*)?$`;
}
}
inDynamic = !inDynamic;
lastIndex = index;
}
function escapeStrRegex(str) {
// borrowed from https://github.com/sindresorhus/escape-string-regexp/blob/master/index.js
return str.replace(/[|\{}()[]^$+*?.]/g, "\$&");
}
}
start
解析
在主应用中通过 registerApplication
函数注册完所有的微应用后,会调用 start
函数启动,启动后微应用的生命周期函数才会被 single-spa 执行,而在启动之前只能对微应用进行加载和解析生命周期函数处理。start
函数执行后会标记 single-spa 的启动标识 started
,后续reroute
函数可使用该标识判断是否要执行微应用的生命周期函数,如下所示:
javascript
import { reroute } from "./navigation/reroute.js";
import { patchHistoryApi } from "./navigation/navigation-events.js";
import { isInBrowser } from "./utils/runtime-environment.js";
let started = false;
export function start(opts) {
started = true;
if (isInBrowser) {
//监听hashchange,popstate事件,
//重写addEventListener,removeEventListener。
//将 popstate/hashchange 的监听器存入capturedEventListeners
//重写pushState,replaceState,解决原生 pushState,replaceState 不会触发 //popstate 的问题,详细了解pushState
patchHistoryApi(opts);
reroute();
}
}
export function isStarted() {
return started;
}
// 我们对history API进行了修补,以便single-spa能接收到所有pushState/replaceState调用的通知。
// 我们对addEventListener/removeEventListener进行了修补,以便能够捕获所有
//的popstate/hashchange事件监听器,
// 并延迟调用它们,直到single-spa完成应用程序的挂载/卸载
export function patchHistoryApi(opts) {
if (historyApiIsPatched) {
throw Error(
formatErrorMessage(
43,
__DEV__ &&
`single-spa: patchHistoryApi() was called after the history api was already patched.`
)
);
}
// True by default, as a performance optimization that reduces
// the number of extraneous popstate events
urlRerouteOnly =
opts && opts.hasOwnProperty("urlRerouteOnly") ? opts.urlRerouteOnly : true;
historyApiIsPatched = true;
originalReplaceState = window.history.replaceState;
// We will trigger an app change for any routing events.
window.addEventListener("hashchange", urlReroute);
window.addEventListener("popstate", urlReroute);
// Monkeypatch addEventListener so that we can ensure correct timing
const originalAddEventListener = window.addEventListener;
const originalRemoveEventListener = window.removeEventListener;
window.addEventListener = function (eventName, fn) {
//在这里劫持了addEventListener,如果触发了routingEventsListeningTo中的事件,会将事件回调函数存储在capturedEventListeners中,等应用切换完成后统一调用,其他类型的事件则直接执行
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 originalRemoveEventListener.apply(this, arguments);
};
window.history.pushState = patchedUpdateState(
window.history.pushState,
"pushState"
);
window.history.replaceState = patchedUpdateState(
originalReplaceState,
"replaceState"
);
}
function urlReroute() {
//arguments 类数组包含:
//{
// 0: Event, // 浏览器事件对象,监听hashchange,popstate调用urlReroute
// length: 1
//}
reroute([], arguments);
}
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) {
// fire an artificial popstate event so that
// single-spa applications know about routing that
// occurs in a different application
window.dispatchEvent(
createPopStateEvent(window.history.state, methodName)
);
}
return result;
};
}
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;
}
为什么要收集hashchange,popstate事件,然后在应用加载或者切换完成后统一触发呢?
1. 避免竞态条件(Race Conditions)
-
问题 : 当用户触发路由变化(如后退/前进)时,多个子应用可能同时响应
popstate
事件,导致:- 旧应用未完全卸载,新应用已开始加载。
- 资源竞争(如全局状态、DOM 元素冲突)。
-
解决 : 统一在 应用切换完成 后触发事件监听器,确保所有子应用状态已就绪。
2. 确保应用生命周期顺序
-
问题 : 微前端架构中,子应用的加载、卸载、挂载是异步且可能耗时的操作。若立即执行
popstate
监听器,可能导致:- 监听器访问未初始化的应用代码(如组件未加载)。
- 新旧应用同时操作 DOM(如路由组件渲染冲突)。
-
解决 : 按顺序处理应用生命周期(卸载旧应用 → 加载新应用 → 挂载新应用),完成后再触发监听器。
3. 统一的路由控制权
-
问题 : 若每个子应用独立监听
popstate
事件,可能导致:- 多个应用尝试修改同一路由状态。
- 无法协调跨应用的路由跳转逻辑。
-
解决 : single-spa 作为中央控制器,统一捕获路由事件,集中决策何时执行监听器。
4. 静默导航与状态恢复
- 场景: 当路由切换被取消(如权限校验失败),需要还原 URL 并不触发任何副作用。
- 解决 : 通过
silentNavigation
标记,跳过事件监听器的执行,避免死循环或不必要的渲染。
5. 性能优化
-
优势: 批量处理事件监听器,减少重复渲染和资源加载:
- 合并多次路由变化:短时间内多次路由跳转可合并为一次处理。
- 按需加载资源:仅加载需要激活的应用,减少冗余请求。
🌰 示例:用户点击后退按钮
- 触发
popstate
事件:浏览器通知 single-spa。 - 捕获事件 :暂存监听器到
capturedEventListeners
。 - 卸载旧应用:清理 DOM 和资源。
- 加载新应用:异步获取新应用的代码和资源。
- 挂载新应用:渲染新应用的组件。
- 统一触发监听器 :确保新应用已就绪后,再执行所有
popstate
监听器。
🔧 实现机制
- 劫持事件监听 :重写
window.addEventListener
,拦截 hashchange,popstate 监听器。 - 暂存监听器 :将监听器保存到
capturedEventListeners
队列。 - 延迟执行 :在应用生命周期完成后,调用
callCapturedEventListeners
触发所有暂存监听器。
总结
统一执行 popstate
事件的核心目标是 协调微前端架构下的复杂路由逻辑,确保:
- 应用状态一致性:避免新旧应用状态冲突。
- 生命周期可控性:按顺序处理加载、卸载、挂载。
- 性能与稳定性:减少冗余操作,提升用户体验。
这种设计是微前端框架(如 single-spa)实现 可靠路由管理 的关键机制。
浏览器原生有popstate事件,为什么要用createPopStateEvent重新创建一次popstate事件返回给dispatachEvent进行派发呢?
添加框架标记( singleSpa
和 singleSpaTrigger
)
-
标识事件来源 : 手动创建的事件会附加
singleSpa=true
和singleSpaTrigger="pushState"
(或replaceState
)属性,用于:- 区分框架触发与原生触发:避免重复处理(如防止子应用误处理框架自身触发的路由事件)。
- 调试与追踪 :明确路由变化的触发来源(例如区分是用户点击后退按钮还是代码调用
pushState
)。
-
示例代码:
-
evt.singleSpa = true; ``evt.singleSpaTrigger = originalMethodName; // "pushState" 或 "replaceState"
浏览器兼容性处理
- IE 11 兼容性 : IE 11 不支持
new PopStateEvent()
构造函数,需通过document.createEvent("PopStateEvent")
+initPopStateEvent
手动创建事件。 try { `` evt = new PopStateEvent("popstate", { state }); ``} catch (err) {// IE 11 兼容方案
evt = document.createEvent("PopStateEvent"); `` evt.initPopStateEvent("popstate", false, false, state); ``}
- 统一行为 : 无论浏览器是否支持
PopStateEvent
构造函数,都生成一个结构一致的事件对象。
确保事件对象包含正确的 state
- 原生事件的
state
限制 : 浏览器原生popstate
事件的state
属性由浏览器自动填充,但仅在用户操作触发时才存在。 - 手动注入
state
: 通过createPopStateEvent(state, methodName)
,主动传递state
,确保子应用能访问到正确的路由状态。 createPopStateEvent(window.history.state, methodName);
避免与原生事件的冲突
- 隔离框架逻辑 : 如果直接派发原生
popstate
事件,可能与其他库或框架的事件监听逻辑冲突(如多个路由库同时监听popstate
)。 - 精准控制: 手动创建的事件仅在 single-spa 的上下文中被处理,其他原生监听器不受影响。
reroute,loadApps,performAppChanges
解析
registerApplication
和 start
都会调用 reroute
函数,该函数主要是在微应用需要发生变化时触发,它会通过 getAppChanges
判断需要变化的微应用列表,然后根据外部是否调用了 start
函数来判断执行微应用的批量加载 loadApps
还是执行所有微应用的变化 performAppChanges
reroute函数解析:
import
import { isStarted } from "../start.js";
import { toLoadPromise } from "../lifecycles/load.js";
import { toBootstrapPromise } from "../lifecycles/bootstrap.js";
import { toMountPromise } from "../lifecycles/mount.js";
import { toUnmountPromise } from "../lifecycles/unmount.js";
import {
getAppStatus,
getAppChanges,
getMountedApps,
} from "../applications/apps.js";
import {
callCapturedEventListeners,
originalReplaceState,
} from "./navigation-events.js";
import { toUnloadPromise } from "../lifecycles/unload.js";
import {
toName,
shouldBeActive,
NOT_MOUNTED,
MOUNTED,
NOT_LOADED,
SKIP_BECAUSE_BROKEN,
} from "../applications/app.helpers.js";
import { assign } from "../utils/assign.js";
import { isInBrowser } from "../utils/runtime-environment.js";
import { formatErrorMessage } from "../applications/app-errors.js";
import { addProfileEntry } from "../devtools/profiler.js";
let appChangeUnderway = false,
peopleWaitingOnAppChange = [],
currentUrl = isInBrowser && window.location.href;
export function triggerAppChange() {
// Call reroute with no arguments, intentionally
return reroute();
}
*/*** * @description 重新路由
* 触发的时机:
* 1. 当浏览器的 url 发生变化 *
2. 当调用 start() 方法后 *
3. 当调用 registerApplication() 方法后 *
4. 当调用 navigateToUrl() 方法后 *
5. 当调用 triggerAppChange() 方法后 *
6. 当调用 unloadApplication() 方法后 *
7. 当调用 loadApplication() 方法后 *
8. 当调用 mountRootParcel() 方法后 *
9. 当调用 unmountRootParcel() 方法后 *
* 总结: * retoute 主要是在微应用需要发生变化时触发, * 比如新增、删除、更新、加载、彻底卸载(unload)、
卸载、挂载应用等。 * * @export * @param [pendingPromises=[]] 等待应用变化的 Promise 数组 * @param eventArguments 事件参数,urlReroute函数会在hashchange和popstate事件触发时。调用,事件参数会透传给reroute
export function reroute(
pendingPromises = [],
eventArguments,
silentNavigation = false
) {
if (appChangeUnderway) {
// 如果当前正在执行 performAppChanges 处理应用变化,
// 则将 eventArguments 存储到 peopleWaitingOnAppChange 数组中
// 如果 performAppChanges 函数还未执行完毕,
// 但是再次调用了 reroute 函数,
// 那么会等待 performAppChanges 函数执行完毕
// 在 performAppChanges 函数执行完毕后,
// 会调用 finishUpAndReturn 函数,
// 如果 peopleWaitingOnAppChange 数组中有数据,
// 则会再次执行 reroute 函数
// 因此这里主要用于延迟执行 reroute 函数
// 将 resolve、reject、eventArguments 存储到 peopleWaitingOnAppChange 数组中
// 当 performAppChanges 函数执行完毕后,
// 会调用 finishUpAndReturn 函数,
// 如果 peopleWaitingOnAppChange 数组中有数据,
// 则会再次执行 reroute 函数*
return new Promise((resolve, reject) => {
peopleWaitingOnAppChange.push({
resolve,
reject,
eventArguments,
});
});
}
let startTime, profilerKind;
if (__PROFILE__) {
startTime = performance.now();
if (silentNavigation) {
profilerKind = "silentNavigation";
} else if (eventArguments) {
profilerKind = "browserNavigation";
} else {
profilerKind = "triggerAppChange";
}
}
// 获取当前应用的变化情况
// 1. appsToUnload: 需要彻底卸载的应用
// 2. appsToUnmount: 需要卸载的应用
// 3. appsToLoad: 需要加载的应用
// 4. appsToMount: 需要挂载的应用
const { appsToUnload, appsToUnmount, appsToLoad, appsToMount } =
getAppChanges();
let appsThatChanged,
cancelPromises = [],
oldUrl = currentUrl,
newUrl = (currentUrl = window.location.href);
if (isStarted()) {
appChangeUnderway = true;
appsThatChanged = appsToUnload.concat(
appsToLoad,
appsToUnmount,
appsToMount
);
return performAppChanges();
} else {
appsThatChanged = appsToLoad;
return loadApps();
}
//cancelNavigation用来取消导航事件,在处理应用变化之前,会通过触发single-spa自定义事件
//fireSingleSpaEvent(
// "before-routing-event",
// getCustomEventDetail(true, { cancelNavigation })
//);
//将cancelNavigation函数作为参数传递给外界调用,外界传递一个任意值,默认为true,取消本次导航
function cancelNavigation(val = true) {
//将调用该函数时传递的参数统一为promise存储到**cancelPromises
//performAppChanges先执行cancelPromises中的promise, Promise.all(cancelPromises).then((cancelValues) => {
// const navigationIsCanceled = cancelValues.some((v) => v);
// if (navigationIsCanceled) {符合条件取消导航,执行下一轮reroute
const promise =
typeof val?.then === "function" ? val : Promise.resolve(val);
cancelPromises.push(
promise.catch((err) => {
console.warn(
Error(
formatErrorMessage(
42,
__DEV__ &&
`single-spa: A cancelNavigation promise rejected with the following value: ${err}`
)
)
);
console.warn(err);
// Interpret a Promise rejection to mean that the navigation should not be canceled
return false;
})
);
}
//加载微应用
function loadApps() {
return Promise.resolve().then(() => {
const loadPromises = appsToLoad.map(toLoadPromise);
let succeeded;
return (
Promise.all(loadPromises)
.then(callAllEventListeners)
// there are no mounted apps, before start() is called, so we always return []
.then(() => {
if (__PROFILE__) {
succeeded = true;
}
return [];
})
.catch((err) => {
if (__PROFILE__) {
succeeded = false;
}
callAllEventListeners();
throw err;
})
.finally(() => {
if (__PROFILE__) {
addProfileEntry(
"routing",
"loadApps",
profilerKind,
startTime,
performance.now(),
succeeded
);
}
})
);
});
}
function performAppChanges() {
return Promise.resolve().then(() => {
// https://github.com/single-spa/single-spa/issues/545
fireSingleSpaEvent(
appsThatChanged.length === 0
? "before-no-app-change"
: "before-app-change",
getCustomEventDetail(true)
);
fireSingleSpaEvent(
"before-routing-event",
getCustomEventDetail(true, { cancelNavigation })
);
return Promise.all(cancelPromises).then((cancelValues) => {
const navigationIsCanceled = cancelValues.some((v) => v);
if (navigationIsCanceled) {
//用户通过监听**before-routing-event事件**取消导航
originalReplaceState.call(
window.history,
history.state,
"",
oldUrl.substring(location.origin.length)
);
// Single-spa's internal tracking of current url needs to be updated after the url change above
currentUrl = location.href;
// necessary for the reroute function to know that the current reroute is finished
appChangeUnderway = false;
if (__PROFILE__) {
addProfileEntry(
"routing",
"navigationCanceled",
profilerKind,
startTime,
performance.now(),
true
);
}
// Tell single-spa to reroute again, this time with the url set to the old URL
return reroute(pendingPromises, eventArguments, true);
}
//用户没有取消导航继续处理应用的变化
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);
let unmountFinishedTime;
unmountAllPromise.then(
() => {
if (__PROFILE__) {
unmountFinishedTime = performance.now();
addProfileEntry(
"routing",
"unmountAndUnload",
profilerKind,
startTime,
performance.now(),
true
);
}
fireSingleSpaEvent(
"before-mount-routing-event",
getCustomEventDetail(true)
);
},
(err) => {
if (__PROFILE__) {
addProfileEntry(
"routing",
"unmountAndUnload",
profilerKind,
startTime,
performance.now(),
true
);
}
throw err;
}
);
/* We load and bootstrap apps while other apps are unmounting, but we
* wait to mount the app until all apps are finishing unmounting
*/
const loadThenMountPromises = appsToLoad.map((app) => {
return toLoadPromise(app).then((app) =>
tryToBootstrapAndMount(app, unmountAllPromise)
);
});
/* These are the apps that are already bootstrapped and just need
* to be mounted. They each wait for all unmounting apps to finish up
* before they mount.
*/
const mountPromises = appsToMount
.filter((appToMount) => appsToLoad.indexOf(appToMount) < 0)
.map((appToMount) => {
return tryToBootstrapAndMount(appToMount, unmountAllPromise);
});
return unmountAllPromise
.catch((err) => {
callAllEventListeners();
throw err;
})
.then(() => {
/*现在需要卸载的应用程序已经卸载,它们的DOM导航
*事件(如hashchange或popstate)应该已被清理。所以很安全
*让剩余的捕获事件侦听器处理DOM事件。
*/
callAllEventListeners();
return Promise.all(loadThenMountPromises.concat(mountPromises))
.catch((err) => {
pendingPromises.forEach((promise) => promise.reject(err));
throw err;
})
.then(finishUpAndReturn)
.then(
() => {
if (__PROFILE__) {
addProfileEntry(
"routing",
"loadAndMount",
profilerKind,
unmountFinishedTime,
performance.now(),
true
);
}
},
(err) => {
if (__PROFILE__) {
addProfileEntry(
"routing",
"loadAndMount",
profilerKind,
unmountFinishedTime,
performance.now(),
false
);
}
throw err;
}
);
});
});
});
}
function finishUpAndReturn() {
const returnValue = getMountedApps();
pendingPromises.forEach((promise) => promise.resolve(returnValue));
try {
const appChangeEventName =
appsThatChanged.length === 0 ? "no-app-change" : "app-change";
fireSingleSpaEvent(appChangeEventName, getCustomEventDetail());
fireSingleSpaEvent("routing-event", getCustomEventDetail());
} catch (err) {
/* We use a setTimeout because if someone else's event handler throws an error, single-spa
* needs to carry on. If a listener to the event throws an error, it's their own fault, not
* single-spa's.
*/
setTimeout(() => {
throw err;
});
}
/* Setting this allows for subsequent calls to reroute() to actually perform
* a reroute instead of just getting queued behind the current reroute call.
* We want to do this after the mounting/unmounting is done but before we
* resolve the promise for the `reroute` function.
*/
appChangeUnderway = false;
if (peopleWaitingOnAppChange.length > 0) {
/* While we were rerouting, someone else triggered another reroute that got queued.
* So we need reroute again.
*/
const nextPendingPromises = peopleWaitingOnAppChange;
peopleWaitingOnAppChange = [];
reroute(nextPendingPromises);
}
return returnValue;
}
/* We need to call all event listeners that have been delayed because they were
* waiting on single-spa. This includes haschange and popstate events for both
* the current run of performAppChanges(), but also all of the queued event listeners.
* We want to call the listeners in the same order as if they had not been delayed by
* single-spa, which means queued ones first and then the most recent one.
*/
function callAllEventListeners() {
//在静默导航期间(当导航被取消并且我们将返回到旧URL时),
//我们不应该触发任何popstate/hashchange事件
if (!silentNavigation) {
pendingPromises.forEach((pendingPromise) => {
callCapturedEventListeners(pendingPromise.eventArguments);
});
callCapturedEventListeners(eventArguments);
}
}
function getCustomEventDetail(isBeforeChanges = false, extraProperties) {
const newAppStatuses = {};
const appsByNewStatus = {
// for apps that were mounted
[MOUNTED]: [],
// for apps that were unmounted
[NOT_MOUNTED]: [],
// apps that were forcibly unloaded
[NOT_LOADED]: [],
// apps that attempted to do something but are broken now
[SKIP_BECAUSE_BROKEN]: [],
};
if (isBeforeChanges) {
appsToLoad.concat(appsToMount).forEach((app, index) => {
addApp(app, MOUNTED);
});
appsToUnload.forEach((app) => {
addApp(app, NOT_LOADED);
});
appsToUnmount.forEach((app) => {
addApp(app, NOT_MOUNTED);
});
} else {
appsThatChanged.forEach((app) => {
addApp(app);
});
}
const result = {
detail: {
newAppStatuses,
appsByNewStatus,
totalAppChanges: appsThatChanged.length,
originalEvent: eventArguments?.[0],
oldUrl,
newUrl,
},
};
if (extraProperties) {
assign(result.detail, extraProperties);
}
return result;
function addApp(app, status) {
const appName = toName(app);
status = status || getAppStatus(appName);
newAppStatuses[appName] = status;
const statusArr = (appsByNewStatus[status] =
appsByNewStatus[status] || []);
statusArr.push(appName);
}
}
function fireSingleSpaEvent(name, eventProperties) {
// During silent navigation (caused by navigation cancelation), we should not
// fire any single-spa events
if (!silentNavigation) {
window.dispatchEvent(
new CustomEvent(`single-spa:${name}`, eventProperties)
);
}
}
}
/**
* Let's imagine that some kind of delay occurred during application loading.
* The user without waiting for the application to load switched to another route,
* this means that we shouldn't bootstrap and mount that application, thus we check
* twice if that application should be active before bootstrapping and mounting.
* https://github.com/single-spa/single-spa/issues/524
*/
function tryToBootstrapAndMount(app, unmountAllPromise) {
if (shouldBeActive(app)) {
return toBootstrapPromise(app).then((app) =>
unmountAllPromise.then(() =>
shouldBeActive(app) ? toMountPromise(app) : app
)
);
} else {
return unmountAllPromise.then(() => app);
}
}
single-spa的生命周期
生命周期汇总:
- toBootstrapPromise:应用变化时,将要卸载的应用处理干净后加载本次变化要加载的应用后进行初始化调用
- toLoadPromise:应用注册完成加载时候会调用,应用变化时候加载会调用一次
- toMountPromise :在调用toBootstrapPromise 后会紧接着判断该app是否需要激活,需要则直接调用toMountPromise挂载应用
- toUnloadPromise:在应用变化时,在判断不取消本次导航过后,toUnloadPromise最先被调用
- toUnmountPromise :toUnloadPromise 调用后紧接着就调用toUnmountPromise , 卸载完成后还要立马调用toUnloadPromise 进行移除,同时single-spa会将toUnloadPromise,toUnmountPromise进行合并,确保操作全部完成后才继续加载,初始化,挂载等生命周期操作
toBootstrapPromise
解析
reasonableTime是一个通用函数,主要是负责生命周期函数的超时处理,超时时间用户可以自己配置,single-spa也有默认值
ini
export function reasonableTime(appOrParcel, lifecycle) {
const timeoutConfig = appOrParcel.timeouts[lifecycle];
const warningPeriod = timeoutConfig.warningMillis;
const type = objectType(appOrParcel);
return new Promise((resolve, reject) => {
let finished = false;
let errored = false;
appOrParcel[lifecycle](getProps(appOrParcel))
.then((val) => {
finished = true;
resolve(val);
})
.catch((val) => {
finished = true;
reject(val);
});
setTimeout(() => maybeTimingOut(1), warningPeriod);
setTimeout(() => maybeTimingOut(true), timeoutConfig.millis);
const errMsg = formatErrorMessage(
31,
__DEV__ &&
`Lifecycle function ${lifecycle} for ${type} ${toName(
appOrParcel
)} lifecycle did not resolve or reject for ${timeoutConfig.millis} ms.`,
lifecycle,
type,
toName(appOrParcel),
timeoutConfig.millis
);
function maybeTimingOut(shouldError) {
//finished初始状态为false当生命周期函数执行完毕后,finished会被置为true
if (!finished) {
//说明生命周期还没被执行完成,shouldError如果为true,说明已经执行超时
if (shouldError === true) {
errored = true;
if (timeoutConfig.dieOnTimeout) {
reject(Error(errMsg));
} else {
console.error(errMsg);
//don't resolve or reject, we're waiting this one out
}
} else if (!errored) {
//shouldError为false,还未超时,重新定时setTimeout,执行下一次超时轮询逻辑
const numWarnings = shouldError;
const numMillis = numWarnings * warningPeriod;
console.warn(errMsg);
if (numMillis + warningPeriod < timeoutConfig.millis) {
setTimeout(() => maybeTimingOut(numWarnings + 1), warningPeriod);
}
}
}
}
});
}
toBootstrapPromise生命周期函数解析:
javascript
import {
NOT_BOOTSTRAPPED,
BOOTSTRAPPING,
NOT_MOUNTED,
SKIP_BECAUSE_BROKEN,
toName,
isParcel,
} from "../applications/app.helpers.js";
import { reasonableTime } from "../applications/timeouts.js";
import { handleAppError, transformErr } from "../applications/app-errors.js";
import { addProfileEntry } from "../devtools/profiler.js";
export function toBootstrapPromise(appOrParcel, hardFail) {
let startTime, profileEventType;
return Promise.resolve().then(() => {
if (appOrParcel.status !== NOT_BOOTSTRAPPED) {
return appOrParcel;
}
if (__PROFILE__) {
profileEventType = isParcel(appOrParcel) ? "parcel" : "application";
startTime = performance.now();
}
appOrParcel.status = BOOTSTRAPPING;
if (!appOrParcel.bootstrap) {
// Default implementation of bootstrap
return Promise.resolve().then(successfulBootstrap);
}
return reasonableTime(appOrParcel, "bootstrap")
.then(successfulBootstrap)
.catch((err) => {
if (__PROFILE__) {
addProfileEntry(
profileEventType,
toName(appOrParcel),
"bootstrap",
startTime,
performance.now(),
false
);
}
if (hardFail) {
throw transformErr(err, appOrParcel, SKIP_BECAUSE_BROKEN);
} else {
handleAppError(err, appOrParcel, SKIP_BECAUSE_BROKEN);
return appOrParcel;
}
});
});
function successfulBootstrap() {
appOrParcel.status = NOT_MOUNTED;
if (__PROFILE__) {
addProfileEntry(
profileEventType,
toName(appOrParcel),
"bootstrap",
startTime,
performance.now(),
true
);
}
return appOrParcel;
}
}
toLoadPromise
解析
typescript
import {
LOAD_ERROR,
NOT_BOOTSTRAPPED,
LOADING_SOURCE_CODE,
SKIP_BECAUSE_BROKEN,
NOT_LOADED,
objectType,
toName,
} from "../applications/app.helpers.js";
import { ensureValidAppTimeouts } from "../applications/timeouts.js";
import {
handleAppError,
formatErrorMessage,
} from "../applications/app-errors.js";
import {
flattenFnArray,
smellsLikeAPromise,
validLifecycleFn,
} from "./lifecycle.helpers.js";
import { getProps } from "./prop.helpers.js";
import { assign } from "../utils/assign.js";
import { addProfileEntry } from "../devtools/profiler.js";
export function toLoadPromise(appOrParcel) {
return Promise.resolve().then(() => {
if (appOrParcel.loadPromise) {
// 如果 app.loadPromise 存在,
// 直接返回 app.loadPromise
// 这里可以确保同一个 app 只会执行一次 loadApp 方法
// 例如注册微应用时会调用 loadApps 方法,
// 会执行微应用的 toLoadPromise,
// 此时会缓存 app.loadPromise
// 而启动 start 函数最终调用 performAppChanges 时还会执行微应用的 toLoadPromise
// 为了避免重复执行 app.loadPromise 方法,
// 这里会直接返回 app.loadPromise(Promise 对象)
return appOrParcel.loadPromise;
}
// 如果 app.status 不是 NOT_LOADED 和 LOAD_ERROR,直接返回 app
if (
appOrParcel.status !== NOT_LOADED &&
appOrParcel.status !== LOAD_ERROR
) {
return appOrParcel;
}
let startTime;
if (__PROFILE__) {
startTime = performance.now();
}
// 将 app.status 设置为 LOADING_SOURCE_CODE
appOrParcel.status = LOADING_SOURCE_CODE;
let appOpts, isUserErr;
// 使用 app.loadPromise 缓存 app 的加载,
// 避免在 loadApps 以及 performAppChanges 时重复加载
return (appOrParcel.loadPromise = Promise.resolve()
.then(() => {
// 这里的 loadApp 其实就是 registerApplication 的第二个参数
// 在主应用中使用 window.fetch 获取子应用的资源,
// 执行后需要返回 Promise,
// 并且返回的是子应用的生命周期函数对象
const loadPromise = appOrParcel.loadApp(getProps(appOrParcel));
if (!smellsLikeAPromise(loadPromise)) {
//如果loadPromise不是promise函数,抛出错误
isUserErr = true;
throw Error(
formatErrorMessage(
33,
__DEV__ &&
`single-spa loading function did not return a promise. Check the second argument to registerApplication('${toName(
appOrParcel
)}', loadingFunction, activityFunction)`,
toName(appOrParcel)
)
);
}
// 这里的 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) => {
appOrParcel.loadErrorTime = null;
appOpts = val;
let validationErrMessage, validationErrCode;
//以下就是对生命周期的格式进行检测,判断appOpts, appOpts 的 bootstrap、mount、unmount 是否符合要求
if (typeof appOpts !== "object") {
validationErrCode = 34;
if (__DEV__) {
validationErrMessage = `does not export anything`;
}
}
if (
// ES Modules don't have the Object prototype
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(
appOrParcel
)}' resolved with the following, which does not have bootstrap, mount, and unmount functions`,
type,
toName(appOrParcel),
appOptsStr
),
appOpts
);
handleAppError(
validationErrMessage,
appOrParcel,
SKIP_BECAUSE_BROKEN
);
return appOrParcel;
}
if (appOpts.devtools && appOpts.devtools.overlays) {
appOrParcel.devtools.overlays = assign(
{},
appOrParcel.devtools.overlays,
appOpts.devtools.overlays
);
}
// 设置 app 的状态为 NOT_BOOTSTRAPPED
appOrParcel.status = NOT_BOOTSTRAPPED;
// 将 appOpts 中的周期函数扁平化
appOrParcel.bootstrap = flattenFnArray(appOpts, "bootstrap");
appOrParcel.mount = flattenFnArray(appOpts, "mount");
appOrParcel.unmount = flattenFnArray(appOpts, "unmount");
appOrParcel.unload = flattenFnArray(appOpts, "unload");
appOrParcel.timeouts = ensureValidAppTimeouts(appOpts.timeouts);
// 删除 app.loadPromise,表明 app.loadPromise 已经执行完成
// 下一次执行 toLoadPromise 时会重新执行 app.loadPromise
delete appOrParcel.loadPromise;
if (__PROFILE__) {
addProfileEntry(
"application",
toName(appOrParcel),
"load",
startTime,
performance.now(),
true
);
}
return appOrParcel;
});
})
.catch((err) => {
delete appOrParcel.loadPromise;
let newStatus;
if (isUserErr) {
newStatus = SKIP_BECAUSE_BROKEN;
} else {
newStatus = LOAD_ERROR;
appOrParcel.loadErrorTime = new Date().getTime();
}
handleAppError(err, appOrParcel, newStatus);
if (__PROFILE__) {
addProfileEntry(
"application",
toName(appOrParcel),
"load",
startTime,
performance.now(),
false
);
}
return appOrParcel;
}));
});
}
为什么要缓存了loadPromise后完成加载后又删除loadPromise呢?
- 防止并发重复加载
场景:
假设多个地方同时调用 toLoadPromise(app)
(例如路由变化和手动触发同时发生),若未缓存 loadPromise
:
- 第一次调用触发加载逻辑(如网络请求、初始化资源)。
- 第二次调用再次触发相同逻辑,导致重复加载。
解决方案:
appOrParcel.loadPromise = Promise.resolve().then(() => {// 加载逻辑...
});
- 首次调用 :创建
loadPromise
并开始加载。 - 后续调用 :直接返回已存在的
loadPromise
,复用同一个 Promise。
- 保证异步操作一致性
场景:
应用加载是一个异步过程(如动态 import()
或网络请求),需要确保所有调用者等待同一个结果。
// 示例:两个并发调用 ``const promise1 = toLoadPromise(app);
const promise2 = toLoadPromise(app); `` // 两者应指向同一个 Promise ``console.log(promise1 === promise2); // true
优势:
- 避免资源浪费:防止多次加载同一应用。
- 状态一致性:所有调用者获得同一结果(成功或失败)。
- 缓存与删除的协作
阶段 | 操作 | 目的 |
---|---|---|
加载中 | 缓存 loadPromise | 防止并发重复加载 |
加载完成 | 删除 loadPromise | 允许后续重新加载(如错误恢复或热更新) |
toMountPromise
解析
javascript
import {
NOT_MOUNTED,
MOUNTED,
SKIP_BECAUSE_BROKEN,
MOUNTING,
toName,
isParcel,
} from "../applications/app.helpers.js";
import { handleAppError, transformErr } from "../applications/app-errors.js";
import { reasonableTime } from "../applications/timeouts.js";
import CustomEvent from "custom-event";
import { toUnmountPromise } from "./unmount.js";
import { addProfileEntry } from "../devtools/profiler.js";
let beforeFirstMountFired = false;
let firstMountFired = false;
export function toMountPromise(appOrParcel, hardFail) {
return Promise.resolve().then(() => {
if (appOrParcel.status !== NOT_MOUNTED) {
return appOrParcel;
}
let startTime, profileEventType;
if (__PROFILE__) {
profileEventType = isParcel(appOrParcel) ? "parcel" : "application";
startTime = performance.now();
}
// 如果是第一次挂载子应用,
// 触发 single-spa:before-first-mount 事件
if (!beforeFirstMountFired) {
window.dispatchEvent(new CustomEvent("single-spa:before-first-mount"));
beforeFirstMountFired = true;
}
appOrParcel.status = MOUNTING;
return reasonableTime(appOrParcel, "mount")
.then(() => {
appOrParcel.status = MOUNTED;
// 如果是第一次挂载子应用,触发 single-spa:first-mount 事件
if (!firstMountFired) {
window.dispatchEvent(new CustomEvent("single-spa:first-mount"));
firstMountFired = true;
}
if (__PROFILE__) {
addProfileEntry(
profileEventType,
toName(appOrParcel),
"mount",
startTime,
performance.now(),
true
);
}
return appOrParcel;
})
.catch((err) => {
//如果我们无法挂载appOrParcel,我们应该在放入SKIP_BEAUSE_BROKEN之前尝试卸载它
//我们暂时将appOrParcel置于MOUNTED状态,以便toUnmountPromise实际尝试卸载它
//而不是仅仅做一个无操作。
appOrParcel.status = MOUNTED;
return toUnmountPromise(appOrParcel, true).then(
setSkipBecauseBroken,
setSkipBecauseBroken
);
function setSkipBecauseBroken() {
if (__PROFILE__) {
addProfileEntry(
profileEventType,
toName(appOrParcel),
"mount",
startTime,
performance.now(),
false
);
}
if (!hardFail) {
handleAppError(err, appOrParcel, SKIP_BECAUSE_BROKEN);
return appOrParcel;
} else {
throw transformErr(err, appOrParcel, SKIP_BECAUSE_BROKEN);
}
}
});
});
}
toUnloadPromise
解析
javascript
import {
NOT_MOUNTED,
UNLOADING,
NOT_LOADED,
LOAD_ERROR,
SKIP_BECAUSE_BROKEN,
toName,
} from "../applications/app.helpers.js";
import { handleAppError } from "../applications/app-errors.js";
import { reasonableTime } from "../applications/timeouts.js";
import { addProfileEntry } from "../devtools/profiler.js";
const appsToUnload = {};
export function toUnloadPromise(appOrParcel) {
return Promise.resolve().then(() => {
const unloadInfo = appsToUnload[toName(appOrParcel)];
if (!unloadInfo) {
/* No one has called unloadApplication for this app,
*/
return appOrParcel;
}
if (appOrParcel.status === NOT_LOADED) {
/*此应用程序已卸载。我们只需要清理一下
*任何仍然认为我们需要卸载应用程序的东西。
*/
finishUnloadingApp(appOrParcel, unloadInfo);
return appOrParcel;
}
if (appOrParcel.status === UNLOADING) {
/*unloadApplication和reroute都希望卸载此应用程序。
*不过,这只需要做一次。
*/
return unloadInfo.promise.then(() => appOrParcel);
}
if (
appOrParcel.status !== NOT_MOUNTED &&
appOrParcel.status !== LOAD_ERROR
) {
/*在挂载之前,无法卸载该应用程序。*/
return appOrParcel;
}
let startTime;
if (__PROFILE__) {
startTime = performance.now();
}
const unloadPromise =
appOrParcel.status === LOAD_ERROR
? Promise.resolve()
: reasonableTime(appOrParcel, "unload");
appOrParcel.status = UNLOADING;
return unloadPromise
.then(() => {
if (__PROFILE__) {
addProfileEntry(
"application",
toName(appOrParcel),
"unload",
startTime,
performance.now(),
true
);
}
finishUnloadingApp(appOrParcel, unloadInfo);
return appOrParcel;
})
.catch((err) => {
if (__PROFILE__) {
addProfileEntry(
"application",
toName(appOrParcel),
"unload",
startTime,
performance.now(),
false
);
}
errorUnloadingApp(appOrParcel, unloadInfo, err);
return appOrParcel;
});
});
}
function finishUnloadingApp(app, unloadInfo) {
delete appsToUnload[toName(app)];
// Unloaded apps don't have lifecycles
delete app.bootstrap;
delete app.mount;
delete app.unmount;
delete app.unload;
app.status = NOT_LOADED;
/* resolve the promise of whoever called unloadApplication.
* This should be done after all other cleanup/bookkeeping
*/
unloadInfo.resolve();
}
function errorUnloadingApp(app, unloadInfo, err) {
delete appsToUnload[toName(app)];
// Unloaded apps don't have lifecycles
delete app.bootstrap;
delete app.mount;
delete app.unmount;
delete app.unload;
handleAppError(err, app, SKIP_BECAUSE_BROKEN);
unloadInfo.reject(err);
}
export function addAppToUnload(app, promiseGetter, resolve, reject) {
appsToUnload[toName(app)] = { app, resolve, reject };
Object.defineProperty(appsToUnload[toName(app)], "promise", {
get: promiseGetter,
});
}
export function getAppUnloadInfo(appName) {
return appsToUnload[appName];
}