深入分析single-spa——事件机制

1,469 阅读5分钟

相关链接

本文是最近分析single-spa中的一篇,全部的文章如下:

  1. 深入分析single-spa——导航事件与reroute
  2. 深入分析single-spa——启动与应用管理
  3. 深入分析single-spa——事件机制
  4. 其他关于模块机制、生命周期、微前端类型的深入分析正在进行中

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触发流程.png

我们可以监听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 */)

事件列表

事件顺序事件名称触发时机
1single-spa:before-app-change / single-spa:before-no-app-change进入reroute方法时,根据发生改变的应用数量触发;与事件顺序6对应
2single-spa:before-routing-event每次 reroute 开始一定会发生,与事件7对应
3single-spa:before-mount-routing-eventurl 发行改变后,旧的应用卸载完毕后,触发该事件,表示后续要开始加载应用
4single-spa:before-first-mount在某个应用第一次 mount 应用之前触发该事件;该事件只会触发一次,定义在mount.js
5single-spa:first-mount在某个应用第一次 mount 应用之后触发该事件;该事件只会触发一次,该定义在mount.js
6single-spa:app-change / single-spa:no-app-change与事件顺序1对应
7single-spa:routing-event与事件 2 对应,发生在 reroute 结束

自定义事件触发流程

Custom Events流程.png

获取事件详情——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);
  });
}

参考资料

Events

custom-event

qiankun