single-spa 原理

核心 API

registerApplication

  1. single-spa 注册应用
  2. 执行 load 加载生命周期
  3. 仅执行应用的加载阶段

start

  1. 执行 load 加载阶段、boostrap 启动阶段、mount 挂载阶段
  2. 加载至挂载整套流程

single-spa 流程和生命周期图

single-spa 流程图

reroute 方法

getAppChanges

根据 app 状态和启动规则找到待卸载、待加载、待挂载的 apps

export function getAppChanges() {
  const appsToUnload = [],
    appsToUnmount = [],
    appsToLoad = [],
    appsToMount = [];

  // We re-attempt to download applications in LOAD_ERROR after a timeout of 200 milliseconds
  const currentTime = new Date().getTime();

  apps.forEach((app) => {
    const appShouldBeActive =
      app.status !== SKIP_BECAUSE_BROKEN && shouldBeActive(app);

    switch (app.status) {
      case LOAD_ERROR:
        if (appShouldBeActive && currentTime - app.loadErrorTime >= 200) {
          appsToLoad.push(app);
        }
        break;
      case NOT_LOADED:
      case LOADING_SOURCE_CODE:
        if (appShouldBeActive) {
          appsToLoad.push(app);
        }
        break;
      case NOT_BOOTSTRAPPED:
      case NOT_MOUNTED:
        if (!appShouldBeActive && getAppUnloadInfo(toName(app))) {
          appsToUnload.push(app);
        } else if (appShouldBeActive) {
          appsToMount.push(app);
        }
        break;
      case MOUNTED:
        if (!appShouldBeActive) {
          appsToUnmount.push(app);
        }
        break;
      // all other statuses are ignored
    }
  });

  return { appsToUnload, appsToUnmount, appsToLoad, appsToMount };
}

loadApps 仅加载阶段

执行 registerApplication 时的流程
started 标识为 false
1.仅加载应用
2.加载完成后执行 callCapturedEventListeners

const loadPromises = appsToLoad.map(toLoadPromise);
Promise.all(loadPromise).then(() => { // ... })

toLoadPromise
1 状态 LOADING_SOURCE_CODE -> NOT_BOOTSTRAPPED
2 执行 app.loadApp 方法
3 为 app 增加 bootstrap、mount、unmount、unload 方法

function toLoadPromise() {
    return Promise.resolve().then(() => {
        app.status = LOADING_SOURCE_CODE

        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");
                  
                  delete app.loadPromise;
                  return app
                })
            })
    })
}

flattenFnArray
返回 Promise.resolve() 链, 通过 promise.then 逐个执行应用的生命周期函数(可为数组)

export function flattenFnArray(appOrParcel, lifecycle) {
  let fns = app[life] || []
  fns = Array.isArray(fns) ? fns : [fns]
  if (fns.length === 0) fns = [() => Promise.resolve()]

  return (props) => fns.reduce((promise, fn) => promise.then(() => fn(props)), Promise.resolve())
}

performAppChanges 加载、启动、挂载阶段

执行 start 时的流程
started 标识为 true
1.先卸载应用
2.加载应用
3.挂载应用
4.卸载应用完执行 callCapturedEventListeners

performAppChanges

// ..
      // appsToLoad 需加载的应用,先加载再启动、挂载
      const loadThenMountPromises = appsToLoad.map((app) => {
        return toLoadPromise(app).then((app) =>
          tryToBootstrapAndMount(app, unmountAllPromise)
        );
      });
      // appsToMount 需启动挂载的 app
      const mountPromises = appsToMount
        .filter((appToMount) => appsToLoad.indexOf(appToMount) < 0)
        .map((appToMount) => {
          return tryToBootstrapAndMount(appToMount, unmountAllPromise);
        });
      return unmountAllPromise 
        .catch((err) => {
          callAllEventListeners();
          throw err;
        })
        .then(() => { // 卸载完毕 执行 callAllEventListeners,无需等待启动、挂载
          callAllEventListeners();
          return Promise.all(loadThenMountPromises.concat(mountPromises))
            .catch((err) => {
              pendingPromises.forEach((promise) => promise.reject(err));
              throw err;
            })
            .then(finishUpAndReturn);
        });

tryToBootstrapAndMount
在应用加载期间如果出现了某种延迟直接切换了另一个路由,那么之前路由的就不该被挂载。
所以进行了两次 shouldBeActive 判断

function tryToBootstrapAndMount(app, unmountAllPromise) {
  if (shouldBeActive(app)) { // 判断 app 是否需激活
    return toBootstrapPromise(app).then((app) => // 执行启动方法
      unmountAllPromise.then(() => // 卸载完后再次判断是否激活,执行挂载函数
        shouldBeActive(app) ? toMountPromise(app) : app
      )
    );
  } else {
    return unmountAllPromise.then(() => app);
  }
}

navigation-events

1.捕获 hashchange、popstate 事件
2.为 history API: pushState、replaceState 打补丁

执行时机

应用启动时待 vue-router 等必须监听的事件执行后

目的

1.浏览器地址改变时先执行 rerouter 进行子应用切换 2.保存导航事件的监听函数,待子应用切换完毕后执行,保证执行顺序正确

捕获路由导航事件

const routingEventsListeningTo = ["hashchange", "popstate"]; // 需捕获的事件

function urlReroute() { // 传入 url 参数执行 reroute 
    reroute([], arguments); 
}

window.addEventListener('hashchange', urlReroute);
window.addEventListener('popstate', urlReroute);
const originalAddEventListener = window.addEventListener;
const originalRemoveEventListener = window.removeEventListener;
window.addEventListener = function(eventName, fn) {
    // capturedEventListeners 中没有 fn
    if (routingEventsListeningTo.indexOf(eventName) >= 0 && !capturedEventListeners[eventName].some(listener => listener == fn)) {
        capturedEventListeners[eventName].push(fn); // 存入 capturedEventListeners 中待 rerouter 中执行 
        return;
    }
    return originalAddEventListener.apply(this, arguments)
}
window.removeEventListener = function(eventName, fn) {
    if (routingEventsListeningTo.indexOf(eventName) >= 0) {
        capturedEventListeners[eventName] = capturedEventListeners[eventName].filter(l => l !== fn);
        return;
    }
    return originalRemoveEventListener.apply(this, arguments)
}

history API 打补丁

当 pushState、replaceState history API 调用时是不会触发 popstate 事件的,需要进行一层处理

需要注意的是调用history.pushState()或history.replaceState()不会触发popstate事件。只有在做出浏览器动作时,才会触发该事件,如用户点击浏览器的回退按钮(或者在Javascript代码中调用history.back()或者history.forward()方法)
popstate_event

简单实现

function patchedUpdateState(updateState,methodName){
    return function(){
        const urlBefore = window.location.href;
        updateState.apply(this,arguments); // 执行 pushState | replaceState
        const urlAfter = window.location.href;

        if(urlBefore !== urlAfter){ // 触发 popstate 事件
            urlReroute(new PopStateEvent('popstate'));
        }
    }
}


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

执行 listeners

等到 rerouter 中执行

export function callCapturedEventListeners(eventArguments) {
  if (eventArguments) {
    const eventType = eventArguments[0].type;
    if (routingEventsListeningTo.indexOf(eventType) >= 0) {
      capturedEventListeners[eventType].forEach((listener) => {
        try {
          listener.apply(this, eventArguments);
        } catch (e) { // 应用的事件错误不应该中断 single-spa 的执行。
          setTimeout(() => {
            throw e;
          });
        }
      });
    }
  }
}

推荐阅读更多精彩内容