微前端icestark源码解读-子应用卸载详解(三)

500 阅读6分钟

我正在参加「掘金·启航计划」

快速链接

icestark源码解读(一):控制微应用加载与卸载的核心原理

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未下载静态资源状态。 调用时机:

  1. 在AppRouter组件的componentWillUnmount时调用
  2. 在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中移除掉。

在下一章节中我们将会详细看下应用间的通信过程