以前看过了源码, single-spa 简单说就是规定了挂载、更新、卸载等声明周期,然后在路由变化的时候,进行注册app的卸载,更新、或者挂载等。
每个app都有注册的时候都有自己的声明函数,mout、update、unmout等,同时activeWhen用于检测 app 是否应该激活
export function isActive(app) {
return app.status === MOUNTED;
}
export function shouldBeActive(app) {
try {
return app.activeWhen(window.location);
} catch (err) {
handleAppError(err, app, SKIP_BECAUSE_BROKEN);
return false;
}
}
globalTimeoutConfig 用于配置五个声明周期时,执行app 声明周期时的一些配置
const globalTimeoutConfig = {
bootstrap: {
millis: 4000,
dieOnTimeout: false,
warningMillis: defaultWarningMillis,
},
mount: {
millis: 3000,
dieOnTimeout: false,
warningMillis: defaultWarningMillis,
},
unmount: {
millis: 3000,
dieOnTimeout: false,
warningMillis: defaultWarningMillis,
},
unload: {
millis: 3000,
dieOnTimeout: false,
warningMillis: defaultWarningMillis,
},
update: {
millis: 3000,
dieOnTimeout: false,
warningMillis: defaultWarningMillis,
},
};
dieOnTimeout 在下面就可以看出,用于是否生命周期函数超时时,要reject 还是静默处理
function maybeTimingOut(shouldError) {
if (!finished) {
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) {
const numWarnings = shouldError;
const numMillis = numWarnings * warningPeriod;
console.warn(errMsg);
if (numMillis + warningPeriod < timeoutConfig.millis) {
setTimeout(() => maybeTimingOut(numWarnings + 1), warningPeriod);
}
}
}
}
对于路由劫持,hashchange 和 popstate 会触发 urlReroute,触发apps的更新卸载挂载等。可以看劫持了addEventListener方法,对于hashchange 和 popstate 方法,优先执行urlReroute,然后存到capturedEventListeners对应的数组里面,这样其他库或者人为的监听,要等app mounted 或者unmounted。
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);
};
而对于pushState和replaceState,我们也进行了劫持,我们知道默认这两个不会触发popstate 或者引起其他app的变化,所以我们需要进行打补丁
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;
}
}
可以看到patchedUpdateState 打补丁方法,主要是为了触发其他app更新,如果因为开始了,则认为触发 popState 监听,否则直接调用 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()) {
// 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;
};
}
不同生命周期函数,挑bootstrap来说,主要也是调用注册的app对应的bootstrap 方法,当然是当过我们上面讲过的 reasonableTime 方法调用,里面有超时检测等方法,当然这里还要设置app的状态
export function toBootstrapPromise(appOrParcel, hardFail) {
return Promise.resolve().then(() => {
if (appOrParcel.status !== NOT_BOOTSTRAPPED) {
return appOrParcel;
}
appOrParcel.status = BOOTSTRAPPING;
if (!appOrParcel.bootstrap) {
// Default implementation of bootstrap
return Promise.resolve().then(successfulBootstrap);
}
return reasonableTime(appOrParcel, "bootstrap")
.then(successfulBootstrap)
.catch((err) => {
if (hardFail) {
throw transformErr(err, appOrParcel, SKIP_BECAUSE_BROKEN);
} else {
handleAppError(err, appOrParcel, SKIP_BECAUSE_BROKEN);
return appOrParcel;
}
});
});
function successfulBootstrap() {
appOrParcel.status = NOT_MOUNTED;
return appOrParcel;
}
}
再讲一个获取app变化的方法getAppChanges, 我们在不同生命周期给app设置周期是有目的的,类似LOAD_ERROR 的状态,如果 appShouldBeActive 而且上次加载时间超过200ms了,我们把 app 加入 appsToLoad,这次加载了它。再比如,如果app状态是 mounted,但是appShouldBeActive 为false,说明我们应该卸载它,把 app 加入 appsToUnmount 数组
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 };
}