single-spa 源码解析

single-spa 源码解析

single-spa是一种微前端的实现方案。阿里的qiankun其实是基于这个项目做了二次开发,其实是做了个拓展,提供了html解析与js沙盒两个功能。本文从single-spa的代码实现角度解析一下它的实现原理。

前提假设

single-spa首先要求每个子应用需要提供bootstrap,mount,unmount,update这四个方法,这样每个子应用从single-spa的角度而言,就是一个具有四个方法的对象。

实现的大体原理

single-spa是基于路由来决定每个子应用的加载与卸载。所以,它是通过拦截与监听浏览器的路由事件做到的。具体的可以分为以下两个方面。

  • 监听浏览器的popstate,hashchange这两个事件,同时它拦截了其他应用对于这两个事件的监听。
  • 它也对于改变路由的两个API也进行了更换window.history.pushState(),window.history.replaceState()。这样,路由变化的事件被single-spa接管,改变路由的API也被接管。所以,整个路由都被single-spa接管。这个是single-spa工作的核心。下面是源码的片段
export const routingEventsListeningTo = ["hashchange", "popstate"];
 // We will trigger an app change for any routing events.
  window.addEventListener("hashchange", urlReroute);
  window.addEventListener("popstate", urlReroute);

  // Monkeypatch addEventListener so that we can ensure correct timing
  const originalAddEventListener = window.addEventListener;
  const originalRemoveEventListener = window.removeEventListener;
  window.addEventListener = function (eventName, fn) {
    if (typeof fn === "function") {
      if (
        routingEventsListeningTo.indexOf(eventName) >= 0 &&
        !find(capturedEventListeners[eventName], (listener) => listener === fn)
      ) {
        capturedEventListeners[eventName].push(fn);
        return;
      }
    }

    return originalAddEventListener.apply(this, arguments);
  };

  window.removeEventListener = function (eventName, listenerFn) {
    if (typeof listenerFn === "function") {
      if (routingEventsListeningTo.indexOf(eventName) >= 0) {
        capturedEventListeners[eventName] = capturedEventListeners[
          eventName
        ].filter((fn) => fn !== listenerFn);
        return;
      }
    }

    return originalRemoveEventListener.apply(this, arguments);
  };

  window.history.pushState = patchedUpdateState(
    window.history.pushState,
    "pushState"
  );
  window.history.replaceState = patchedUpdateState(
    window.history.replaceState,
    "replaceState"
  );

工作的核心

single-spa在接管路由后,当路由发生变化,其实也就是popstate,hashchange这两个事件发生的时候。single-spa会去计算更新每个app的状态。代码里面叫reroute()。这个方法的逻辑就是计算每个app的状态,然后,首先卸载inactive的app.接着挂载需要激活的app.同时会发布一些生命周期的事件以及原始的popstate,hashchange事件。下面是最主要的代码片段。忽略了部分细节

 function performAppChanges() {
    return Promise.resolve().then(() => {
      // https://github.com/single-spa/single-spa/issues/545
      window.dispatchEvent(
        new CustomEvent(
          appsThatChanged.length === 0
            ? "single-spa:before-no-app-change"
            : "single-spa:before-app-change",
          getCustomEventDetail(true)
        )
      );

      window.dispatchEvent(
        new CustomEvent(
          "single-spa:before-routing-event",
          getCustomEventDetail(true, { cancelNavigation })
        )
      );
    // 在这个时间点,我们是有可能取消这个事件的
      if (navigationIsCanceled) {
        window.dispatchEvent(
          new CustomEvent(
            "single-spa:before-mount-routing-event",
            getCustomEventDetail(true)
          )
        );
        finishUpAndReturn();
        navigateToUrl(oldUrl);
        return;
      }
    // 首先卸载不用的app,其实就调用app.unload 方法
      const unloadPromises = appsToUnload.map(toUnloadPromise);

    // 摘出不用的app, 调用 app.unmount方法
      const unmountUnloadPromises = appsToUnmount
        .map(toUnmountPromise)
        .map((unmountPromise) => unmountPromise.then(toUnloadPromise));

      const allUnmountPromises = unmountUnloadPromises.concat(unloadPromises);

      const unmountAllPromise = Promise.all(allUnmountPromises);
    // 完成了不用的app 的摘除与卸载, 发布对应的事件
      unmountAllPromise.then(() => {
        window.dispatchEvent(
          new CustomEvent(
            "single-spa:before-mount-routing-event",
            getCustomEventDetail(true)
          )
        );
      });
    //加载使用的app,加载完后调用app.bootstrap(),这些都是异步的
      /* We load and bootstrap apps while other apps are unmounting, but we
       * wait to mount the app until all apps are finishing unmounting
       */
      const loadThenMountPromises = appsToLoad.map((app) => {
        return toLoadPromise(app).then((app) =>
          tryToBootstrapAndMount(app, unmountAllPromise)
        );
      });

    // bootstrap 并且mount 应该active的app
      /* These are the apps that are already bootstrapped and just need
       * to be mounted. They each wait for all unmounting apps to finish up
       * before they mount.
       */
      const mountPromises = appsToMount
        .filter((appToMount) => appsToLoad.indexOf(appToMount) < 0)
        .map((appToMount) => {
          return tryToBootstrapAndMount(appToMount, unmountAllPromise);
        });
    // 当不用的应用卸载后,我们会通知所有监听popstate,hashchange的监听者。
    // 当所有的应用加载后,我们会发布single-spa:app-change, single-spa:routing-event,
      return unmountAllPromise
        .catch((err) => {
          callAllEventListeners();
          throw err;
        })
        .then(() => {
          /* Now that the apps that needed to be unmounted are unmounted, their DOM navigation
           * events (like hashchange or popstate) should have been cleaned up. So it's safe
           * to let the remaining captured event listeners to handle about the DOM event.
           */
          callAllEventListeners();

          return Promise.all(loadThenMountPromises.concat(mountPromises))
            .catch((err) => {
              pendingPromises.forEach((promise) => promise.reject(err));
              throw err;
            })
            .then(finishUpAndReturn);
        });
    });
  }

上面就是single-spa工作的最核心的原理。

上面的代码就是针对registerApplicaiton(),start()以后的single-spa的工作流程。是不是非常简单。不过这个思想确实非常的新颖。不过这个里面有个问题,就是它要求所有的应用自己抽象成那四个方法。而且所有的应用都在同一个js的上下文中执行,所有很容易会出现互相打架的情况。qiankun 提供的js沙盒,就是为了这个准备的。对应用之间做了隔离。

posted @ 2024-08-01 16:03  kongshu  阅读(89)  评论(0编辑  收藏  举报