qiankun源码深挖

2020-10-16  本文已影响0人  彩云Coding

使用qiankun也有一段时间了,但是在使用的过程中,总会遇到各种问题,面对这些问题最终虽然解决了,但是总感觉心里面不踏实,想要去看看qiankun的源码,加深一些理解,学习一下qiankun内部的实现原理。断断续续读了好几周,主要的目的是为了弄清楚qiankun、主应用、子应用、single-spa这四个部分的生命周期的执行顺序以及触发的条件。

生命周期

这里给出qiankun、主应用、子应用、single-spa的生命周期的总图。

qiankun总的生命周期.jpg

主应用和子应用的生命周期

在主应用中注册子应用的时候,可以传入五个生命周期函数,如下

registerMicroApps(apps, {
    beforeLoad: app => console.log('before load', app.name),
    beforeMount: [
      app => console.log('before mount', app.name),
    ],
    afterMount: app => console.log('after mount', app.name),
    beforeUnmount: app => console.log('before unmount', app.name),
    afterUnmount: app => console.log('after unmount', app.name)
});

依照子应用的接入规范,子应用必须暴露三个生命周期,如下

export async function bootstrap() {
  console.log('app bootstraped');
}
export async function mount(props) {
  console.log('app mount', props);
  render(props);
}
export async function unmount() {
  console.log('app unmount');
  instance.$destroy();
  instance = null;
  router = null;
}

qiankun的生命周期

qiankun主要是利用beforeLoadbeforeMountbeforeUnmount这三个生命周期添加和删除全局变量。

//qiankun-master\src\addons\engineFlag.ts
export default function getAddOn(global: Window): FrameworkLifeCycles<any> {
  return {
    async beforeLoad() {
      // eslint-disable-next-line no-param-reassign
      global.__POWERED_BY_QIANKUN__ = true;
    },

    async beforeMount() {
      // eslint-disable-next-line no-param-reassign
      global.__POWERED_BY_QIANKUN__ = true;
    },

    async beforeUnmount() {
      // eslint-disable-next-line no-param-reassign
      delete global.__POWERED_BY_QIANKUN__;
    },
  };
}
//qiankun-master\src\addons\runtimePublicPath.ts
export default function getAddOn(global: Window, publicPath = '/'): FrameworkLifeCycles<any> {
  let hasMountedOnce = false;

  return {
    async beforeLoad() {
      // eslint-disable-next-line no-param-reassign
      global.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ = publicPath;
    },

    async beforeMount() {
      if (hasMountedOnce) {
        // eslint-disable-next-line no-param-reassign
        global.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ = publicPath;
      }
    },

    async beforeUnmount() {
      if (rawPublicPath === undefined) {
        // eslint-disable-next-line no-param-reassign
        delete global.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
      } else {
        // eslint-disable-next-line no-param-reassign
        global.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ = rawPublicPath;
      }

      hasMountedOnce = true;
    },
  };
}

以上是qiankun、主应用、子应用分别可以使用的生命周期函数,但是这里还无法知道这些生命周期函数是何时何地执行,有谁进行调用,这是下面章节的主要内容,核心就是single-spa

生命周期的执行机制

首先需要明确的是qiankun实际上是对single-spa的进一步封装,使其更加容易开箱即用。

qiankun

我们关注qiankun暴露的registerMicroApps方法,

//qiankun-master\src\apis.ts
export function registerMicroApps<T extends object = {}>(
  apps: Array<RegistrableApp<T>>,
  lifeCycles?: FrameworkLifeCycles<T>,
) {
  //过滤出还没有注册的应用
  const unregisteredApps = apps.filter(app => !microApps.some(registeredApp =>                                           registeredApp.name === app.name));
  microApps = [...microApps, ...unregisteredApps];
  //对于每一个没有注册的应用调用qiankun的API进行注册
  unregisteredApps.forEach(app => {
    const { name, activeRule, loader = noop, props, ...appConfig } = app;
    registerApplication({
      name,
      app: async () => {
        loader(true);
        //这就是为了控制整个流程,可以看到frameworkStartedDefer.promise在start方法的最后才会              resolve,所以在start调用后,这边才会继续执行
        await frameworkStartedDefer.promise;
        //这里需要关注的是loadApp这个方法,该方法主要就是完成qiankun与主应用和子应用的生命周期的整合,           最终返回整理后的生命周期数组,并且是按照执行顺序排列的
        const { mount, ...otherMicroAppConfigs } = await loadApp(
          { name, props, ...appConfig },
          frameworkConfiguration, //这个参数在start函数中被赋值
          lifeCycles,
        );
        return {
          mount: [async () => loader(true), ...toArray(mount), async () => loader(false)],
          ...otherMicroAppConfigs,
        };
      },
      activeWhen: activeRule,
      customProps: props,
    });
  });
}

由于loadApp这个方法代码过长,这边就不完全贴出来,主要的执行过程用一个流程图给出来,

localapp执行过程.jpg

详细的代码大家可以自已前去阅读,这里简单给出函数的返回值,其中有部分代码省略,只关注主要的执行代码,

//qiankun-master\src\loader.ts
const parcelConfig: ParcelConfigObject = {
    name: appInstanceId,
    //子应用暴露的生命周期
    bootstrap,
    //这里是一个数组,里面有qiankun执行的一些逻辑
    mount: [
      //该函数是判断是否为单例模式,如果是单例模式就必须等待前面的应用先完成卸载操作。由于整个mount数组后面再执行的时候都是阻塞式的,所以这里如果返回的是一个promise,那么后续的函数只有等待该函数resolve之后才能继续执行。
      async () => {
        if ((await validateSingularMode(singular, app)) && prevAppUnmountedDeferred) {
          return prevAppUnmountedDeferred.promise;
        }
        return undefined;
      },
      // 该函数确保每次应用加载前容器 dom 结构已经设置完毕
      async () => {
        element = element || createElement(appContent, strictStyleIsolation);
        render({ element, loading: true }, 'mounting');
      },
      //该函数是执行beforeMount生命周期数组
      async () => execHooksChain(toArray(beforeMount), app, global),
      //该函数是挂载沙盒
      mountSandbox,
      //该函数是执行子应用暴露的mount生命周期函数
      async props => mount({ ...props, container: containerGetter(), setGlobalState,                                   onGlobalStateChange }),
      //该函数是执行afterMount生命周期数组
      async () => execHooksChain(toArray(afterMount), app, global),
      //该函数是检测若是单例模式,那么就需要给prevAppUnmountedDeferred赋予一个promise,该promise会在unmount的时候resolve,并且在一些需要判断单例模式下的一些执行时需要使用该变量
      async () => {
        if (await validateSingularMode(singular, app)) {
          prevAppUnmountedDeferred = new Deferred<void>();
        }
      }
    ],
    unmount: [
      //该函数是执行beforeUnmount生命周期数组
      async () => execHooksChain(toArray(beforeUnmount), app, global),
      //该函数是执行子应用暴露的unmount生命周期函数
      async props => unmount生命周期函数({ ...props, container: containerGetter() }),
      //该函数是卸载沙盒
      unmountSandbox,
      //该函数是执行afterUnmount生命周期数组
      async () => execHooksChain(toArray(afterUnmount), app, global),
      //该函数是卸载子应用的dom并且去除全局状态监听
      async () => {
        render({ element: null, loading: false }, 'unmounted');
        offGlobalStateChange(appInstanceId);
        // for gc,为了垃圾回收,引用计数为零可以回收
        element = null;
      },
      //该函数是将prevAppUnmountedDeferred进行resolve,防止单例模式下影响其他流程的正常运转
      async () => {
        if ((await validateSingularMode(singular, app)) && prevAppUnmountedDeferred) {
          prevAppUnmountedDeferred.resolve();
        }
      },
    ],
  };

single-spa

qiankun在调用single-sparegisterApplication方法后,就是将自己以及主应用、子应用的相关生命周期托管给了single-spa来进行触发。

我们看single-sparegisterApplication方法,

single-spa的registerApplication.jpg
//single-spa-master\src\applications\apps.js
export function registerApplication(
  appNameOrConfig,
  appOrLoadApp,
  activeWhen,
  customProps
) {
  //这里做一些参数化校验以及规整
  const registration = sanitizeArguments(
    appNameOrConfig,
    appOrLoadApp,
    activeWhen,
    customProps
  );
  //检查该app是否已经注册过了
  if (getAppNames().indexOf(registration.name) !== -1)
    throw Error(
      formatErrorMessage(
        21,
        __DEV__ &&
          `There is already an app registered with name ${registration.name}`,
        registration.name
      )
    );
  //这里的apps是single-spa维护的变量,用来存储所有的应用
  apps.push(
    assign(
      {
        loadErrorTime: null,
        status: NOT_LOADED,
        parcels: {},
        devtools: {
          overlays: {
            options: {},
            selectors: [],
          },
        },
      },
      registration
    )
  );
  if (isInBrowser) {
    ensureJQuerySupport();
    //这个方法是single-spa的核心方法,类似一个状态机,完成应用不同状态的流转
    reroute();
  }
}

到这里,基本上所有应用都已全部托管给single-spa,而single-spa后续对于每一个应用的处理,状态的变化,事件的触发都基本通过reroute这个方法完成,所以这个方法也是我们关注的一个重点。

single-spa的核心方法reroute()

首先需要从整体上看,reroute()这个方法基本是有三个地方会调用,

reroute的调用点.jpg

这三个调用点可以大致分为显示调用(微应用注册、start),隐式调用(路有变化)。有路由的变化是随时可能发生,更多的随机性,更多的情况需要考虑,所以先梳理当路由变化时,reroute方法是如何被触发的。

路由变化触发reroute

这里的代码逻辑比较细碎,并且比较长,有兴趣的可以去下载源码找到代码位置为,

single-spa-master\src\navigation\navigation-events.js自行阅读一下。

我这边阅读之后画了一个基础的流程图,大概的说明single-spa对于路由变化的处理方式,

路由变化时触发reroute.jpg

这里面主要需要注意的就是single-spa是通过监听hashchange 和popstate 两个事件来判断路由发生变化,但是由于window.history.pushState 和window.history.replaceState 并没有触发popstate 事件,所以碎玉这两个函数需要加强,

//single-spa-master\src\navigation\navigation-events.js
window.history.pushState = patchedUpdateState(
    window.history.pushState,
    "pushState"
);
window.history.replaceState = patchedUpdateState(
    window.history.replaceState,
    "replaceState"
);

在patchedUpdateState 方法中完成两个任务

(1)正确完成URl的变化

(2)调用reroute方法,并且new一个popstate 事件传递过去,这么做的主要目的是能够正确的执行用户的事件监听。

这里single-spa之所以能够拿到用户的监听回调,是因为其重写了window.addEventListener ,并在其中判断是否是监听的hashchange 和popstate 这两个事件,如果是,则会缓冲到一个回调函数的数组之中。

  //single-spa-master\src\navigation\navigation-events.js
  const originalAddEventListener = window.addEventListener;
  const originalRemoveEventListener = window.removeEventListener;
  window.addEventListener = function (eventName, fn) {
    // 如果用户监听的是 hashchange 和 popstate 事件,并且这个监听器此前未加入事件监听列表
    // 那这个事件是有可能引发应用变更的,需要加入 capturedEventListeners 中
    // 直接 return 掉,说明 hashchange 和 popstate 事件并没有马上执行
    // 而是在执行完 reroute 逻辑之后在执行
    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);
  };

弄清楚路由变化的时候,single-spa的主要操作之后,下面就是要摸清楚reroute函数具体是做了哪些操作。

执行reroute(pendingPromises = [], eventArguments)

在这个方法中最主干的逻辑是,

//single-spa-master\src\navigation\reroute.js
export function reroute(pendingPromises = [], eventArguments) {
  //如果正在执行上一个路由变化的操作,则将该事件缓存到peopleWaitingOnAppChange数组中,该事件大概率是用     户的监听事件,被single-spa劫持了。
  if (appChangeUnderway) {
    return new Promise((resolve, reject) => {
      peopleWaitingOnAppChange.push({
        resolve,
        reject,
        eventArguments,
      });
    });
  }
  //依据当前变化后的URl判断目前所有应用的状态即将发生的变化
  const {
    appsToUnload,
    appsToUnmount,
    appsToLoad,
    appsToMount,
  } = getAppChanges();
    
  let appsThatChanged;
  //判断是否已经执行start函数,如果是,则进一步执行每一个app的生命周期,如果没有,则只load应用。
  if (isStarted()) {
    appChangeUnderway = true;
    appsThatChanged = appsToUnload.concat(
      appsToLoad,
      appsToUnmount,
      appsToMount
    );
    return performAppChanges();
  } else {
    appsThatChanged = appsToLoad;
    return loadApps();
  }
}

依据上面的主要执行过程,画了一个基本的流程图,大概说明了reroute方法对于应用状态的变化以及single-spa提供的各个公共事件的派发机制。

reroute的执行过程.jpg

single-spa在执行过程中派发的几个全局事件,应用可以进行监听,这其实也算是single-spa的一个生命周期,主要的执行过程为,

single-spa派发的事件.jpg

每一个应用自身的状态都会随着URl的不断改变而变化,基本由下面的几个阶段组成,

应用状态的几个阶段.jpg

小结

源码还有很多的细节可以去看,我这边只是大致梳理了一下qiankun生命周期的执行过程,基本就是将生命周期的逻辑封装好,托管给single-spa去执行。

reroute 流程作为 single-spa 的核心流程,充当了一个应用状态机的角色,控制了应用的生命周期的流转和事件分发。qiankun 就是利用了这一特性,将应用交给 single-spa 管理,自己实现应用的加载方法(loadApp)和生命周期

参考

single-spa 的生命周期和事件管理

万字长文+图文并茂+全面解析微前端框架 qiankun 源码 - qiankun 篇

single-spa官网

qiankun官网

还有一些就不全罗列出来了。。眼睛酸,多休息。欢迎大家多多吐槽。

上一篇下一篇

猜你喜欢

热点阅读