微前端——single-spa源码学习

news2024/11/17 3:26:08

前言

本来是想直接去学习下qiankun的源码,但是qiankun是基于single-spa做的二次封装,通过解决了single-spa的一些弊端和不足来帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。

所以我们应该先对single-spa有一个全面的认识和了解,了解它的不足和缺陷,到时候让我们带着问题去学习qiankun的底层,会有更大的帮助。

single-spa中文文档

代码库地址 https://github.com/sunlianglife/single-spa-study,可以打开代码再对照着阅读,更容易理解

关于微前端

可以分别从single-spa的文档介绍和qiankun的文档介绍初步了解

目录结构及相关文件总览

我是先在github的clone的qiankun代码,看了下package.json里面的single-spa的版本是5.9.2的,所以我就clone了对应版本的single-spa的代码

多写注释,做笔记,编写示例代码+console调试

在这里插入图片描述

1、src/single-spa.js

single-spa的入口文件,其中就是暴露出single-spa的一些属性和方法

2、src/start.js

应用注册完之后,调用start()的逻辑

  • started——应用是否启动的标志
  • start——开启应用的方法
  • isStarted——判断应用是否启动的方法

3、src/jquery-support.js

确保jquery的支持

4、utils

utils里面的工具函数在下一节开始会介绍到

5、src/parcels/mount-parcel.js

沙箱 Parcels
single-spa的一个高级特性,与框架无关,api与注册应用一致,不同的是:parcel组件需要手动挂载,而不是通过 activity 方法被动激活。
single-spa中的微前端有两种类型

  • single-spa applications: application 模式下,子应用的切换(挂载、卸载)都是由修改路由触发的,整个切换过程由 single-spa 框架控制,子应用仅需提供正确的生命周期方法即可。
  • single-spa parcels: 不受路由控制,渲染组件的微前端。在 parcel 模式下,我们需要使用 single-spa 提供的 mountRootParcel 方法来手动挂载/更新/卸载组件

mountParcelmountRootParcel 将立即挂载parcel并返回这个parcel对象。 需要卸载需要手动调用 parcel的 unmount.
mountRootParcelmountParcel 的用法完全一样,只不过 mountParcel 方法不能直接从 single-spa 中获取,需要从子应用/组件的 mount 生命周期方法执行时传入的 props 中获取,

6、src/navigation/navigation-events.js

处理导航事件的文件,包括事件监听,自定义事件创建、事件收集、不同应用之间的跳转等

  • capturedEventListeners——导航事件的收集
  • routingEventsListeningTo——监听到浏览器导航变化的两种事件
  • navigateToUrl——导航到对应url,实现在不同注册应用之前的切换
  • patchedUpdateState——当触发replaceState和pushState方法时,对其进行一个增强
  • createPopStateEvent——创建自定义事件
  • window.addEventListener——对“hashchange”和“popstate”监听
  • parseUri——创建一个a连接的导航

7、src/navigation/reroute.js

reroute()在整个single-spa中就是负责改变app.status和执行在子应用中注册的生命周期函数。

8、src/applications/app-errors.js

异常处理的方法文件

9、src/applications/app.helpers.js

  • 定义应用各个状态的常量
  • isActive——应用是否加载完毕
  • shouldBeActive——当前路由关联的子应用是否激活
  • toName——返回应用的名称
  • isParcel——是否为Parcel模式
  • objectType——区分single-spa的两种模式 parcel || application

10、src/applications/apps.js

注册子应用的方法就这里面,其他大多数是对参数的一些校验处理

  • registerApplication——注册子应用
  • getAppChanges——将子应用按照状态拆分
  • getMountedApps——获取已经挂载的应用名称
  • getAppNames——获取应用的名称
  • getAppStatus——根据名称获取应用的状态
  • checkActivityFunctions——将会调用每个应用的 activeWhen 并且返回一个根据当前路径判断那些应用应该被挂载的列表
  • unregisterApplication——应用卸载
  • unloadApplication——移除已注册的应用的目的是将其设置回 NOT_LOADED 状态,
  • immediatelyUnloadApp——立即卸载应用,调用卸载的生命周期函数
  • validateRegisterWithArguments——参数异常处理
  • validateRegisterWithConfig——验证应用的配置信息是否合法,抛出异常
  • validCustomProps——验证注册子应用的propps
  • sanitizeArguments——格式化注册子应用的属性参数
  • sanitizeLoadApp——验证注册子应用是的第二个参数一定是一个返回promise的函数
  • sanitizeCustomProps——保证props存在
  • sanitizeActiveWhen——得到一个函数,用来判断当前地址和用户的给定的baseUrl的比配关系,函数返回boolean
  • pathToActiveWhen——函数返回boolean值,判断当前路由是否匹配用户给定的路径
  • toDynamicPathValidatorRegex——根据用户提供的baseURL,生成正则表达式

11、src/applications/timeouts.js

超时的一些处理

12、src/devtools/devtools.js

暴露的属性和方法,在入口文件中导出

// 暴露的方法集合
// window.__SINGLE_SPA_DEVTOOLS__  single-spa在window中挂载的变量
if (isInBrowser && window.__SINGLE_SPA_DEVTOOLS__) {
  window.__SINGLE_SPA_DEVTOOLS__.exposedMethods = devtools;
}

13、src/lifecycles

这个文件夹下面的文件,从名字就能看出是子应用各个生命周期的执行方法,改变状态,和src/applications/app.helpers.js中定义的状态是对应的

源码分析(摘取部分核心的方法,全部代码可以去代码仓库上去看)

拿到一个陌生的项目,首先需要看的是package.jsonREADME.mdconfig文件,从目录能看出来single-spa是用rollup来打包的,打开之后在导出的配置信息里面找到入口文件src/single-spa.js

input: “./src/single-spa.js”

  • 先介绍一下utils的工具函数,好多地方会用到
    在这里插入图片描述

  • 应用的状态常量

// App statuses
export const NOT_LOADED = "NOT_LOADED"; // single-spa应用注册了,还未加载。
export const LOADING_SOURCE_CODE = "LOADING_SOURCE_CODE"; // 应用代码正在被拉取。
export const NOT_BOOTSTRAPPED = "NOT_BOOTSTRAPPED"; // 应用已经加载,还未初始化。
export const BOOTSTRAPPING = "BOOTSTRAPPING"; // 生命周期函数已经执行,还未结束。
export const NOT_MOUNTED = "NOT_MOUNTED"; // 应用已经加载和初始化,还未挂载
export const MOUNTING = "MOUNTING"; // 应用正在被挂载,还未结束。
export const MOUNTED = "MOUNTED"; // 应用目前处于激活状态,已经挂载到DOM元素上。
export const UPDATING = "UPDATING"; // 更新中
export const UNMOUNTING = "UNMOUNTING"; // 应用正在被卸载,还未结束
export const UNLOADING = "UNLOADING"; // 应用正在被移除,还未结束
export const LOAD_ERROR = "LOAD_ERROR"; // 应用的加载功能返回了一个rejected的Promise。这通常是由于下载应用程序的javascript包时出现网络错误造成的。Single-spa将在用户从当前路由导航并返回后重试加载应用。
export const SKIP_BECAUSE_BROKEN = "SKIP_BECAUSE_BROKEN"; // 应用在加载、初始化、挂载或卸载过程中抛出错误,由于行为不当而被跳过,因此被隔离。其他应用将正常运行。

01|src/single-spa.js 入口文件

我们先来看single-spa给我们暴露了哪些属性和方法

export { start } from "./start.js"; // 启动的方法
export { ensureJQuerySupport } from "./jquery-support.js"; // 确保jquery支持,可以外部传入
export {
  setBootstrapMaxTime, // 全局配置初始化超时时间。
  setMountMaxTime, // 全局配置挂载超时时间。
  setUnmountMaxTime, // 全局配置卸载超时时间
  setUnloadMaxTime, // 全局配置移除超时时间。
} from "./applications/timeouts.js";
export {
  registerApplication, // 注册子应用的方法
  unregisterApplication, // 卸载子应用
  getMountedApps, // 返回当前已经挂载的子应用的名称
  getAppStatus, // 参数:注册应用的名字,返回:应用的状态
  unloadApplication, // 移除已注册的应用
  checkActivityFunctions, // 将会调用每个应用的 mockWindowLocation 并且返回一个根据当前路判断那些应用应该被挂载的列表。
  getAppNames, // 获取应用的名称(任何状态)
  pathToActiveWhen, // 判断应用的前缀url,返回:boolean
} from "./applications/apps.js";
export { navigateToUrl } from "./navigation/navigation-events.js"; // 实现在不同注册应用之前的切换
export { triggerAppChange } from "./navigation/reroute.js"; // 返回一个Promise对象,当所有应用挂载/卸载时它执行 resolve/reject 方法,它一般被用来测试single-spa,在生产环境可能不需要。
export {
  addErrorHandler, // 添加异常处理,抛出错误
  removeErrorHandler, // 删除给定的错误处理程序函数
} from "./applications/app-errors.js";
export { mountRootParcel } from "./parcels/mount-parcel.js"; // 将会创建并挂载一个 single-spa parcel.

// 应用的状态,已备注到app.helpers.js中
export {
  NOT_LOADED,
  LOADING_SOURCE_CODE,
  NOT_BOOTSTRAPPED,
  BOOTSTRAPPING,
  NOT_MOUNTED,
  MOUNTING,
  UPDATING,
  LOAD_ERROR,
  MOUNTED,
  UNMOUNTING,
  SKIP_BECAUSE_BROKEN,
} from "./applications/app.helpers.js";

import devtools from "./devtools/devtools"; // 暴露的方法集合
import { isInBrowser } from "./utils/runtime-environment.js"; // 判断浏览器环境

// 暴露的方法集合
// window.__SINGLE_SPA_DEVTOOLS__  single-spa在window中挂载的变量
if (isInBrowser && window.__SINGLE_SPA_DEVTOOLS__) {
  window.__SINGLE_SPA_DEVTOOLS__.exposedMethods = devtools;
}

上面导出的和挂载到window上的都是我们可以在开发阶段获取到的
single-spa官网api解析

02|注册子应用 registerApplication ——src/applications/apps.js

/**
 * 
 * @param {*} appNameOrConfig 子应用的名称
 * @param {*} appOrLoadApp 应用的加载方法,返回一个应用或者promise
 * @param {*} activeWhen 纯函数,返回应用是否激活的boolean
 * @param {*} customProps 传递给子应用的props
 * 每注册一个子应用 registerApplication方 法就需要调用一次
 */
export function registerApplication(
  appNameOrConfig,
  appOrLoadApp,
  activeWhen,
  customProps
) {
  // 格式化注册子应用的参数
  const registration = sanitizeArguments(
    appNameOrConfig,
    appOrLoadApp,
    activeWhen,
    customProps
  );
  // 子应用注册的防重复校验
  if (getAppNames().indexOf(registration.name) !== -1)
    throw Error(
      formatErrorMessage(
        21,
        __DEV__ &&
          `There is already an app registered with name ${registration.name}`,
        registration.name
      )
    );

  // 将各个应用的配置信息存储到apps数组中
  apps.push(
    assign(
      {
        loadErrorTime: null,
        status: NOT_LOADED,
        parcels: {},
        devtools: {
          overlays: {
            options: {},
            selectors: [],
          },
        },
      },
      registration
    )
  );
  // 浏览器环境运行
  if (isInBrowser) {
    ensureJQuerySupport();
    reroute();
  }
}

这里注意最后调用的方法reroute()后面会说到

能看出来注册方法做的事情不多,就是对接受的参数做一个格式化校验,然后将各个应用的配置信息存储到apps数组中,最后执行reroute()方法。

文件里面的其他方法及属性在第一节总览里面有介绍,在具体的可以去代码仓库看详细的,源码分析这一块只摘了大流程相关的

03|启动应用start()——src/start.js

在start被调用之前,应用先被下载,但不会初始化/挂载/卸载。

/**
 * reroute // reroute在整个single-spa就是负责改变app.status和执行在子应用中注册的生命周期函数。
 * formatErrorMessage 格式化异常信息
 * setUrlRerouteOnly // 路由的变化,应用是否从定向
 * isInBrowser 是否是浏览器环境
 */
import { reroute } from "./navigation/reroute.js";
import { formatErrorMessage } from "./applications/app-errors.js";
import { setUrlRerouteOnly } from "./navigation/navigation-events.js";
import { isInBrowser } from "./utils/runtime-environment.js";

// 应用启动的标志
let started = false;

// 开启的方法
/**
 * 必须在你single spa的配置中调用!在调用 start 之前, 应用会被加载, 但不会初始化,挂载或卸载。 
 * start 的原因是让你更好的控制你单页应用的性能。
 * 举个栗子,你想立即声明已经注册过的应用(开始下载那些激活应用的代码),
 * 但是实际上直到初始化AJAX(或许去获取用户的登录信息)请求完成之前不会挂载它们 。 
 * 在这个例子里,立马调用 registerApplication 方法,完成AJAX后再去调用 start方法会获得最佳性能。
 * 
 * @param {*}  opts 属性对象,可选 示例: {urlRerouteOnly: true}
 * urlRerouteOnly:默认为false的布尔值。如果设置为true,
 * 对history.pushState()和history.replaceState()的调用将不会触发单个spa重新定向路由,
 * 除非客户端路由已更改。在某些情况下,将此设置为true可以提高性能。有关更多信息,请阅读https://github.com/single-spa/single-spa/issues/484。
 */
export function start(opts) {
  started = true;
  if (opts && opts.urlRerouteOnly) {
    setUrlRerouteOnly(opts.urlRerouteOnly);
  }
  if (isInBrowser) {
    reroute();
  }
}

// 返回应用是否启动的boolean值
export function isStarted() {
  return started;
}

// 在浏览器环境中
if (isInBrowser) {
  setTimeout(() => {
    // 如果应用注册了,没有调用start方法,抛出异常,“single-spa应用加载5000后尚未调用start方法。。。。”
    if (!started) {
      console.warn(
        formatErrorMessage(
          1,
          __DEV__ && // 是否是开发环境
            `singleSpa.start() has not been called, 5000ms after single-spa was loaded. Before start() is called, apps can be declared and loaded, but not bootstrapped or mounted.`
        )
      );
    }
  }, 5000);
}

这里重点只看start()方法:更改应用启动的标志之后,也调用了reroute()方法

04|以reroute()为切入点——src/navigation/reroute.js

reroute在整个single-spa就是负责改变app.status和执行在子应用中注册的生命周期函数。

export function reroute(pendingPromises = [], eventArguments) {
  // ....省略展示

  if (isStarted()) {
    appChangeUnderway = true;
    appsThatChanged = appsToUnload.concat(
      appsToLoad,
      appsToUnmount,
      appsToMount
    );
    return performAppChanges();
  } else {
    appsThatChanged = appsToLoad;
    return loadApps();
  }

  // .... 省略展示
}

在调用start方法之前会执行loadApps()方法, 调用start方法之后执行performAppChanges()方法

1、调用start之前的逻辑

  • 调用start之前也就是注册应用时触发的rerote方法
import { toLoadPromise } from "../lifecycles/load.js";
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)
          // there are no mounted apps, before start() is called, so we always return []
          .then(() => [])
          .catch((err) => {
            callAllEventListeners(); // 遍历执行路由收集的函数
            throw err;
          })
      );
    });
  }
}
  • getAppChanges()方法
// 将应用按照状态拆分
export function getAppChanges() {
  const appsToUnload = [], // 需要移除的
    appsToUnmount = [], // 需要卸载的
    appsToLoad = [], // 需要加载的
    appsToMount = []; // 需要挂载的

  // We re-attempt to download applications in LOAD_ERROR after a timeout of 200 milliseconds
  // 超时200毫秒后,重新尝试在LOAD_ERROR中下载应用程序
  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 };
}
  • toLoadPromise
// 应用代码正在被拉取的生命周期
/**
 *  通过微任务加载子应用,最终是return了一个promise出行,在注册了加载子应用的微任务.
 *  更改app.status为LOAD_SOURCE_CODE => NOT_BOOTSTRAP,当然还有可能是LOAD_ERROR
 *  执行加载函数,并将props传递给加载函数,给用户处理props的一个机会,因为这个props是一个完备的props
 *  验证加载函数的执行结果,必须为promise,且加载函数内部必须return一个对象
 *  这个对象是子应用的,对象中必须包括各个必须的生命周期函数
 *  然后将生命周期方法通过一个函数包裹并挂载到app对象上
 *  app加载完成,删除app.loadPromise
 * @param {*} app 
 */
export function toLoadPromise(app) {
  return Promise.resolve().then(() => {
    if (app.loadPromise) {
      // app已经在被加载
      return app.loadPromise;
    }

    // 状态为NOT_LOADED和LOAD_ERROR的app才可以被加载
    if (app.status !== NOT_LOADED && app.status !== LOAD_ERROR) {
      return app;
    }

    app.status = LOADING_SOURCE_CODE;

    let appOpts, isUserErr;

    return (app.loadPromise = Promise.resolve()
      .then(() => {
        // 执行app的加载函数,并给子应用传递props => 用户自定义的customProps和内置的比如应用的名称、singleSpa实例
        const loadPromise = app.loadApp(getProps(app));
        if (!smellsLikeAPromise(loadPromise)) {
          // The name of the app will be prepended to this error message inside of the handleAppError function
          isUserErr = true;
          throw Error(
            formatErrorMessage(
              33,
              __DEV__ &&
                `single-spa loading function did not return a promise. Check the second argument to registerApplication('${toName(
                  app
                )}', loadingFunction, activityFunction)`,
              toName(app)
            )
          );
        }
        return loadPromise.then((val) => {
          app.loadErrorTime = null;

          // window.singleSpa
          appOpts = val;

          let validationErrMessage, validationErrCode;

          if (typeof appOpts !== "object") {
            validationErrCode = 34;
            if (__DEV__) {
              validationErrMessage = `does not export anything`;
            }
          }

          // 必须导出bootstrap生命周期函数 
          if (
            // ES Modules don't have the Object prototype
            Object.prototype.hasOwnProperty.call(appOpts, "bootstrap") &&
            !validLifecycleFn(appOpts.bootstrap)
          ) {
            validationErrCode = 35;
            if (__DEV__) {
              validationErrMessage = `does not export a valid bootstrap function or array of functions`;
            }
          }

          // 必须导出mount生命周期函数 
          if (!validLifecycleFn(appOpts.mount)) {
            validationErrCode = 36;
            if (__DEV__) {
              validationErrMessage = `does not export a mount function or array of functions`;
            }
          }

          // 必须导出unmount生命周期函数 
          if (!validLifecycleFn(appOpts.unmount)) {
            validationErrCode = 37;
            if (__DEV__) {
              validationErrMessage = `does not export a unmount function or array of functions`;
            }
          }

          const type = objectType(appOpts);

          if (validationErrCode) {
            let appOptsStr;
            try {
              appOptsStr = JSON.stringify(appOpts);
            } catch {}
            console.error(
              formatErrorMessage(
                validationErrCode,
                __DEV__ &&
                  `The loading function for single-spa ${type} '${toName(
                    app
                  )}' resolved with the following, which does not have bootstrap, mount, and unmount functions`,
                type,
                toName(app),
                appOptsStr
              ),
              appOpts
            );
            handleAppError(validationErrMessage, app, SKIP_BECAUSE_BROKEN);
            return app;
          }

          if (appOpts.devtools && appOpts.devtools.overlays) {
            app.devtools.overlays = assign(
              {},
              app.devtools.overlays,
              appOpts.devtools.overlays
            );
          }

          app.status = NOT_BOOTSTRAPPED;
          // 在app对象上挂载生命周期方法,每个方法都接收一个props作为参数,方法内部执行子应用导出的生命周期函数,并确保生命周期函数返回一个promise
          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) => {
        delete app.loadPromise;

        let newStatus;
        if (isUserErr) {
          newStatus = SKIP_BECAUSE_BROKEN;
        } else {
          newStatus = LOAD_ERROR;
          app.loadErrorTime = new Date().getTime();
        }
        handleAppError(err, app, newStatus);

        return app;
      }));
  });
}

2、调用start之后

  • reroute
import { toLoadPromise } from "../lifecycles/load.js";
export function reroute(pendingPromises = [], eventArguments) {
  // ....省略展示

  const {
    appsToUnload, // 需要移除
    appsToUnmount, // 需要卸载
    appsToLoad, // 需要加载
    appsToMount, // 需要挂载
  } = getAppChanges(); // 得到各个状态的应用
  
  appChangeUnderway = true;
    appsThatChanged = appsToUnload.concat(
      appsToLoad,
      appsToUnmount,
      appsToMount
    );
  return performAppChanges();
}
  • performAppChanges
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, { cancelNavigation })
        )
      );

      if (navigationIsCanceled) {
        window.dispatchEvent(
          new CustomEvent(
            "single-spa:before-mount-routing-event",
            getCustomEventDetail(true)
          )
        );
        finishUpAndReturn();
        navigateToUrl(oldUrl);
        return;
      }

      // 移除应用 => 更改应用状态,执行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.
       */
      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);
        });
    });
  }

05|监听路由变化

其实说了这么多,到底是在哪里监听的路由变化呢,看这个文件src/navigation/navigation-events.js

if (isInBrowser) {
  // We will trigger an app change for any routing events.
  // 在浏览器环境对 hashchange 和 popstate的触发做一个监听
  window.addEventListener("hashchange", urlReroute);
  window.addEventListener("popstate", urlReroute);

  // Monkeypatch addEventListener so that we can ensure correct timing
  const originalAddEventListener = window.addEventListener;
  const originalRemoveEventListener = window.removeEventListener;
  // 监听事件触发的时候,对触发的事件做一个收集
  window.addEventListener = function (eventName, fn) {
    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);
  };

  // 移除事件响应的对收集的事件做删除
  window.removeEventListener = function (eventName, listenerFn) {
    if (typeof listenerFn === "function") {
      if (routingEventsListeningTo.indexOf(eventName) >= 0) {
        capturedEventListeners[eventName] = capturedEventListeners[
          eventName
        ].filter((fn) => fn !== listenerFn);
        return;
      }
    }

    return originalRemoveEventListener.apply(this, arguments);
  };

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

  if (window.singleSpaNavigate) {
    console.warn(
      formatErrorMessage(
        41,
        __DEV__ &&
          "single-spa has been loaded twice on the page. This can result in unexpected behavior."
      )
    );
  } else {
    /* For convenience in `onclick` attributes, we expose a global function for navigating to
     * whatever an <a> tag's href is.
     */
    window.singleSpaNavigate = navigateToUrl;
  }
}

这段代码不是放在方法里面导出调用的,而是直接这样写,是什么意思呢

文件通过引入建立依赖关系,在最后打包输出为bundle文件时,这段代码是存在全局作用域的,所用当引入single-spa的时候这些会自动执行

在使用 window.history 时,如果执行 pushState(repalceState) 方法,是不会触发 popstate 事件的,而 single-spa 通过一种巧妙的方式,实现了执行 pushState(replaceState) 方法可触发 popstate 事件

/**
 * 因为上面只对hashChange和popState事件做了监听,所以当触发replaceState和pushState方法时,对其进行一个增强,保证其内部逻辑不变的同时,执行自定义事件
 * @param {*} updateState | 浏览器的replaceState和pushState方法触发
 * @param {*} methodName | 字符串 ‘replaceState‘ || 'pushState'
 * @returns 
 */
function patchedUpdateState(updateState, methodName) {
  return function () {
    // 跳转之前的url
    const urlBefore = window.location.href;
    // 劫持使用传入的updateState方法,保证原来的功能不失效
    const result = updateState.apply(this, arguments);
    const urlAfter = window.location.href;

    if (!urlRerouteOnly || urlBefore !== urlAfter) {
      if (isStarted()) {
        // fire an artificial popstate event once single-spa is started,
        // so that single-spa applications know about routing that
        // occurs in a different application

        // 如过开启了start方法,则不会调用reroute方法
        // window.dispatchEvent 触发自定义事件,
        window.dispatchEvent(
          // 创建自定义事件
          createPopStateEvent(window.history.state, methodName)
        );
      } else {
        // do not fire an artificial popstate event before single-spa is started,
        // since no single-spa applications need to know about routing events
        // outside of their own router.
        reroute([]);
      }
    }

    return result;
  };
}

/**
 * 创建自定义事件
 * @param {*} state window.history.state
 * @param {*} originalMethodName  方法名 replaceState || pushState
 * @returns 
 */
function createPopStateEvent(state, originalMethodName) {
  // https://github.com/single-spa/single-spa/issues/224 and https://github.com/single-spa/single-spa-angular/issues/49
  // We need a popstate event even though the browser doesn't do one by default when you call replaceState, so that
  // all the applications can reroute. We explicitly identify this extraneous event by setting singleSpa=true and
  // singleSpaTrigger=<pushState|replaceState> on the event instance.
  let evt;
  try {
    // 创建 popstate 自定义事件,当触发 replaceState || pushState 时, 监听popstate就能触发
    evt = new PopStateEvent("popstate", { state });
  } catch (err) {
    // IE 11 compatibility https://github.com/single-spa/single-spa/issues/299
    // https://docs.microsoft.com/en-us/openspecs/ie_standards/ms-html5e/bd560f47-b349-4d2c-baa8-f1560fb489dd
    evt = document.createEvent("PopStateEvent");
    evt.initPopStateEvent("popstate", false, false, state);
  }
  evt.singleSpa = true;
  evt.singleSpaTrigger = originalMethodName;
  return evt;
}

之所以能在执行 pushState、replaceState 方法时,触发 popstate 事件,是因为 single-spa在这里 重写了 window.history 的 pushState 和 replaceState 方法。在执行 pushState、replaceState 方法时,会通过原生方法 – PopStateEvent 构建一个事件对象,然后调用 window.dispatchEvent 方法,手动触发 popState 事件。

06|流程梳理

当我们启动应用时,会调用registerApplication注册子应用和start开启应用,这两个方法内部都调用了reroute函数

  • 其中registerApplication注册子应用,对应用的信息进行配置包裹到apps中
  • start方法执行时通过urlRerouteOnly判断是否要监听url路由变化,然后调用reroute方法
  • 与此同时全局对浏览器的hashchange 和 popstate的触发做一个监听,并通过createPopStateEvent自定义popstate事件的方式对replaceState和pushState进行重写。所以我们通过history.replaceState或者history.pushState本质上还是触发了我们监听的popstate事件,从而触发reroute。
  • reroute方法内部调用getAppChanges,该方法会遍历apps应用数组,根据shouldBeActive方法判断window.location匹配的app激活规则判断子应用是已激活,返回不同状态的应用
  • 然后reroute方法根据started变量的状态走了两个分支,如果started是未开启状态会调用loadApps函数执行app.loadApp来实际加载子应用。再调用callAllEventListeners遍历执行路由收集的函数
  • 如果started是开启状态则调用performAppChanges方法先卸载需要卸载的应用,再执行appsToLoad、appsToMount加载启动挂载应用,期间子应用的生命周期函数会挂载到app配置对象的属性上,在指定的情况下执行

关注的点 Q&A

1、single-spa 是如何工作的

single-spa 有两种使用模式:application 和 parcel

  • application

    application 模式下,先通过 registerApplication 注册子应用,然后在基座应用挂载完成以后执行 start 方法, 这样基座应用就可以根据 url 的变化来进行子应用切换,激活对应的子应用。

  • parcel
    取到组件的生命周期方法,然后通过 mountRootParcel 方法直接挂载。

    mountRootParcel 方法会返回一个 parcel 实例对象,内部包含 update、unmount 方法。当我们需要更新组件时,直接调用 parcel 对象的 update 方法,就可以触发组件的 update 生命周期方法;当我们需要卸载组件时,直接调用 parcel 对象的 unmount 方法。
    在执行 mountRootParcel 方法时,传入的第二个参数,会作为组件 mount 生命周期方法的入参;在执行 parcel.update 方法时,传入的参数,会作为组件 update 生命周期方法的入参。

2、如何通信

父组件 —— parcel

父组件通过props透传

具体的前面也有简单提到:在执行 mountRootParcel 方法时,传入的第二个参数,会作为组件 mount 生命周期方法的入参;在执行 parcel.update 方法时,传入的参数,会作为组件 update 生命周期方法的入参。
就像平时开发组件:子组件回调伏组件某个方法这种方式,我们在父组件定一个方法传给parcel组件,parcel组件就可以在需要的时候执行这个方法通知父组件更新

parcel组件之间的通信

这种其实也是 parcel 组件和父组件之间的通信。 parcel 组件可以通过父组件传递的方法,触发父组件的更新,父组件更新以后,在触发另一个parcel 组件的更新。

基座应用和子应用的通信

在基座应用注册子应用的时候,可以给每个子应用定义一个customProps,这个会作为mount方法的入参数,里面也可以包裹回调的方法,当子应用需要通知基座应用更新时,可以执行这个方法

子应用的通信

也是基于和基座应用通信的这种方式

3、为什么子应用导出的生命周期函数都是一个promise

子应用使用

export function mount(props) {
    return Promise.resolve().then(() => {
        // 子应用/组件具体的挂载逻辑
        ...
    })
}

single-spa——src/lifecycles/mount.js中执行逻辑

// 应用挂载完的生命周期
export function toMountPromise(appOrParcel, hardFail) {
  return Promise.resolve().then(() => {
    if (appOrParcel.status !== NOT_MOUNTED) {
      return appOrParcel;
    }

    // single-spa其实在不同的阶段提供了相应的自定义事件,让用户可以做一些事情
    if (!beforeFirstMountFired) {
      window.dispatchEvent(new CustomEvent("single-spa:before-first-mount"));
      beforeFirstMountFired = true;
    }

    // 执行子应用的生命周期方法
    return reasonableTime(appOrParcel, "mount")
      .then(() => {
        appOrParcel.status = MOUNTED;

        if (!firstMountFired) {
          window.dispatchEvent(new CustomEvent("single-spa:first-mount"));
          firstMountFired = true;
        }

        return appOrParcel;
      })
      .catch((err) => {
        // If we fail to mount the appOrParcel, we should attempt to unmount it before putting in SKIP_BECAUSE_BROKEN
        // We temporarily put the appOrParcel into MOUNTED status so that toUnmountPromise actually attempts to unmount it
        // instead of just doing a no-op.
        appOrParcel.status = MOUNTED;
        return toUnmountPromise(appOrParcel, true).then(
          setSkipBecauseBroken,
          setSkipBecauseBroken
        );

        function setSkipBecauseBroken() {
          if (!hardFail) {
            handleAppError(err, appOrParcel, SKIP_BECAUSE_BROKEN);
            return appOrParcel;
          } else {
            throw transformErr(err, appOrParcel, SKIP_BECAUSE_BROKEN);
          }
        }
      });
  });
}

4、single-spa 生命周期 hooks

single-spa 定义了一些生命周期 hooks,可以帮助我们在子应用/组件生命周期中执行自定义操作,这些 hooks 包括:

  • single-spa:before-first-mount:第一次挂载子应用/组件之前触发,之后就不会再触发
  • single-spa:first-mount:第一次挂载子应用/组件之后触发,之后就不会再触发
  • single-spa:before-no-app-change:application 模式下,修改 url 会触发子应用的切换。如果路由注册表中没有匹配当前 url 的子应用,那么 single-spa:before-no-app-change 事件会触发
  • single-spa:before-app-change:修改 url 导致子应用切换时,如果路由注册表中有匹配当前 url 的子应用, single-spa:before-app-change 事件会触发。
  • single-spa:before-routing-event:application 模式下, hashchange、popstate 触发以后,single-spa:before-routing-event 事件就会触发。
  • single-spa:before-mount-routing-event:application 模式下, 旧的子应用卸载完成之后,新的子应用挂载之前触发。
  • single-spa:no-app-change:application 模式下,执行performAppChanges方法里面,在single-spa:before-app-change触发以后触发
  • single-spa:app-change:application 模式下,执行performAppChanges方法里面,在single-spa:before-app-change触发以后触发
  • single-spa:routing-event:application 模式下, single-spa:app-change / single-spa:no-app-change 触发以后, single-spa:routing-event 触发。

例如:single-spa——src/lifecycles/mount.js

// single-spa其实在不同的阶段提供了相应的自定义事件,让用户可以做一些事情
    if (!beforeFirstMountFired) {
      window.dispatchEvent(new CustomEvent("single-spa:before-first-mount"));
      beforeFirstMountFired = true;
    }

这样我们可以自定义使用

window.addEventListener('single-spa:before-first-mount', event => {...})

不足

  • single-spa 采用 JS Entry 的方式接入微应用,对微应用的入侵太强
    ○ 微应用路由改造,添加一个特定的前缀
    ○ 微应用入口改造,挂载点变更和生命周期函数导出
    ○ 打包工具配置更改
  • 通信问题
    通过注册微应用时给微应用注入一些状态信息,剩下的只能用户自己去实现,实现方式上面也有提到几种通信方式
  • 资源预加载
    single-spa会将微应用打包成一个js文件
  • js隔离
    js全局对象污染的问题
  • 样式隔离问题
    只能通过约定命名的方式去做规范实现

结尾

single-spa是一个很好的微前端基础框架,阿里的qiankun就是基于single-spa实现的,在它的基础上做了一层封装和解决了一些缺陷。接下来会去学习下qiankun的源码。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/22748.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

看了就能懂的NIO使用深入详解

NIO概述 NIO介绍 传统IO流(java.io):读写操作结束前,处于线性阻塞,代码简单,安全,性能低 NIO&#xff1a;支持非阻塞式编程,性能更有优势,但代码编写较为复杂。 概念理解 同步(synchronous):一条线程执行期间,其他线程就只能等待。 异步(asynchronous):一条线程在执行…

Java基础深化和提高-------多线程与并发编程

目录 多线程与并发编程 多线程介绍 什么是程序&#xff1f; 什么是进程? 什么是线程&#xff1f; 进程、线程的区别 什么是并发 线程和方法的执行特点 方法的执行特点 线程的执行特点 什么是主线程以及子线程 主线程 子线程 线程的创建 通过继承Thread类实现多线程 通过Ru…

暴力美学,拒绝平庸,Alibab开源内部神仙级“K8S核心笔记”下载

各大互联网巨头在技术战略层面&#xff0c;都把云原生列为了主要发展方向。以阿里巴巴为例&#xff0c;他们技术老大说&#xff0c;云原生是云计算释放红利的最短路径&#xff0c;也是企业数字化的最短路径。 现在云原生工程师、Kubernetes 工程师工资都特别高&#xff0c;并且…

大厂光环下的功能测试,出去面试自动化一问三不知

在一家公司待久了技术能力反而变弱了&#xff0c;原来的许多知识都会慢慢遗忘&#xff0c;这种情况并不少见。 一个京东员工发帖吐槽&#xff1a;感觉在大厂快待废了&#xff0c;出去面试问自己接口环境搭建、pytest测试框架&#xff0c;自己做点工太久都忘记了。平时用的时候…

【BLE】蓝牙数据速率

【BLE】蓝牙数据速率 理论速度 物理层 未编码PHY&#xff0c;每位数据使用1个符号表示 1Mbps&#xff08;LE 1M PHY&#xff09; 2Mbps&#xff08;LE 2M PHY&#xff09; 编码PHY 500Kbps&#xff08;S2&#xff09; 125Kbps&#xff08;S8&#xff09; 1Mbps指的是每…

MATLAB改变默认工作路径

软件版本&#xff1a;MATLAB2022a 电脑系统&#xff1a;win10 问题&#xff1a; 每次打开matlab都会自动打开matlab.exe文件夹位置&#xff0c;而不是打开自己新建的工作空间每次都要转换&#xff0c;很麻烦 方法&#xff1a; 1、找到安装目录下的matlabrc.m文件&#xff0…

大事务问题到底要如何解决?

文章目录大事务引发的问题pom依赖解决方法1. 少用Transactional 注解2. 将查询(select)方法放到事务外3. 事务中避免远程调用4. 事务中避免一次性处理太多数据5. 非事务执行6. 异步处理大事务引发的问题 在 分 享 解 决 办 法 之 前 &#xff0c;先 看 看 系 统 中 如 果 出 现…

一款集成ST-link下载及虚拟串口的STM32F103C8T6最小系统板设计

前言 在以前的STM32单片机应用中&#xff0c;经常使用STM32F103C8T6最小系统板&#xff08;小蓝板&#xff09;作为主控。程序下载和串口交互都需要额外器件和接线&#xff0c;程序下载的话要用到ST-link&#xff0c;串口交互用到USB-TTL&#xff0c;常见的样子就下面这…

(历史上最详细的网络)华为初级网络工程师知识点总结(二)工作考研均受益

超级详细网络知识二一&#xff0c;关于IPV4和IPV6地址的介绍&#xff08;重点是IPV4&#xff09;1,IPV4地址的组成2&#xff0c;子网掩码的详解3&#xff0c;IP地址的分类和播的形式4&#xff0c;IP地址的分类可用地址5&#xff0c;IPV4的特殊地址&#xff0c;公网地址&#xf…

信息数据采集软件-什么工具可以快速收集信息

随着时代的不断的进步&#xff0c;我们已经悄然无息地步入了一个大数据信息时代&#xff0c;每个人在互联网上都离不开信息数据的汇总分析以及信息数据的应用&#xff0c;不管是亮化自己的信息数据&#xff0c;还是分析同行详细信息的数据。今天小编就教大家如何用信息抓取软件…

yapi文档转换jmx脚本

需求 需要自动生成接口测试脚本接口文档&#xff08;swagger/yapi/wiki&#xff09;很多&#xff0c;我不想一个一个去复制黏贴到jmeter 期望 一键自动生成接口测试脚本&#xff0c;解放双手&#xff0c;降低纯手力劳动占比&#xff0c;进而给自己提供更多的时间去思考、理解…

第九章:单调栈与单调队列

单调栈与单调队列一、单调栈1、什么是单调栈&#xff1f;2、单调栈的模板&#xff08;1&#xff09;问题&#xff1a;&#xff08;2&#xff09;分析&#xff1a;二、单调队列1、什么是单调队列2、单调队列模板&#xff08;1&#xff09;问题&#xff08;2&#xff09;分析一、…

深入浅出学习透析Nginx服务器的基本原理和配置指南「Https安全控制篇」

Https反向代理 之前的内容中我们主要针对于一些对安全性要求比较高的站点&#xff0c;可能会使用HTTPS&#xff08;一种使用SSL通信标准的安全HTTP协议&#xff09;&#xff0c;针对于HTTP 协议和SSL标准相信大家都知道了&#xff0c;在这里我就不为大家进行介绍了&#xff0c…

共建“医疗合规科技实验室”,美创科技实力护航医疗数据安全

11月15日-17日&#xff0c;由工业和信息化部、深圳市人民政府主办&#xff0c;中国互联网协会、广东省通信管理局、深圳市工业和信息化局等单位承办的2022中国互联网大会隆重召开。 在互联网医疗健康合规发展论坛上&#xff0c;医疗合规科技实验室合作伙伴计划正式启动&#xf…

scau Java综合性实验之Java源程序分析程序

1. 编写一个Java应用程序&#xff0c;实现对某个目录中的所有Java源程序文件&#xff08;包含该目录的子目录中的源程序文件&#xff09;进行统计。统计内容包括&#xff1a; (1) 目录中每个源程序文件的总行数和空白行数&#xff0c;文件的字节数&#xff1b; (2) 目录中所有源…

ADB调试--详细教程(附华为手机无法显示设备解决方法)

终端打开开发者模式&#xff0c;用数据线连接电脑&#xff0c;然后按照下面的步骤操作 1、开启开发者选项&#xff1a; 设置->关于设备->版本号&#xff08;连续点击5次&#xff09; 2、打开USB调试 在开发者选项中&#xff0c;找到USB调试&#xff0c;将此打开。 3、…

作为资深程序民工怎么能被性能优化难倒!原理与实战齐飞,源自大厂自然更专业!

性能优化是一个很复杂的工作&#xff0c;且充满了不确定性。它不像Java业务代码&#xff0c;可以一次编写到处运行(write once, run anywhere)&#xff0c;往往一些我们可能并不能察觉的变化&#xff0c;就会带来惊喜/惊吓。能够全面的了解并评估我们所负责应用的性能&#xff…

全渠道商城授权管控经销商,渠道商管理系统助力医药企业快速扩大渠道规模

随着医改的稳步推进&#xff0c;医药行业传统的以销售为主的扩张模式难以为继&#xff0c;国内药企面临创新转型。如何探寻医药数字化营销方法论&#xff0c;如何把握政策机遇和用户需求&#xff0c;利用数字化推动医药创新渠道破局&#xff0c;已成业内关注的重点。 后疫情时…

如何在win11中用双硬盘或移动硬盘装Ubuntu 20.04 双系统

首先明确一下思路&#xff0c;这个多硬盘的安装方式与单硬盘的方式没什么本质区别 下面介绍具体的方法&#xff1a; 1.下载Ubuntu系统镜像、制作系统盘 1.1 下载镜像 ubuntu20.04镜像下载&#xff1a;ubuntu20.04官网&#xff0c;点击进入下载 现在最新版是 Ubuntu 22.04.1…

ZX297520V3T:Codec NAU88C22驱动调试

一、音频驱动框架 ALSA(Advanced Linux Sound Architecture)是目前linux的主流音频体系结构。ALSA不仅在内核设备驱动层提供了alsa-driver,同时在应用层为我们提供了alsa-lib,应用程序只要调用alsa-lib提供的API,即可以完成对底层音频硬件的控制。为了方便调试,ALSA也提…