微前端06 : single-spa的注册机制

1,032 阅读7分钟

推荐阅读

Vue3源码

微前端源码

React18源码

/******************************************************************/
/*****************     欢迎关注微信公众号:杨艺韬     *****************/
/******************************************************************/

在前面的5篇文章中,我们对乾坤进行了比较深入的介绍,但是无论怎么深入都是不全面的,甚至某种意义上来讲乾坤并不是一个微前端框架,single-spa才是,乾坤只是一个对single-spa进行增强的一个方案。接下来的几篇文章主要对single-spa的一些核心机制和功能从源码层面对其进行分析。本文主要分析single-spa的注册机制。

registerApplication的主要逻辑

我先来看single-spa暴露的注册函数的主要逻辑:

// 代码片段1
export function registerApplication(
  appNameOrConfig,
  appOrLoadApp,
  activeWhen,
  customProps
) {
  // 关键点1
  const registration = sanitizeArguments(
    appNameOrConfig,
    appOrLoadApp,
    activeWhen,
    customProps
  );
  // 这里省去许多逻辑...
  // 关键点2
  apps.push(
    assign(
      {
        loadErrorTime: null,
        status: NOT_LOADED,
        parcels: {},
        devtools: {
          overlays: {
            options: {},
            selectors: [],
          },
        },
      },
      registration
    )
  );

  if (isInBrowser) {
    // 关键点3
    ensureJQuerySupport();
    // 关键点4
    reroute();
  }
}

从整体上看,registerApplication一共做了4件比较重要的事情。 首先,是对参数进行处理,对应代码片段1中的关键点1,参数处理函数sanitizeArguments有几十行代码,具体怎么处理的,逻辑相对简单,这里就不描述了。对参数的合理处理,给用户提供了更多的灵活性,可以通过不同形式来传递参数,然后将不同格式的参数处理成统一格式。同时,对参数进行了校验。这种写法很常见,在我们日常编程中可以借鉴。 其次,是将微应用保存到数组apps中,apps是一个全局变量,会存放所有的注册过的微应用。这个数组很重要,微应用的各种状态都保存在这里,实际上single-spa的核心工作就是对apps中保存的微应用进行管理和控制。 再次,是调用ensureJQuerySupport函数对JQuery的某些监听事件进行拦截,下文中进行详述。 最后,是调用reroute函数,主要是加载微应用,下文中会进行详述。

ensureJQuerySupport

我看先看ensureJQuerySupport函数的逻辑:

// 代码片段2
export function ensureJQuerySupport(jQuery = window.jQuery) {
    // 这里省略一些代码...
    const originalJQueryOn = jQuery.fn.on;
    const originalJQueryOff = jQuery.fn.off;

    jQuery.fn.on = function (eventString, fn) {
      return captureRoutingEvents.call(
        this,
        originalJQueryOn,
        window.addEventListener,
        eventString,
        fn,
        arguments
      );
    };

    jQuery.fn.off = function (eventString, fn) {
      // 这里省略许多代码... 与jQeury.fn.on类似,不在此赘述
    };
    // 这里省略一些代码...
}

代码片段2中省略了许多逻辑判断,但核心功能可以理解为做了两件事。是保存了jQuery的事件监听和事件取消函数。是对jQuery的事件监听函数进行了拦截,具体怎么拦截的,让我们进入captureRoutingEvents函数中一探究竟。

captureRoutingEvents

// 代码片段3
function captureRoutingEvents(
  originalJQueryFunction,
  nativeFunctionToCall,
  eventString,
  fn,
  originalArgs
) {
  if (typeof eventString !== "string") {
    return originalJQueryFunction.apply(this, originalArgs);
  }
  const eventNames = eventString.split(/\s+/);
  eventNames.forEach((eventName) => {
    if (routingEventsListeningTo.indexOf(eventName) >= 0) {
      // 关键点1
      nativeFunctionToCall(eventName, fn);
      eventString = eventString.replace(eventName, "");
    }
  });

  if (eventString.trim() === "") {
    // 关键点2
    return this;
  } else {
    return originalJQueryFunction.apply(this, originalArgs);
  }
}

还记得我们上文保存的originalJQueryFunction函数吗,在函数captureRoutingEvents有了体现。可以概括为该函数在某些条件下执行jQuery.fn.on未被重写前的逻辑。否则就返回this。相对于代码片段2jQuery.fn.on中的调用,关键点1处的代码,相当于执行了window.addEventListener("hashchange"|"popstate",()=>{})。当然里面利用了些条件逻辑,如果监听的事件不仅仅进只有hashchange、popstate两个事件,则继续调用jQuery.fn.on未被重写前的逻辑进行事件的监听。由于我没研究过jQeury,调用被重写前的jQuery.fn.on函数会发生什么,并不太清楚,不过对于理解single-spa而言,能理解上文呈现的逻辑就足够了,对于关键点2处返回this有什么用途,后续遇见该逻辑再进行剖析。

reroute

聊完了ensureJQuerySupport,是时候探索reroute了。reroute函数有将近300行代码,对其中次要逻辑进行删减,且只留下和注册相关的逻辑,如下所示:

// 代码片段4
export function reroute(pendingPromises = [], eventArguments) {
  // 这里省略掉许多代码...
  const {
    appsToUnload,
    appsToUnmount,
    appsToLoad,
    appsToMount,
  } = getAppChanges();
  // 这里省略掉许多代码...
  return loadApps();
  
  // 这里省略掉许多代码...

  function loadApps() {
    return Promise.resolve().then(() => {
      const loadPromises = appsToLoad.map(toLoadPromise);

      return (
        Promise.all(loadPromises)
          .then(callAllEventListeners)
          .then(() => [])
          .catch((err) => {
            callAllEventListeners();
            throw err;
          })
      );
    });
  }
  // 这里省略许多代码...
}

代码片段4中留下了reroute函数的核心逻辑,做了两件事情,是获取处于各种状态的微应用。是返回函数loadApps的执行结果。而loadApps中做了一件重要的事情,就是调用了这样一行代码const loadPromises = appsToLoad.map(toLoadPromise);,我们不难知道appsToLoad代码着需要加载的微应用,而toLoadPromise主要完成什么功能呢?请看下文讲解。

toLoadPromise

// 代码片段5,为了精简逻辑,除了省略一些代码,还做了微调
export function toLoadPromise(app) {
  return Promise.resolve().then(() => {
    // 这里省略许多代码...
    return (app.loadPromise = Promise.resolve()
      .then(() => {
        const loadPromise = app.loadApp(getProps(app));
        // 这里省略许多代码...
        return loadPromise.then((val) => {
          // 这里省略许多代码...
          app.status = NOT_BOOTSTRAPPED;
          app.bootstrap = flattenFnArray(appOpts, "bootstrap");
          app.mount = flattenFnArray(appOpts, "mount");
          app.unmount = flattenFnArray(appOpts, "unmount");
          app.unload = flattenFnArray(appOpts, "unload");
          app.timeouts = ensureValidAppTimeouts(appOpts.timeouts);

          delete app.loadPromise;

          return app;
        });
      })
      .catch((err) => {
        // 这里省略许多代码...
      }));
  };
}

代码片段5中原本有100多行代码,对其进行精简,我们发现核心逻辑其实只做了三件事,是调用子应用传入的加载微应用的方法。是保存微应用的各种状态。是规范发子应用暴露的方法。其实看源码就要抓住核心逻辑,其他疑惑都会迎刃而解,忌讳胡子眉毛一把抓,逐行阅读,最终会把自己陷在里面,不知道代码究竟想要表达什么,进而丧失了继续读下去的热情。好了,关于single-spa的注册机制今天就分析到这里,请朋友们期待更多关于single-spa的文章。

欢迎关注我的微信公众号:杨艺韬,可以获取最新动态。