微前端:single-spa是什么

背景

我们都知道 qiankun 是基于 single-spa 开发的,今天我们来扒一扒 single-spa 到底做了哪些事。
在这里插入图片描述

微前端架构

一般来说,微前端需要解决的问题分为两大类:

  • 应用的加载与切换:路由问题、应用入口、应用加载
  • 应用的隔离与通信:js隔离、css样式隔离、应用间通信

single-spa 则很好地解决了 路由问题、应用入口 两个问题,但并没有解决应用加载问题,而是将该问题暴露出来由使用者实现(一般可以用 system.js 或原生 script 标签来实现);qiankun在此基础上封装了一个应用加载方案,即 import-html-entry (可以看我写的这篇 微前端:qiankun的依赖import-html-entry的作用),并给出了js隔离、css样式隔离和应用间通信三个问题的解决方案,同时提供了预加载功能。

原理

上面我们知道了,single-spa 则很好地解决了 路由问题、应用入口 两个问题,那么接下来我们就详细的看看其中的原理。

路由问题

single-spa是通过监听hashChange和popState这两个原生事件来检测路由变化的,它会根据路由的变化来加载对应的应用,相关的代码可以在single-spa的 src/navigation/navigation-events.js 中找到:

...
// 139行
if (isInBrowser) {
  // We will trigger an app change for any routing events.
  window.addEventListener("hashchange", urlReroute);
  window.addEventListener("popstate", urlReroute);
...
// 174行,劫持pushState和replaceState
  window.history.pushState = patchedUpdateState(
    window.history.pushState,
    "pushState"
  );
  window.history.replaceState = patchedUpdateState(
    window.history.replaceState,
    "replaceState"
  );

function urlReroute() {
  reroute([], arguments);
}
export function reroute(pendingPromises = [], eventArguments) {
  ...
  // getAppChanges会根据路由改变应用的状态,状态包含4类
  // 待清除、待卸载、待加载、待挂载
  const {
    appsToUnload,
    appsToUnmount,
    appsToLoad,
    appsToMount,
  } = getAppChanges();
  ...
  // 如果应用已启动,则调用performAppChanges加载和挂载应用
  // 否则,只加载未加载的应用
  if (isStarted()) {
    appChangeUnderway = true;
    appsThatChanged = appsToUnload.concat(
      appsToLoad,
      appsToUnmount,
      appsToMount
    );
    return performAppChanges();
  } else {
    appsThatChanged = appsToLoad;
    return loadApps();
  }
  ...
  function performAppChanges() {
    return Promise.resolve().then(() => {
      // 1. 派发应用更新前的自定义事件
      // 2. 执行应用暴露出的生命周期函数
      // appsToUnload -> unload生命周期钩子
      // appsToLoad -> 执行加载方法
      // appsToUnmount -> 卸载应用,并执行对应生命周期钩子
      // appsToMount -> 尝试引导和挂载应用
    })
  }
  ...
}
export function start(opts) {
  started = true;
  if (opts && opts.urlRerouteOnly) {
    setUrlRerouteOnly(opts.urlRerouteOnly);
  }
  if (isInBrowser) {
    reroute();
  }
}
  1. 根据传入的参数activeWhen判断哪个应用需要加载,哪个应用需要卸载或清除,并将其push到对应的数组
  2. 如果应用已经启动,则进行应用加载或切换。针对应用的不同状态,直接执行应用自身暴露出的生命周期钩子函数即可。
  3. 如果应用未启动,则只去下载appsToLoad中的应用。

总的来看,当路由发生变化时,hashChange或popState会触发,这时single-spa会监听到,并触发urlReroute;接着它会调用reroute,该函数正确设置各个应用的状态后,直接通过调用应用所暴露出的生命周期钩子函数即可。当某个应用被推送到appsToMount后,它的mount函数会被调用,该应用就会被挂载;而推送到appsToUnmount中的应用则会调用其unmount钩子进行卸载。

应用入口

single-spa采用的是协议入口,即只要实现了single-spa的入口协议规范,它就是可加载的应用。single-spa的规范要求应用入口必须暴露出以下三个生命周期钩子函数,且必须返回Promise,以保证single-spa可以注册回调函数:

  1. bootstrap
  2. mount
  3. unmount

bootstrap用于应用引导,基座应用会在子应用挂载前调用它。
mount用于应用挂载,就是一般应用中用于渲染的逻辑,即上述的new Vue语句。
unmount用于应用卸载,我们可以在这里调用实例的destroy方法手动卸载应用,或清除某些内存占用等。

let instance = null;
let router = null;

/**
 * 渲染函数
 * 两种情况:主应用生命周期钩子中运行 / 微应用单独启动时运行
 */
function render() {
  // 在 render 中创建 VueRouter,可以保证在卸载微应用时,移除 location 事件监听,防止事件污染
  router = new VueRouter({
    // 运行在主应用中时,添加路由命名空间 /vue
    base: window.__POWERED_BY_QIANKUN__ ? "/vue" : "/",
    mode: "history",
    routes,
  });

  // 挂载应用
  instance = new Vue({
    router,
    render: (h) => h(App),
  }).$mount("#app");
}

// 独立运行时,直接挂载应用
if (!window.__POWERED_BY_QIANKUN__) {
  render()
}

/**
 * bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
 * 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
 */
export async function bootstrap() {
  console.log("VueMicroApp bootstraped");
}

/**
 * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
 */
export async function mount(props) {
  console.log("VueMicroApp mount", props);
  render(props);
}

/**
 * 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
 */
export async function unmount() {
  console.log("VueMicroApp unmount");
  instance.$destroy();
  instance = null;
  router = null;
}

应用加载

那么,我们传一下上面两部分内容,解决了路由问题、应用入口(什么样的应用可以被加载,没有说如何加载),应用是如何进行加载的。single-spa并没有提供自己的解决方案,而是将它开放出来,由开发者提供。

刚刚说了,qiankun 提供了 import-html-entry 来进行加载,那么脱离 qiankun 普通的方式是怎么样的,这里举例用 system.js 来加载

<script type="systemjs-importmap">
  {
    "imports": {
      "app1": "http://localhost:8080/app1.js",
      "app2": "http://localhost:8081/app2.js",
      "single-spa": "https://cdnjs.cloudflare.com/ajax/libs/single-spa/4.3.7/system/single-spa.min.js"
    }
  }
</script>
... // system.js的相关依赖文件
<script>
(function(){
  // 加载single-spa
  System.import('single-spa').then((res)=>{
    var singleSpa = res;
    // 注册子应用
    singleSpa.registerApplication('app1',
      () => System.import('app1'),
      location => location.hash.startsWith(`#/app1`);
    );
    singleSpa.registerApplication('app2',
      () => System.import('app2'),
      location => location.hash.startsWith(`#/app2`);
    );
    // 启动single-spa
    singleSpa.start();
  })
})()
</script>

我们在调用singleSpa.registerApplication注册应用时提供的第二个参数就是加载这个子应用的方法。single-spa会调用这个函数,下载子应用代码并分别调用其bootstrap和mount方法进行引导和挂载。

总结

以上就是single-spa的核心原理,从上面的介绍中不难看出,single-spa只是负责把应用加载到一个页面中,至于应用能否协同工作,是很难保证的。而qiankun所要解决的,就是协同工作的问题。

参考资料

官网
https://zhuanlan.zhihu.com/p/378346507
https://blog.csdn.net/qq_41694291/article/details/113842872

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Lvan的前端笔记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值