single-spa源码分析

作用: 它就是一个子应用加载器 + 状态机的结合体,而且具体怎么加载子应用还是基座应用提供的;框架里面维护了各个子应用的状态,以及在适当的时候负责更改子应用的状态、执行相应的生命周期函数

实现流程

  1. 注册子应用

    实现非常简单 就是向一个全局的apps数组里面添加应用对象。添加应用时顺便给应用一些初始状态

    apps.push(
        // 给每个应用增加一个内置属性
        assign(
          {
            loadErrorTime: null,
            // 最重要的,应用的状态
            status: NOT_LOADED,
            parcels: {},
            devtools: {
              overlays: {
                options: {},
                selectors: [],
              },
            },
          },
          registration
        )
      );
  2. 核心函数reroute

     /**
      * 每次切换路由前,将应用分为4大类,
      * 首次加载时执行loadApp
      * 后续的路由切换执行performAppChange
      * 为四大类的应用分别执行相应的操作,比如更改app.status,执行生命周期函数
      * 所以,从这里也可以看出来,single-spa就是一个维护应用的状态机
      * @param {*} pendingPromises 
      * @param {*} eventArguments 
      */
     export function reroute(pendingPromises = [], eventArguments) {
       // 应用正在切换,这个状态会在执行performAppChanges之前置为true,执行结束之后再置为false
       // 如果在中间用户重新切换路由了,即走这个if分支,暂时看起来就在数组中存储了一些信息,没看到有什么用
       // 字面意思理解就是用户等待app切换
       if (appChangeUnderway) {
         return new Promise((resolve, reject) => {
           peopleWaitingOnAppChange.push({
             resolve,
             reject,
             eventArguments,
           });
         });
       }
    
       // 将应用分为4大类
       const {
         // 需要被移除的
         appsToUnload,
         // 需要被卸载的
         appsToUnmount,
         // 需要被加载的
         appsToLoad,
         // 需要被挂载的
         appsToMount,
       } = getAppChanges(); // 内部就是通过获取当前apps状态来判断的
    
       let appsThatChanged;
    
       // 是否已经执行 start 方法
       if (isStarted()) {
         // 已执行
         appChangeUnderway = true;
         // 所有需要被改变的的应用
         appsThatChanged = appsToUnload.concat(
           appsToLoad,
           appsToUnmount,
           appsToMount
         );
         // 执行改变
         return performAppChanges();
       } else {
         // 未执行
         appsThatChanged = appsToLoad;
         // 加载Apps
         return loadApps();
       }
    
       // 整体返回一个立即resolved的promise,通过微任务来加载apps
       function loadApps() {
         return Promise.resolve().then(() => {
           // 加载每个子应用,并做一系列的状态变更和验证(比如结果为promise、子应用要导出生命周期函数)
           const loadPromises = appsToLoad.map(toLoadPromise);
    
           return (
             // 保证所有加载子应用的微任务执行完成
             Promise.all(loadPromises)
               .then(callAllEventListeners)
               // there are no mounted apps, before start() is called, so we always return []
               .then(() => [])
               .catch((err) => {
                 callAllEventListeners();
                 throw err;
               })
           );
         });
       }
    
       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)
             )
           );
           // 移除应用 => 更改应用状态,执行unload生命周期函数,执行一些清理动作
           // 其实一般情况下这里没有真的移除应用
           const unloadPromises = appsToUnload.map(toUnloadPromise);
    
           // 卸载应用,更改状态,执行unmount生命周期函数
           const unmountUnloadPromises = appsToUnmount
             .map(toUnmountPromise)
             // 卸载完然后移除,通过注册微任务的方式实现
             .map((unmountPromise) => unmountPromise.then(toUnloadPromise));
    
           const allUnmountPromises = unmountUnloadPromises.concat(unloadPromises);
    
           const unmountAllPromise = Promise.all(allUnmountPromises);
    
           // 卸载全部完成后触发一个事件
           unmountAllPromise.then(() => {
             window.dispatchEvent(
               new CustomEvent(
                 "single-spa:before-mount-routing-event",
                 getCustomEventDetail(true)
               )
             );
           });
    
           /* We load and bootstrap apps while other apps are unmounting, but we
           * wait to mount the app until all apps are finishing unmounting
           * 这个原因其实是因为这些操作都是通过注册不同的微任务实现的,而JS是单线程执行,
           * 所以自然后续的只能等待前面的执行完了才能执行
           * 这里一般情况下其实不会执行,只有手动执行了unloadApplication方法才会二次加载
           */
           const loadThenMountPromises = appsToLoad.map((app) => {
             return toLoadPromise(app).then((app) =>
               tryToBootstrapAndMount(app, unmountAllPromise)
             );
           });
    
           /* 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.
           * 初始化和挂载app,其实做的事情很简单,就是改变app.status,执行生命周期函数
           * 当然这里的初始化和挂载其实是前后脚一起完成的(只要中间用户没有切换路由)
           */
           const mountPromises = appsToMount
             .filter((appToMount) => appsToLoad.indexOf(appToMount) < 0)
             .map((appToMount) => {
               return tryToBootstrapAndMount(appToMount, unmountAllPromise);
             });
    
           // 后面就没啥了,可以理解为收尾工作
           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);
             });
         });
       }
     }

Last updated