美文网首页
qiankun源码深挖

qiankun源码深挖

作者: 彩云Coding | 来源:发表于2020-10-16 23:27 被阅读0次

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

    生命周期

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

    qiankun总的生命周期.jpg

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

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

    • beforeLoad - Lifecycle | Array<Lifecycle> - 可选
    • beforeMount - Lifecycle | Array<Lifecycle> - 可选
    • afterMount - Lifecycle | Array<Lifecycle> - 可选
    • beforeUnmount - Lifecycle | Array<Lifecycle> - 可选
    • afterUnmount - Lifecycle | Array<Lifecycle> - 可选
    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官网

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

    相关文章

      网友评论

          本文标题:qiankun源码深挖

          本文链接:https://www.haomeiwen.com/subject/eirupktx.html