相关链接
本文是最近分析single-spa中的一篇,全部的文章如下:
- 深入分析single-spa——导航事件与reroute
- 深入分析single-spa——启动与应用管理
- 深入分析single-spa——事件机制
- 其他关于模块机制、生命周期、微前端类型的深入分析正在进行中
single-spa中的事件
在之前关于navigation-events和reroute的分享中,其实有很多的事件触发,在这里就single-spa中的事件作进一步的展开介绍。在路由导航的不同阶段以及应用的不同状态处理中,single-spa会派发不同的事件;通过对这些事件的理解和处理,我们可以增加一些自定义的功能。
在single-spa中,事件大致分为如下两类:
原生事件
在navigation-events中,single-spa实现了对于浏览器的路由导航事件的监听:
- popstate
- hashchange
- pushstate
- replacestate
自定义的popstate事件
single-spa对于原生的popstate
事件增加了一些自定义的属性:
- singleSpa:标示该popstate事件由single-spa触发
- singleSpaTrigger:标示该popstate事件触发的原始方法名
function createPopStateEvent(state, originalMethodName) {
let evt;
try {
evt = new PopStateEvent("popstate", { state });
} catch (err) {
evt = document.createEvent("PopStateEvent");
evt.initPopStateEvent("popstate", false, false, state);
}
// 给原生popstate事件增加singleSpa和singleSpaTrigger属性
evt.singleSpa = true;
evt.singleSpaTrigger = originalMethodName;
return evt;
}
触发时机
该方法会在如下方法调用的时候被触发:
- history.pushState
- history.replaceState
- triggerAppChange等可能触发callCapturedEventListeners的single-spa自定义方法
我们可以监听popstate
事件,来判断该事件是否由single-spa触发:
// 这是官方文档中的一段代码
window.addEventListener('popstate', evt => {
if (evt.singleSpa) {
console.log(
'This event was fired by single-spa to forcibly trigger a re-render',
);
console.log(evt.singleSpaTrigger); // pushState | replaceState
} else {
console.log('This event was fired by native browser behavior');
}
});
自定义事件(Custom Events)
在核心方法——reroute的执行中,single-spa会触发一系列的自定义事件,其固定的事件名称格式为:single-spa:event-name
。对于这些自定义事件的入参,single-spa通过getCustomEventDetail方法进行封装,提供了统一的事件接口。
- 事件封装与派发
single-spa对于自定义事件的封装,是基于一个名为custom-event的库实现的,这个库实现了跨浏览器的自定义事件支持;创建的事件,则通过window.dispatchEvent
来进行派发,类似:
window.dispatchEvent('single-spa:event-name', new CustomEvent(/* */))
关于Custom-Event的更多信息,可以参考MDN CustomEvent.
- 事件监听
这些事件,我们都可以当做浏览器的原生事件,通过事件监听的方式进行处理:
window.addEventListener('single-spa:event-name', /* 回调函数,入参即为custom event detail */)
事件列表
事件顺序 | 事件名称 | 触发时机 |
---|---|---|
1 | single-spa:before-app-change / single-spa:before-no-app-change | 进入reroute方法时,根据发生改变的应用数量触发;与事件顺序6对应 |
2 | single-spa:before-routing-event | 每次 reroute 开始一定会发生,与事件7对应 |
3 | single-spa:before-mount-routing-event | url 发行改变后,旧的应用卸载完毕后,触发该事件,表示后续要开始加载应用 |
4 | single-spa:before-first-mount | 在某个应用第一次 mount 应用之前触发该事件;该事件只会触发一次,定义在mount.js 中 |
5 | single-spa:first-mount | 在某个应用第一次 mount 应用之后触发该事件;该事件只会触发一次,该定义在mount.js 中 |
6 | single-spa:app-change / single-spa:no-app-change | 与事件顺序1对应 |
7 | single-spa:routing-event | 与事件 2 对应,发生在 reroute 结束 |
自定义事件触发流程
获取事件详情——getCustomEventDetail
single-spa中,通过该方法实现了对于Custom Events的事件详情的统一封装,具体实现如下:
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: {
// 应用状态哈希,key为应用名称,value为应用状态
newAppStatuses,
// 各状态对应的应用哈希,key为应用状态,value为对应状态的应用列表
appsByNewStatus,
// 状态改变的应用列表数量
totalAppChanges: appsThatChanged.length,
// 初始事件信息
originalEvent: eventArguments?.[0],
// 原本的url地址
oldUrl,
// 导航到的url地址
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);
}
}
自定义事件的使用
实例场景
- 取消导航
在single-spa:before-routing-event
中,传递了cancelNavigation
方法作为该事件的入参,调用该方法即可取消该次routing:
// route.js
window.dispatchEvent(
new CustomEvent(
"single-spa:before-routing-event",
getCustomEventDetail(true, { cancelNavigation })
)
);
function cancelNavigation() {
navigationIsCanceled = true;
}
// 这是官方文档中的一段代码;如果监听该事件,并且调用了cancelNavigation,就可以取消这次导航
window.addEventListener(
'single-spa:before-routing-event',
({ detail: { oldUrl, newUrl, cancelNavigation } }) => {
if (
new URL(oldUrl).pathname === '/route1' &&
new URL(newUrl).pathname === '/route2'
) {
cancelNavigation();
}
},
);
// 调用cancelNavigation会设置navigationIsCanceled = false,从而结束这次routing, 并导航回到之前的url
if (navigationIsCanceled) {
window.dispatchEvent(
new CustomEvent(
"single-spa:before-mount-routing-event",
getCustomEventDetail(true)
)
);
finishUpAndReturn();
navigateToUrl(oldUrl);
return;
}
在基于single-spa封装的知名微前端框架——qiankun中,也基于一些事件做了自定义的处理:
- 设置默认mount的微前端应用
// 参考https://qiankun.umijs.org/zh/api#setdefaultmountappapplink
export function setDefaultMountApp(defaultAppLink: string) {
window.addEventListener('single-spa:no-app-change', function listener() {
const mountedApps = getMountedApps();
if (!mountedApps.length) {
navigateToUrl(defaultAppLink);
}
window.removeEventListener('single-spa:no-app-change', listener);
});
}
- 如果要在微前端应用加载后做监测/埋点,或者需要对微前端实行prefetch
// 参考https://qiankun.umijs.org/zh/api#runafterfirstmountedeffect
export function runAfterFirstMounted(effect: () => void) {
// can not use addEventListener once option for ie support
window.addEventListener('single-spa:first-mount', function listener() {
if (process.env.NODE_ENV === 'development') {
console.timeEnd(firstMountLogLabel);
}
effect();
window.removeEventListener('single-spa:first-mount', listener);
});
}
// 如果在start的参数中配置了prefetch, 或者手动调用prefetchApps等方法,则会触发prefetchAfterFirstMounted
function prefetchAfterFirstMounted(apps: AppMetadata[], opts?: ImportEntryOpts): void {
window.addEventListener('single-spa:first-mount', function listener() {
const notLoadedApps = apps.filter((app) => getAppStatus(app.name) === NOT_LOADED);
if (process.env.NODE_ENV === 'development') {
const mountedApps = getMountedApps();
console.log(`[qiankun] prefetch starting after ${mountedApps} mounted...`, notLoadedApps);
}
notLoadedApps.forEach(({ entry }) => prefetch(entry, opts));
window.removeEventListener('single-spa:first-mount', listener);
});
}