single-spa源码解析
我们先从Github上下载single-spa的源码:https://github.com/single-spa/single-spa
整个源码采用rollup来构建的,可以从rollup.config.js中找到入口文件,在src/single-spa.js中,对外提供了一系列的方法,像start、registerApplication等。
single-spa最主要的实现了应用的注册、路由的修改和监听。其中路由的监听在src/navigation/navigation-events.js中,应用的注册主要包括了应用的生命周期相关内容在src/lifecycles/xxx.js中。
应用注册
应用注册提供了registerApplication方法,源码如下:
//applications/apps
**
* 注册应用,两种方式
* registerApplication('app1', loadApp(url), activeWhen('/app1'), customProps)
* registerApplication({
* name: 'app1',
* app: loadApp(url),
* activeWhen: activeWhen('/app1'),
* customProps: {}
* })
* @param {*} appNameOrConfig 应用名称或者应用配置对象
* @param {*} appOrLoadApp 应用的加载方法,是一个 promise
* @param {*} activeWhen 判断应用是否激活的一个方法,方法返回 true or false
* @param {*} customProps 传递给子应用的 props 对象
*/
export function registerApplication(
appNameOrConfig,
appOrLoadApp,
activeWhen,
customProps
) {
// 数据整理, 验证传参的合理性, 最后整理得到数据源:
// {
// name: xxx,
// loadApp: xxx,
// activeWhen: xxx,
// customProps: xxx,
// }
const registration = sanitizeArguments(
appNameOrConfig,
appOrLoadApp,
activeWhen,
customProps
);
// 如果有重名,则抛出错误, 所以 name 应该是要保持唯一值
if (getAppNames().indexOf(registration.name) !== -1)
throw Error(
formatErrorMessage(
21,
__DEV__ &&
`There is already an app registered with name ${registration.name}`,
registration.name
)
);
// apps 是 single-spa 的一个全局变量, 用来存储当前的应用数据
apps.push(
assign(
{
loadErrorTime: null,
status: NOT_LOADED,
parcels: {},
devtools: {
overlays: {
options: {},
selectors: [],
},
},
},
registration
)
);
// 判断 window 是否为空, 进入条件
if (isInBrowser) {
ensureJQuerySupport(); // 确保 jq 可用
reroute();
}
}
首先对调用了sanitizeArguments对registerApplication的参数进行整合,然后判断子应用是否注册过,注册过则抛出异常,然后将子应用添加到apps这个数组中,等到调用start方法时来进行对应的渲染。
最后调用reroute方法。reroute 是 single-spa 的核心函数, 在注册应用时调用此函数的作用, 就是将应用的 promise 加载函数, 注入一个待加载的数组中 等后面正式启动时再调用, 类似于 ()=>import('xxx')
// navigation/reroute.js
export function reroute(pendingPromises = [], eventArguments) {
// 一开始默认是 false
if (appChangeUnderway) {
// 如果是 true, 则返回一个 promise, 在队列中添加 resolve 参数等等
return new Promise((resolve, reject) => {
peopleWaitingOnAppChange.push({
resolve,
reject,
eventArguments,
});
});
}
// 遍历所有应用数组 apps , 根据 app 的状态, 来分类到这四个数组中
// 会根据 url 和 whenActive 判断是否该 load
// unload , unmount, to load, to mount
const {
appsToUnload,
appsToUnmount,
appsToLoad,
appsToMount,
} = getAppChanges();
let appsThatChanged,
navigationIsCanceled = false,
oldUrl = currentUrl,
newUrl = (currentUrl = window.location.href);
// 存储着一个闭包变量, 是否已经启动, 在注册步骤中, 是未启动的
if (isStarted()) {
appChangeUnderway = true;
// 合并状态需要变更的 app
appsThatChanged = appsToUnload.concat(
appsToLoad,
appsToUnmount,
appsToMount
);
// 返回 performAppChanges 函数
return performAppChanges();
} else {
// 未启动, 直接返回 loadApps, 他的定义在下方
appsThatChanged = appsToLoad;
return loadApps();
}
function cancelNavigation() {
navigationIsCanceled = true;
}
// 返回一个 resolve 的 promise,通过微任务来加载apps
// 将需要加载的应用, map 成一个新的 promise 数组
// 并且用 promise.all 来返回
// 不管成功或者失败, 都会调用 callAllEventListeners 函数, 进行路由通知
function loadApps() {
return Promise.resolve().then(() => {
// toLoadPromise 主要来定义资源的加载, 以及对应的回调
const loadPromises = appsToLoad.map(toLoadPromise);
// 通过 Promise.all 来执行, 返回的是 app.loadPromise,这是资源加载
return (
Promise.all(loadPromises)
.then(callAllEventListeners)
// there are no mounted apps, before start() is called, so we always return []
.then(() => [])
.catch((err) => {
callAllEventListeners();
throw err;
})
);
});
}
function performAppChanges() {
return Promise.resolve().then(() => {
// https://github.com/single-spa/single-spa/issues/545
//触发自定义事件
// 当前事件触发 getCustomEventDetail
// 主要是 app 的状态, url 的变更, 参数等等
window.dispatchEvent(
new CustomEvent(
appsThatChanged.length === 0
? "single-spa:before-no-app-change"
: "single-spa:before-app-change",
getCustomEventDetail(true)
)
);
window.dispatchEvent(
new CustomEvent(
"single-spa:before-routing-event",
getCustomEventDetail(true, { cancelNavigation })
)
);
// 除非在上一个事件中调用了 cancelNavigation, 才会进入这一步
if (navigationIsCanceled) {
window.dispatchEvent(
new CustomEvent(
"single-spa:before-mount-routing-event",
getCustomEventDetail(true)
)
);
// 将 peopleWaitingOnAppChange 的数据重新执行 reroute 函数 reroute(peopleWaitingOnAppChange)
finishUpAndReturn();
// 更新 url
navigateToUrl(oldUrl);
return;
}
// 准备卸载的 app
const unloadPromises = appsToUnload.map(toUnloadPromise);
// 执行子应用中的 unmount 函数, 如果超时也会有报警
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)
)
);
});
/* We load and bootstrap apps while other apps are unmounting, but we
* wait to mount the app until all apps are finishing unmounting
*/
// 执行 bootstrap 生命周期, tryToBootstrapAndMount 确保先执行 bootstrap
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.
*/
// 执行 mount 事件
const mountPromises = appsToMount
.filter((appToMount) => appsToLoad.indexOf(appToMount) < 0)
.map((appToMount) => {
return tryToBootstrapAndMount(appToMount, unmountAllPromise);
});
return unmountAllPromise
.catch((err) => {
callAllEventListeners();
throw err;
})
.then(() => {
/* Now that the apps that needed to be unmounted are unmounted, their DOM navigation
* events (like hashchange or popstate) should have been cleaned up. So it's safe
* to let the remaining captured event listeners to handle about the DOM event.
*/
callAllEventListeners();
return Promise.all(loadThenMountPromises.concat(mountPromises))
.catch((err) => {
pendingPromises.forEach((promise) => promise.reject(err));
throw err;
})
.then(finishUpAndReturn);
});
});
}
function finishUpAndReturn() {
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) {
/* 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() {
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,
navigationIsCanceled,
},
};
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);
}
}
}
我们看一下toLoadPromise,注册流程中 reroute的主要执行函数,它的主要功能是赋值 loadPromise 给 app, 其中 loadPromise 函数中包括了: 执行函数、来加载应用的资源、定义加载完毕的回调函数、状态的修改、还有加载错误的一些处理。
//lifecycles/load.js
export function toLoadPromise(app) {
return Promise.resolve().then(() => {
// 是否重复注册 promise 加载了
if (app.loadPromise) {
return app.loadPromise;
}
// 刚注册的就是 NOT_LOADED 状态
if (app.status !== NOT_LOADED && app.status !== LOAD_ERROR) {
return app;
}
// 修改状态为, 加载源码
app.status = LOADING_SOURCE_CODE;
let appOpts, isUserErr;
// 返回的是 app.loadPromise
return (app.loadPromise = Promise.resolve()
.then(() => {
// 这里调用的了 app的 loadApp 函数(由外部传入的), 开始加载资源
// getProps 用来判断 customProps 是否合法, 最后传值给 loadApp 函数
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
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)
)
);
}
return loadPromise.then((val) => {
// 资源加载成功
app.loadErrorTime = null;
appOpts = val;
let validationErrMessage, validationErrCode;
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(
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状态为未初始化,表示加载完了
app.status = NOT_BOOTSTRAPPED;
// 在app对象上挂载生命周期方法,每个方法都接收一个props作为参数,方法内部执行子应用导出的生命周期函数,并确保生命周期函数返回一个promise
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);
// 执行完毕之后删除 loadPromise
delete app.loadPromise;
return app;
});
})
.catch((err) => {
// 报错也会删除 loadPromise
delete app.loadPromise;
// 修改状态为 用户的传参报错, 或者是加载出错
let newStatus;
if (isUserErr) {
newStatus = SKIP_BECAUSE_BROKEN;
} else {
newStatus = LOAD_ERROR;
app.loadErrorTime = new Date().getTime();
}
handleAppError(err, app, newStatus);
return app;
}));
});
}
应用启动
注册完应用之后, 最后是 start 方法执行。调用start之前,应用会被加载,但不会初始化、挂载和卸载,有了start可以更好的控制性能。
// start.js
export function start(opts) {
// 主要作用还是将标记符 started设置为 true 了
started = true;
if (opts && opts.urlRerouteOnly) {
// 使用此参数可以人为地触发事件 popstate
setUrlRerouteOnly(opts.urlRerouteOnly);
}
if (isInBrowser) {
reroute();
}
}
启动后也会调用reroute方法,在reroute方法中,就会触发此函数 performAppChanges,并返回结果,该函数的作用主要是事件的触发, 包括自定义事件和子应用中的一些事件。
路由监听
在navigation-events.js中定义了许多的方法,路由监听代码被放在全局作用域内,bundle被加载后自动执行。
其中路由监听部分代码如下:
// navigation/navigation-events.js
if (isInBrowser) {
// 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) {
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);
};
window.history.pushState = patchedUpdateState(
window.history.pushState,
"pushState"
);
window.history.replaceState = patchedUpdateState(
window.history.replaceState,
"replaceState"
);
if (window.singleSpaNavigate) {
console.warn(
formatErrorMessage(
41,
__DEV__ &&
"single-spa has been loaded twice on the page. This can result in unexpected behavior."
)
);
} else {
/* For convenience in `onclick` attributes, we expose a global function for navigating to
* whatever an <a> tag's href is.
*/
window.singleSpaNavigate = navigateToUrl;
}
}
对hashchange和popstate进行监听,如果有路由切换的操作就会执行urlReroute函数,然后对history的pushState和replaceState进行了一层封装,通过patchedUpdateState方法来提供。
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()) {
// fire an artificial popstate event once single-spa is started,
// so that single-spa applications know about routing that
// occurs in a different application
window.dispatchEvent(
createPopStateEvent(window.history.state, methodName)
);
} else {
// do not fire an artificial popstate event before single-spa is started,
// since no single-spa applications need to know about routing events
// outside of their own router.
reroute([]);
}
}
return result;
};
}
patchedUpdateState这个方法对旧路由和新路由进行一个对照,如果旧路由不等于新路由,表示子应用进行切换了,则执行微前端自定义的操作代码。
生命周期
子应用生命周期包含bootstrap,mount,unmount三个回调函数。在reroute中的toLoadPromise函数中加载应用的资源、定义加载完毕的回调函数。
....
// 设置app状态为未初始化,表示加载完了
app.status = NOT_BOOTSTRAPPED;
// 在app对象上挂载生命周期方法,每个方法都接收一个props作为参数,方法内部执行子应用导出的生命周期函数,并确保生命周期函数返回一个promise
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);
....
其中flattenFnArray,返回一个接受props作为参数的函数,这个函数负责执行子应用中的生命周期函数,并确保生命周期函数返回的结果为promise。
// lifecycles/lifecycle.helpers.js
/**
*
* @param {*} appOrParcel => window.singleSpa,子应用打包后的对象
* @param {*} lifecycle => 字符串,生命周期名称
*/
export function flattenFnArray(appOrParcel, lifecycle) {
// fns = fn or []
let fns = appOrParcel[lifecycle] || [];
// fns = [] or [fn]
fns = Array.isArray(fns) ? fns : [fns];
// 有些生命周期函数子应用可能不会设置,比如unload
if (fns.length === 0) {
fns = [() => Promise.resolve()];
}
const type = objectType(appOrParcel);
const name = toName(appOrParcel);
return function (props) {
// 这里最后返回了一个promise链,这个操作似乎没啥必要,因为不可能出现同名的生命周期函数,所以,这里将生命周期函数放数组,没太理解目的是啥
return fns.reduce((resultPromise, fn, index) => {
return resultPromise.then(() => {
// 执行生命周期函数,传递props给函数,并验证函数的返回结果,必须为promise
const thisPromise = fn(props);
return smellsLikeAPromise(thisPromise)
? thisPromise
: Promise.reject(
formatErrorMessage(
15,
__DEV__ &&
`Within ${type} ${name}, the lifecycle function ${lifecycle} at array index ${index} did not return a promise`,
type,
name,
lifecycle,
index
)
);
});
}, Promise.resolve());
};
}