single-spa 原理
核心 API
registerApplication
- single-spa 注册应用
- 执行 load 加载生命周期
- 仅执行应用的加载阶段
start
- 执行 load 加载阶段、boostrap 启动阶段、mount 挂载阶段
- 加载至挂载整套流程
single-spa 流程和生命周期图
reroute 方法
getAppChanges
根据 app 状态和启动规则找到待卸载、待加载、待挂载的 apps
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 };
}
loadApps 仅加载阶段
执行 registerApplication 时的流程
started 标识为 false
1.仅加载应用
2.加载完成后执行 callCapturedEventListeners
const loadPromises = appsToLoad.map(toLoadPromise);
Promise.all(loadPromise).then(() => { // ... })
toLoadPromise
1 状态 LOADING_SOURCE_CODE -> NOT_BOOTSTRAPPED
2 执行 app.loadApp 方法
3 为 app 增加 bootstrap、mount、unmount、unload 方法
function toLoadPromise() {
return Promise.resolve().then(() => {
app.status = LOADING_SOURCE_CODE
return (app.loadPromise = Promise.resolve()
.then(() => {
const loadPromise = app.loadApp(getProps(app));
return loadPromise.then((val) => {
app.status = NOT_BOOTSTRAPPED;
app.bootstrap = flattenFnArray(appOpts, "bootstrap");
app.mount = flattenFnArray(appOpts, "mount");
app.unmount = flattenFnArray(appOpts, "unmount");
app.unload = flattenFnArray(appOpts, "unload");
delete app.loadPromise;
return app
})
})
})
}
flattenFnArray
返回 Promise.resolve() 链, 通过 promise.then 逐个执行应用的生命周期函数(可为数组)
export function flattenFnArray(appOrParcel, lifecycle) {
let fns = app[life] || []
fns = Array.isArray(fns) ? fns : [fns]
if (fns.length === 0) fns = [() => Promise.resolve()]
return (props) => fns.reduce((promise, fn) => promise.then(() => fn(props)), Promise.resolve())
}
performAppChanges 加载、启动、挂载阶段
执行 start 时的流程
started 标识为 true
1.先卸载应用
2.加载应用
3.挂载应用
4.卸载应用完执行 callCapturedEventListeners
performAppChanges
// ..
// appsToLoad 需加载的应用,先加载再启动、挂载
const loadThenMountPromises = appsToLoad.map((app) => {
return toLoadPromise(app).then((app) =>
tryToBootstrapAndMount(app, unmountAllPromise)
);
});
// appsToMount 需启动挂载的 app
const mountPromises = appsToMount
.filter((appToMount) => appsToLoad.indexOf(appToMount) < 0)
.map((appToMount) => {
return tryToBootstrapAndMount(appToMount, unmountAllPromise);
});
return unmountAllPromise
.catch((err) => {
callAllEventListeners();
throw err;
})
.then(() => { // 卸载完毕 执行 callAllEventListeners,无需等待启动、挂载
callAllEventListeners();
return Promise.all(loadThenMountPromises.concat(mountPromises))
.catch((err) => {
pendingPromises.forEach((promise) => promise.reject(err));
throw err;
})
.then(finishUpAndReturn);
});
tryToBootstrapAndMount
在应用加载期间如果出现了某种延迟直接切换了另一个路由,那么之前路由的就不该被挂载。
所以进行了两次 shouldBeActive 判断
function tryToBootstrapAndMount(app, unmountAllPromise) {
if (shouldBeActive(app)) { // 判断 app 是否需激活
return toBootstrapPromise(app).then((app) => // 执行启动方法
unmountAllPromise.then(() => // 卸载完后再次判断是否激活,执行挂载函数
shouldBeActive(app) ? toMountPromise(app) : app
)
);
} else {
return unmountAllPromise.then(() => app);
}
}
navigation-events
1.捕获 hashchange、popstate 事件
2.为 history API: pushState、replaceState 打补丁
执行时机
应用启动时待 vue-router 等必须监听的事件执行后
目的
1.浏览器地址改变时先执行 rerouter 进行子应用切换 2.保存导航事件的监听函数,待子应用切换完毕后执行,保证执行顺序正确
捕获路由导航事件
const routingEventsListeningTo = ["hashchange", "popstate"]; // 需捕获的事件
function urlReroute() { // 传入 url 参数执行 reroute
reroute([], arguments);
}
window.addEventListener('hashchange', urlReroute);
window.addEventListener('popstate', urlReroute);
const originalAddEventListener = window.addEventListener;
const originalRemoveEventListener = window.removeEventListener;
window.addEventListener = function(eventName, fn) {
// capturedEventListeners 中没有 fn
if (routingEventsListeningTo.indexOf(eventName) >= 0 && !capturedEventListeners[eventName].some(listener => listener == fn)) {
capturedEventListeners[eventName].push(fn); // 存入 capturedEventListeners 中待 rerouter 中执行
return;
}
return originalAddEventListener.apply(this, arguments)
}
window.removeEventListener = function(eventName, fn) {
if (routingEventsListeningTo.indexOf(eventName) >= 0) {
capturedEventListeners[eventName] = capturedEventListeners[eventName].filter(l => l !== fn);
return;
}
return originalRemoveEventListener.apply(this, arguments)
}
history API 打补丁
当 pushState、replaceState history API 调用时是不会触发 popstate 事件的,需要进行一层处理
需要注意的是调用history.pushState()或history.replaceState()不会触发popstate事件。只有在做出浏览器动作时,才会触发该事件,如用户点击浏览器的回退按钮(或者在Javascript代码中调用history.back()或者history.forward()方法)
popstate_event
简单实现
function patchedUpdateState(updateState,methodName){
return function(){
const urlBefore = window.location.href;
updateState.apply(this,arguments); // 执行 pushState | replaceState
const urlAfter = window.location.href;
if(urlBefore !== urlAfter){ // 触发 popstate 事件
urlReroute(new PopStateEvent('popstate'));
}
}
}
window.history.pushState = patchedUpdateState(window.history.pushState,'pushState');
window.history.replaceState = patchedUpdateState(window.history.replaceState,'replaceState');
执行 listeners
等到 rerouter 中执行
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) { // 应用的事件错误不应该中断 single-spa 的执行。
setTimeout(() => {
throw e;
});
}
});
}
}
}
发表评论 (审核通过后显示评论):