我正在参加「掘金·启航计划」
快速链接
icestark源码解读(一):控制微应用加载与卸载的核心原理
前言
在第一章节中,有一个reroute函数,当监听的路由发生变化时,则会执行该函数。函数体内部主要是根据microApps变量里面储存的所有子应用配置的状态,来决定是加载子应用还是卸载子应用的。第二章节中我们了解到icestark加载子应用的一个过程,本章我们重点关注下,icestark卸载子应用的过程。
此处我们回顾下reroute函数, 下面代码中可以看到当子应用的状态是 MOUNTED(已挂载) 或者 LOADING_ASSETS(正在下载资源中)的时候,则去执行unmountMicroApp函数去卸载子应用。
/**
* 监听到路由的变化之后,比对路由前后是否发生变化,以此来控制子应用的加载与卸载
* @param url
* @param type
*/
export function reroute(url: string, type: RouteType | 'init' | 'popstate' | 'hashchange') {
const { pathname, query, hash } = urlParse(url, true); // 解析出url中的参数
if (lastUrl !== url) { // 前后路由进行比对
globalConfiguration.onRouteChange(url, pathname, query, hash, type); // 触发路由变化事件
const unmountApps = []; // 储存要卸载的子应用
const activeApps = []; // 储存要加载的子应用
// 获取全局中储存的所有子应用进行遍历,分出哪些子应用要卸载,哪些子应用要加载
getMicroApps().forEach((microApp: AppConfig) => {
const shouldBeActive = !!microApp.findActivePath(url);
if (shouldBeActive) {
activeApps.push(microApp);
} else {
unmountApps.push(microApp);
}
});
// 子应用开始被激活的回调
globalConfiguration.onActiveApps(activeApps);
// call captured event after app mounted
Promise.all(
// call unmount apps
unmountApps.map(async (unmountApp) => {
if (unmountApp.status === MOUNTED || unmountApp.status === LOADING_ASSETS) {
globalConfiguration.onAppLeave(unmountApp); // 子应用卸载前的回调
}
// 根据子应用唯一标识去卸载子应用
await unmountMicroApp(unmountApp.name);
}).concat(activeApps.map(async (activeApp) => {
if (activeApp.status !== MOUNTED) {
globalConfiguration.onAppEnter(activeApp); // 子应用渲染前的回调
}
// 加载子应用
await createMicroApp(activeApp);
})),
).then(() => {
// 子应用发生了卸载与加载,说明路由变化了,故在这里要去执行下开发者自己对popostate以及hashchange的监听事件
callCapturedEventListeners();
});
}
lastUrl = url;
}
unmountMicroApp 函数
/**
* 卸载子应用----此时子应用的资源都还在
* @param appName 子应用的唯一标识
*/
export async function unmountMicroApp(appName: string) {
const appConfig = getAppConfig(appName); // 获取子应用配置
if (appConfig && (appConfig.status === MOUNTED || appConfig.status === LOADING_ASSETS || appConfig.status === NOT_MOUNTED)) {
// 如果子应用没有设置缓存,则直接移除子应用的资源
const { shouldAssetsRemove } = getAppConfig(appName)?.configuration || globalConfiguration;
// 将子应用的资源全部从document文档上面移除掉
const removedAssets = emptyAssets(shouldAssetsRemove, !appConfig.cached && appConfig.name);
/**
* Since es module natively imported twice may never excute twice. https://dmitripavlutin.com/javascript-module-import-twice/
* Cache all child's removed assets, then append them when app is mounted for the second time.
* Only cache removed assets when app's loadScriptMode is import which may not cause break change.
*/
if (appConfig.loadScriptMode === 'import') {
importCachedAssets[appName] = removedAssets;
}
updateAppConfig(appName, { status: UNMOUNTED }); // 更新子应用的状态为已卸载
if (!appConfig.cached && appConfig.appSandbox) {
appConfig.appSandbox.clear(); // 移除子应用的沙箱
appConfig.appSandbox = null;
}
if (appConfig.unmount) { // 执行子应用卸载的生命周期函数
await appConfig.unmount({ container: appConfig.container, customProps: appConfig.props });
}
}
}
该函数接收子应用的唯一标识作为参数,从microApps中获取对应子应用的配置信息,判断当子应用的状态为 MOUNTED LOADING_ASSETS NOT_MOUNTED三者中的任一状态时,则去执行 emptyAssets函数,将子应用的静态资源,从主应用的document中全部移除。移除完成之后,会执行子应用配置的卸载生命周期函数。
- 特点:调用只是该函数,子应用只是从主应用的document中移除,其自身的静态资源并没有被移除,并且仍然存在于
microApps中
emptyAssets 函数
/**
* 移除子应用的资源
* @returns Removed assets.
*/
export function emptyAssets(
shouldRemove: (
assetUrl: string,
element?: HTMLElement | HTMLLinkElement | HTMLStyleElement | HTMLScriptElement,
) => boolean,
cacheKey: string|boolean,
) {
const removedAssets: HTMLElement[] = []; // 记录已经被移除的子应用静态资源
// remove extra assets
// 带有icestark=static的属性是主应用的静态资源,此处利用这个特性可以直接获取到document中子应用的静态资源
// 获取子应用的全部style标签
const styleList: NodeListOf<HTMLStyleElement> = document.querySelectorAll(
`style:not([${PREFIX}=${STATIC}])`,
);
// 遍历子应用的style标签进行一个个移除操作
styleList.forEach((style) => {
if (shouldRemove(null, style) && checkCacheKey(style, cacheKey)) {
style.parentNode.removeChild(style);
removedAssets.push(style);
}
});
// 获取所有的子应用link标签资源
const linkList: NodeListOf<HTMLLIElement> = document.querySelectorAll(
`link:not([${PREFIX}=${STATIC}])`,
);
// 遍历子应用的link标签进行一个个移除操作
linkList.forEach((link) => {
if (shouldRemove(link.getAttribute('href'), link) && checkCacheKey(link, cacheKey)) {
link.parentNode.removeChild(link);
removedAssets.push(link);
}
});
// 获取所有的子应用script标签资源
const jsExtraList: NodeListOf<HTMLScriptElement> = document.querySelectorAll(
`script:not([${PREFIX}=${STATIC}])`,
);
// 遍历子应用的script标签进行一个个移除操作
jsExtraList.forEach((js) => {
if (shouldRemove(js.getAttribute('src'), js) && checkCacheKey(js, cacheKey)) {
js.parentNode.removeChild(js);
removedAssets.push(js);
}
});
return removedAssets; // 返回被移除的子应用静态资源
}
该函数主要是利用在document中,主应用和子应用的静态资源的唯一标识不同,来去获取子应用的js与css,从而将其从document中移除掉。
主应用的style、link以及script标签上会带有icestark=static的标识。
主应用上面的这个标识是在AppRouter组件的componentDidMount生命周期里面的 start 函数中添加的。在子应用加载之前,给主应用添加icestark=static的标识。避免与子应用的静态资源混淆。
unloadMicroApp 函数
/**
* 卸载子应用的同时,还会把子应用的静态资源从配置上删除掉
* @param appName
*/
export async function unloadMicroApp(appName: string) {
const appConfig = getAppConfig(appName);
if (appConfig) {
unmountMicroApp(appName);
delete appConfig.mount;
delete appConfig.unmount;
delete appConfig.appAssets; // 删除子应用的静态资源
updateAppConfig(appName, { status: NOT_LOADED }); // 更新子应用的状态为未下载资源状态
} else {
log.error(
formatErrMessage(
ErrorCode.CANNOT_FIND_APP,
isDev && 'Can not find app {0} when call {1}',
appName,
'unloadMicroApp',
),
);
}
}
该函数是主要是在卸载子应用之后,将子应用的静态资源从子应用自身的配置上删除,并将子应用的状态设置为NOT_LOADED未下载静态资源状态。
调用时机:
- 在AppRouter组件的
componentWillUnmount时调用 - 在AppRoute组件的
componentDidUpdate中,当前后路径不一样的时候,那就说明切换子应用了,要先把已经加载的子应用移除掉。
- 特点:调用该函数,子应用不仅从主应用的document中移除,同时其自身的静态资源也会被移除,但是仍然存在于
microApps中
removeMicroApp 函数
/**
* 将子应用从全局变量microApps中移除掉
* @param appName
*/
export function removeMicroApp(appName: string) {
const appIndex = getAppNames().indexOf(appName); // 拿到子应用在microApps数组中的索引
if (appIndex > -1) {
// unload micro app in case of app is mounted
unloadMicroApp(appName); // 为了防止子应用处于已挂载状态,要先卸载子应用
microApps.splice(appIndex, 1); // 从microApps中移除该子应用的配置
} else {
log.error(
formatErrMessage(
ErrorCode.CANNOT_FIND_APP,
isDev && 'Can not find app {0} when call {1}',
appName,
'removeMicroApp',
),
);
}
}
该函数的作用是在卸载子应用之后,将子应用的配置从microApps中直接删除掉。
调用时机:目前在源码中没有看到有主动调用该函数的地方,根据官方文档来看是作者开放出来,允许开发者自行控制移除子应用。
- 特点:调用该函数,子应用不仅从主应用的document中移除,同时其自身的静态资源也会被移除,并且也会从
microApps中移除掉。
clearMicroApps
/**
* 清空子应用
*/
export function clearMicroApps() {
getAppNames().forEach((name) => {
unloadMicroApp(name);
});
microApps = [];
}
该函数的目的是删除所有的子应用,并将microApps置为空数组
调用时机:在AppRouter组件的componentWillUnmount时调用
- 特点:调用该函数,就证明不加载任何子应用。和子应用有关的配置或者资源都会被重置和移除。
总结
本章阅读起来比较轻松,从源码上去看,子应用的卸载无非就是将子应用所有的静态资源从主应用的document文档上全部移除即可。并且根据不同的调用时机来决定是否将子应用的配置信息从microApps中移除掉。
在下一章节中我们将会详细看下应用间的通信过程