前端微服务:single-spa与qiankun的区别

4,672 阅读4分钟

我们在之前的文档中了解到,目前的微前端采用的是组合式应用路由分发。通过父类对路由的检测,动态的对子应用进行卸载或挂载。

在咱们理解原理之前,先看一下如何使用single-spa。我们以vue为例。

首先我们定义两个子项目,以及一个父类基座。

  1. 在子类项目中引入single-spa-vue

    npm install single-spa-vue --save
    
  1. 在子类的main.js中加入single-spa-vue相应的生命周期
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import singleSpaVue from 'single-spa-vue'Vue.config.productionTip = falseconst appOptions = {
  el: '#microApp',
  router,
  render: h => h(App)
}
​
// 支持应用独立运行、部署,不依赖于基座应用
if (!process.env.isMicro) {
  delete appOptions.el
  new Vue(appOptions).$mount('#app')
}
// 基于基座应用,导出生命周期函数
const vueLifecycle = singleSpaVue({
  Vue,
  appOptions
})
// 启动生命周期
export function bootstrap (props) {
  console.log('app2 bootstrap')
  return vueLifecycle.bootstrap(() => {})
}
// 挂载生命周期
export function mount (props) {
  console.log('app2 mount')
  return vueLifecycle.mount(() => {})
}
// 卸载生命周期
export function unmount (props) {
  console.log('app2 unmount')
  return vueLifecycle.unmount(() => {})
}
​
  1. 新建vue.config.js文件,并设置导出格式为umd

    const package = require('./package.json')
    module.exports = {
      // 告诉子应用在这个地址加载静态资源,否则会去基座应用的域名下加载
      publicPath: '//localhost:8082',
      // 开发服务器
      devServer: {
        port: 8082
      },
      configureWebpack: {
        // 导出umd格式的包,在全局对象上挂载属性package.name,基座应用需要通过这个全局对象获取一些信息,比如子应用导出的生命周期函数
        output: {
          // library的值在所有子应用中需要唯一
          library: package.name,
          libraryTarget: 'umd'
        }
      }
    

    经过以上几步操作,一个微前端的子项目基本就创建完成了。

    接下来我们继续来创建父类基座。

    1. 父类基座引入single-spa

      npm install single-spa --save
      
    1. 改造父类main.js

      import Vue from 'vue'
      import App from './App.vue'
      import router from './router'
      import { registerApplication, start } from 'single-spa'Vue.config.productionTip = false// 远程加载子应用
      function createScript(url) {
        return new Promise((resolve, reject) => {
          const script = document.createElement('script')
          script.src = url
          script.onload = resolve
          script.onerror = reject
          const firstScript = document.getElementsByTagName('script')[0]
          firstScript.parentNode.insertBefore(script, firstScript)
        })
      }
      ​
      // 记载函数,返回一个 promise
      function loadApp(url, globalVar) {
        // 支持远程加载子应用
        return async () => {
          // 
          await createScript(url + '/js/chunk-vendors.js')
          await createScript(url + '/js/app.js')
          // 这里的return很重要,需要从这个全局对象中拿到子应用暴露出来的生命周期函数
          return window[globalVar]
        }
      }
      ​
      // 子应用列表
      const apps = [
        {
          // 子应用名称
          name: 'app1',
          // 子应用加载函数,是一个promise
          app: loadApp('http://localhost:8083', 'app1'),
          // 当路由满足条件时(返回true),激活(挂载)子应用
          activeWhen: location => location.pathname.startsWith('/app1'),
          // 传递给子应用的对象
          customProps: {}
        },
        {
          name: 'app2',
          app: loadApp('http://localhost:8082', 'app2'),
          activeWhen: location => location.pathname.startsWith('/app2'),
          customProps: {}
        }
      ]
      ​
      // 注册子应用
      for (let i = apps.length - 1; i >= 0; i--) {
        registerApplication(apps[i])
      }
      ​
      new Vue({
        router,
        mounted() {
          // 启动
          start()
        },
        render: h => h(App)
      }).$mount('#app')
      

      经过上面改造完成之后呢,一个父类基座应用也就创建完毕了。

      那么我们对比一下main.js与改造后的区别

      image-20210920143550690

    我们通过比较可以看出,我们在父类基座中总共新加了这么几个方法或属性。分别是:

    loadApp, createScript, apps。从具体的实现中,我们可看出:

    1. loadApp主要是根据资源地址去请求资源
    2. createScript主要是将请求回来的script添加到页面中。
    3. apps就是一些子类应用的描述。
    4. registerApplication就是将这些子应用注册到single-spa中。

    对比完父类基座的不同,我们再来对比子类的不同吧。

    image-20210920144047313

    我们可以看出,子类最大的不同就是添加了一些single-spa-vue的生命周期。因为微前端的方案,是single-spa的,想用的话,就必须导入这些个生命周期。

    那么看了对比之后,我们在看一张图,就能对微前端的原理豁然开朗。

    2155778-ec1fb1f2f34e459f

从图上,我们可以看出浏览器首次打开父类应用的时候,第一步就是先使用调用registerApplication注册app,接下来判断当前的路由是属于哪一个子应用的,他的判断依据就是apps中的activeWhen,接下来就会将当前的子应用划分状态,appToLoad,appToUnmounted, appToMounted。接下来根据子应用的状态,先去执行需要卸载的子应用,卸载完成之后,就会去执行状态为appToLoad, appToMounted的子应用,那么在最后在执行相应的回调函数,也就是我们在子应用中注册的那些生命周期。

那么我们有single-spa这种微前端解决方案,为什么还需要qiankun呢?

qiankun从0-1k开发

相比于single-spaqiankun他解决了JS沙盒环境,不需要我们自己去进行处理。在single-spa的开发过程中,我们需要自己手动的去写调用子应用JS的方法(如上面的 createScript方法),而qiankun不需要,乾坤只需要你传入响应的apps的配置即可,会帮助我们去加载。还有一个就是他们本质的区别

那么在说这个本质的区别之前呢,我们需要了解一下组合式应用路由分发他也分为两种解决方案,一种是JS entry,另外一种是html entry

JS Entry 的方式通常是子应用将资源打成一个 entry script,但这个方案的限制也颇多,如要求子应用的所有资源打包到一个 js bundle 里,包括 css、图片等资源。除了打出来的包可能体积庞大之外的问题之外,资源的并行加载等特性也无法利用上。

HTML Entry 则更加灵活,直接将子应用打出来 HTML 作为入口,主框架可以通过 fetch html 的方式获取子应用的静态资源,同时将 HTML document 作为子节点塞到主框架的容器中。这样不仅可以极大的减少主应用的接入成本,子应用的开发方式及打包方式基本上也不需要调整,而且可以天然的解决子应用之间样式隔离的问题(后面提到)。

以上就是他们之间的区别。简要概括:qiankun基于single-spa,在single-spa上做了改造,使得接入更加方便。