微前端学习系列(三):qiankun

3,621 阅读22分钟

本文使用「署名 4.0 国际 (CC BY 4.0)」 许可协议,欢迎转载、或重新修改使用,但需要注明来源。

作者: 百应前端团队 @0o华仔o0 首发于 juejin.cn/post/695534…

目录

前言

目前 single-spa 是一种比较流行且成熟的微前端方案,它可以为我们提供类似单页应用的用户体验,做到技术栈无关多应用共存,并且通过其丰富的生态帮我们提高开发效率。但在实际项目中,面对诸如子应用加载应用之间 js、css 的隔离子应用切换遗留的副作用清理子应用状态恢复以及子应用之间通信子应用预加载之类的常见问题,single-spa 框架本身并没有给出对应的解决方案,需要开发人员自己去写代码解决,对开发人员来说不是很友好。

针对上述这些问题,qiankun 提供了一种新的微前端方案。在 single-spa 的基础上,qiankun 做了二次开发,提供了通用的子应用加载、通信、预加载方案,并通过技术手段实现了应用之间的 js、css 隔离以及副作用清理工作状态恢复,帮助开发人员更加简单快捷的实现一个微前端应用。

接下来我们就来了解一下 qiankun 的用法,以及它是怎么来解决上面提到的问题的。

如何使用 qiankun 快速搭建一个微前端应用

由于 qiankun 是在 single-spa 的基础上做的二次开发,所以 qiankun 的用法和 single-spa 基本一样,也分为 application 模式和 parcel 模式。

application 模式是基于路由工作的,它将应用分为两类:基座应用子应用。其中,基座应用需要维护一个路由注册表,根据路由的变化来切换子应用子应用是一个个独立的应用,需要提供生命周期方法基座应用使用。parcel 模式和 application 模式相反,它与路由无关,子应用切换是手动控制的。

接下来,我们通过官方示例 github.com/umijs/qiank…, 来看看 application 模式是如何使用的。

示例的项目结构如图:

image.png

示例启动以后效果如图:

image.png

single-spa 一样,我们需要对基座应用子应用分别做改造。

  • 基座应用改造

    qiankun 基座应用的改造和 single-spa 基本相同,即构建一个路由注册表,然后根据路由注册表使用 qiankun 提供的 registerMicroApps 方法注册子应用,最后执行 start 方法来启动 qiankun

    具体代码如下:

      import { registerMicroApps, start } from 'qiankun';
    
      ...
    
      const apps = [
        {
          name: 'react16',
          entry: '//localhost:7100',
          container: '#subapp-viewport',
          activeRule: '/react16',
          ...
        },
        {
          name: 'react15',
          entry: '//localhost:7102',
          container: '#subapp-viewport',
          activeRule: '/react15',
          ...
        },
        {
          name: 'vue',
          entry: '//localhost:7101',
          container: '#subapp-viewport',
          activeRule: '/vue',
          ...
        },
        {
          name: 'angular9',
          entry: '//localhost:7103',
          container: '#subapp-viewport',
          activeRule: '/angular9',
          ...
        },
        {
          name: 'purehtml',
          entry: '//localhost:7104',
          container: '#subapp-viewport',
          activeRule: '/purehtml',
        },
        {
          name: 'vue3',
          entry: '//localhost:7105',
          container: '#subapp-viewport',
          activeRule: '/vue3',
          ...
        },
      ]
    
      const lifeCycles = {
        beforeLoad: [
          app => {
            console.log('[LifeCycle] before load %c%s', 'color: green;', app.name);
          },
        ],
        beforeMount: [
          app => {
            console.log('[LifeCycle] before mount %c%s', 'color: green;', app.name);
          },
        ],
        afterUnmount: [
          app => {
            console.log('[LifeCycle] after unmount %c%s', 'color: green;', app.name);
          },
        ],
      }
      
      registerMicroApps(apps, lifeCycles);
    
      ...
    
      start();
    

    qiankun 基座应用中的路由注册表的结构和 single-spa 有点类似,每个子应用对应的配置项需要指定 nameentrycontaineractiveRule

    • name

      子应用的唯一标识,是一个字符串不可重复

    • entry

      子应用的入口,一般情况下是一个 url 字符串,即子应用的访问地址,如 localhost:8080;

    • container

      子应用挂载的节点,可以是一个 domElement 实例,也可以是一个 dom 元素的 id 字符串

    • activeRule

      子应用激活的条件,一般是一个函数。路由发生变化时,基座应用会遍历注册的子应用,通过子应用的 activeRule 条件,找到需要激活的子应用。

  • 子应用改造

    子应用的改造,和 single-spa 是完全一样的,涉及两个方面:

    • 入口文件 index.js 添加生命周期方法 - mount、unmount、update 等
    • 打包构建改造

    子应用入口文件 - index 的改造如下:

    ...
    
    function render() { ... }
    
    export function mount(props) { 
        ...
        render();
    }
    
    export function unmount(props) {...}
    
    ...
    

    打包构建改造如下:

    // vue.config.js
    
        module.exports = {
            configureWebpack: {
                ...
                publicPath: 'http://localhost:7101'
                output: {
                    library: 'vue',
                    libraryTarget: 'umd'
                },
                ...
            }
        }
    

完成基座应用子应用的改造以后,启动基座应用,我们即可通过修改 url 来进行子应用的切换

了解完 qiankunapplication 模式外,我们在再了解一下 parcel 模式。

parcel 模式路由无关的,给了我们手动挂载/卸载/更新子应用的机会,具体是通过 qiankun 提供的 loadMicroApp 来实现的。关于 loadMicroApp 的用法,官网 已给了详细说明,在这里就不再提供示例说明了。

常用 API

qiankun 的相关 API,官方文档已经有了详细说明,本文就不再一一说明。下面列出了 qiankun 所有 API 的官网链接,大家可以移步官网去查看。

qiankun 是如何解决 single-spa 在使用过程中遇到的问题的

子应用加载 - Html Entry vs Js Entry

在使用 single-spa 时,最关键的步骤就是在创建路由注册表时,确定子应用的加载方法 - loadAppFunc

通常情况下,我们会将子应用的所有静态资源 - js、css、img等打包成一个 js bundle,然后在 loadAppFunc 中通过加载执行这个 js bundle 的方式,获取子应用提供的生命周期方法,然后执行子应用的 mount 方法来加载子应用。这种方式称为 Js Entry

Js Entry 方式使用起来会比较麻烦,具体表现为:

  • 首先,不同的子应用打包出来的 js bundle 的名称可能会不一样,而且子应用更新也会导致 js bundle 的名称随时会变化,这就使得我们在定义 loadAppFunc 时,必须能动态获取子应用 js bundle 名称(子应用 js bundle 名称得保存起来以便 loadAppFunc 来获取)。

  • 其次,所有的静态资源打包到一起,css 提取资源并行加载首屏加载等优化也就没有了。

  • 最后,为了使得子应用的按需加载功能生效,我们需要在子应用打包过程中,修改相应的配置以补全子应用 js 资源的路径。

qiankun 使用了一种新的子应用加载方式 - html entry,来帮助我们解决 js entry 方式的痛点。

使用 qiankun 时,创建路由注册表依旧是最关键的步骤。但不同的是,我们不再需要给子应用定义加载方法 - loadAppFunc,只需要确定子应用的入口 - entry 即可,子应用加载方法 - loadAppFuncqiankun 会帮我们实现

qiankun 是基于原生 fetch 来实现 loadAppFunc 的。简单来说,就是加载子应用时,qiankun 会根据子应用 entry 配置项指定的 url,通过 fetch 方法来获取子应用对应的 html 内容字符串,然后解析 html 内容,收集子应用的样式js 脚本安装样式并执行 js 脚本来获取子应用的生命周期方法,然后执行子应用的 mount 方法。

整个详细过程,可以通过下面的流程图来了解:

image.png

通过上述的过程,我们可以拿到子应用所有的 js 脚本解析以后的 html 模板,然后把解析以后的 html 模板添加到 container 指定的节点上,然后手动触发所有 js 脚本的执行,这样就可以拿到子应用的生命周期方法 - mount,然后挂载子应用。

对比 Js EntryHtml Entry 的优点如下:

  • 不需要将子应用打包成一个 js bundlecss 提取资源并行加载首屏加载优化可以照常使用(通过 fetch 获取外部样式表的内容是并行的);

  • 不用关心子应用 js 文件的名称以及子应用更新以后导致 js 文件名发生变化

  • 不用为按需加载功能特别修改打包配置(qiankun 在懒加载 js 脚本的时候会基于 entry 配置项自动补全 url);

结合上面 Html Entry 的工作流程图,我们再来梳理一下 qiankun 是如何工作的:

image.png

最后,提一个问题。为什么 qiankun 要将外部样式表转化成内部样式表添加到 html 模板中并且要把 js 脚本收集起来手动执行呢?这样做有什么意义呢?

其实,之所以 qiankun 会这样做,是为了能通过技术手段来解决 jscss 隔离。接下来我们就在 js 隔离css 隔离 章节中具体分析。

js 隔离

使用微前端方案时,我们要确保应用之间全局变量不能互相影响。比如,子应用 A 给 window 对象定义了新的属性 - globalState,子应用 B 也给 window 对象定义了新的属性 globalState,那我们就需要确保子应用 A 和子应用 B 能获取到各自定义的 globalState。

single-spa 中,我们一般通过人为约定添加命名前缀的方式来进行 js 隔离,如子应用 A 给 window 对象定义了新的属性 - A_globalState,子应用 B 给 window 对象定义了新的属性 B_globalState。这种方式低效不说,还容易引发 bug。相比这个方式,qiankun 提供了更好的方式 - 沙盒(sandbox), 来帮助我们实现 js 隔离

沙盒(英语:sandbox,又译为沙箱),计算机术语,是计算机安全领域中的一种安全机制,可以运行中的程序提供的隔离环境。通过沙盒qiankun 为每个子应用,提供了隔离的运行环境,保证子应用的 js 代码在执行时使用的全局变量都是独属于当前子应用的。

qiankun 实现 sandbox 的原理其实很好理解,简单来说就是:

  • 第一步,为每一个子应用创建一个唯一类 window 对象

  • 第二步,手动执行子应用的 js 脚本,将类 window 对象作为全局变量,对全局变量的读写都作用在类 window 对象上;

    在这一步,html entry 阶段解析出来的所有 js 脚本字符串 在执行时会先使用一个 IIFE - 立即执行函数包裹,然后通过 eval 方法手动触发,如下:

    var fakeWindowA = { name: 'appA'}; // 子应用 appA 对应的类 window 对象
    var fakeWindowB = { name: 'appB'}; // 子应用 appB 对应的类 window 对象
    var jsStr = 'console.log(name)'; // 子应用 appA、appB 的都有的脚本字符串
    var codeA = `(function(window){with(window){${jsStr}}})(fakeWindowA)`; 
    var codeB = `(function(window){with(window){${jsStr}}})(fakeWindowB)`;
    eval(codeA); // appA
    eval(codeB); // appB
    

这样,通过上述的两个步骤,每个子应用的 js 代码在执行时使用的全局变量都是独属于当前子应用的,不会互相影响。

了解完 sandbox 的原理以后,我们具体来看一下 qiankun 是如何实现 sandbox 的。相关源代码如下:

class ProxySandbox {
    ...
    
    constructor(name) {
        // 以子应用的名称作为沙盒的名称
        this.name = name;
        const self = this;
        // 获取原生的 window 对象
        const rawWindow = window;
        // 假的 window 对象
        const fakeWindow = {};
        // 在这里,qiankun 之所以要使用 proxy,主要是想拦截 fakeWindow 的读写等操作
        // 比如,子应用中要使用 setTimeout 方法,fakeWindow 中并没有,就需要从 rawWindow 获取
        this.proxy = new Proxy(fakeWindow, {
            set(target, key, value) {
                if (self.sandboxRunning) { // 沙盒已经激活
                    ...
                    // 子应用新增/修改的全局变量都保存到对应的fakeWindow
                    target[key] = value;
                }
            },
            get(target, key) {
                ...
                // 读取属性时,先从 fakeWindow 中获取,如果没有,就从 rawWindow 中获取
                return key in target ? target[key] : rawWindow[key];
            },
            ...
        });
        
        
    }
}

qiankun 在实现 sandbox 时,先构建一个空对象 - fakeWindow 作为一个假的 window 对象,然后在 fakeWindow 的基础上通过原生的 Proxy 创建一个 proxy 对象,这个 proxy 最后会作为子应用 js 代码执行时的全局变量。有了这个 proxy,我们就可以很方便的劫持 js 代码中对全局变量的读写操作。当子应用中需要添加(修改)全局变量时,直接在 fakeWindow 中添加(修改);当子应用需要从全局变量中读取某个属性(方法)时,先从 fakeWindow 中获取,如果 fakeWindow 中没有,再从原生 window 中获取。

当然,针对不支持 proxy 的浏览器,qiankun 也提供了相应的解决方案,具体实现如下:

class SnapshotSandbox {
    ...
    name: string;  // 子应用的名称
    proxy: WindowProxy; // 沙盒对应的 proxy 对象
    sandboxRunning: boolean; // 判断沙盒是否激活
    private windowSnapshot!: Window; // window 对象的快照
    private modifyPropsMap: Record<any, any> = {}; // 收集修改的 window 属性
    
    constructor(name: string) {
        this.name = name;
        this.proxy = window;
        this.type = SandBoxType.Snapshot; // 快照类型
     }
     
     // 沙盒的激活方法
     active() {
        // 记录当前快照
        this.windowSnapshot = {} as Window;
        // 遍历 window 对象的属性,把 window 对象可枚举的属性添加到 windowSnapshot 中
        iter(window, (prop) => {
          this.windowSnapshot[prop] = window[prop];
        });

        // 恢复之前的变更
        Object.keys(this.modifyPropsMap).forEach((p: any) => {
          window[p] = this.modifyPropsMap[p];
        });

        this.sandboxRunning = true;
     }
     // 沙盒的失活方法
     inactive() {
        this.modifyPropsMap = {};

        iter(window, (prop) => {
          if (window[prop] !== this.windowSnapshot[prop]) {
            // 记录变更,恢复环境
            this.modifyPropsMap[prop] = window[prop];
            window[prop] = this.windowSnapshot[prop];
          }
        });
        
        ...

        this.sandboxRunning = false;
     }
}

SnapshotSandbox 称为快照沙盒qiankun 在实现 SnapshotSandbox 时,也是先创建一个 fakeWindow 作为假的 window 对象,这个 fakeWindow 最后会作为子应用 js 代码执行时的全局变量。由于不支持 proxy(也不支持 setter/getter),所以 qiankun 将原生 window 上的属性、方法全部拷贝了一份到 fakeWindow,以便子应用在读取全局变量时,可以在 fakeWindow 中全部获取到。

另外,除了 ProxySandboxSnapshotSandboxqiankun 还提供了另外一种 sandbox - SingularProxySandbox单例沙盒单例沙盒,是 qiankun 在启用单例模式(父应用只有一个子应用挂载)时,会自动创建。SingularProxySandbox 也是基于 proxy 实现的。但是和 ProxySandbox 不同,SingularProxySandbox 是在原生 window 对象上直接修改属性的,这会导致父子应用之间全局变量的互相影响。目前,不管是单子应用还是多子应用qiankun 默认都使用 ProxySandboxSingularProxySandbox 只有我们我们在 start 方法中显示配置 { sandbox: {loose: true }} 才会使用。

最新版本的 qiankun 中,SingularProxySandbox 会逐步废弃, 改为使用 ProxySandbox

综上,由于 qiankun 使用了 sandbox,各个子应用在工作工程中都会有各自独立的全局变量,不会修改原生的 window 对象,因此父子应用、多子应用之间都能保证全局变量互不影响。

最后,我们再来解释一下为什么 html entry 解析子应用 html 模板字符串时需要将所有的 js 脚本收集起来然后手动触发了。之所以这么做,是为了动态修改 js 脚本执行时的全局对象,保证每个子应用的 js 脚本执行时的全局对象都是各自独立的 fakeWindow,而不是原生的 window

css 隔离

js 隔离一样,qiankun 也通过技术手段实现了 css 隔离

具体的方式有两种:严格样式隔离scoped 样式隔离

  • 严格样式隔离

    启动严格样式隔离,我们需要在使用 start 方法时添加 strictStyleIsolation 配置项,即:

    import { start } from 'qiankun';
    
    start({
        sandbox: {
           strictStyleIsolation: true 
        }
    })
    

    严格样式隔离,默认情况下是关闭的。如果需要开启,必须显示配置

    严格样式隔离,是基于 Web Componentshadow Dom 实现的。通过 shadow Dom, 我们可以将一个隐藏的、独立的 dom 附加到一个另一个 dom 元素上,保证元素的私有化,不用担心与文档的其他部分发生冲突。

    具体的实现如下:

      ...
      if (appElement.attachShadow) {
        shadow = appElement.attachShadow({ mode: 'open' });
      } else {
        // createShadowRoot was proposed in initial spec, which has then been deprecated
        shadow = (appElement as any).createShadowRoot();
      }
      shadow.innerHTML = innerHTML;
      ...
    

    其中,appElement 为子应用挂载的 dom 节点, innerHTMLHtml Entry 解析生成的 html 模板字符串。 通过 shadow dom,可自动实现父子应用、多个子应用之间的样式隔离

  • scoped 样式隔离

    qiankun 实现样式隔离的另一种方式为 scoped 样式隔离,具体的用法如下:

    import { start } from 'qiankun';
    
    start({
        sandbox: {
           experimentalStyleIsolation: true 
        }
    });
    

    scoped 样式隔离,是基于属性选择器实现的,具体如下:

    div["data-qiankun=vue"] div {
        background-color: green;
    }
    

    html entry 解析以后的 html 模板字符串,在添加到 container 指定的节点之前,会先包裹一层 div,并且为这个 div 节点添加 data-qian 属性,属性值为子应用的 name 属性;然后遍历 html 模板字符串中所有的 style 节点,依次为内部样式表中的样式添加 div["data-qiankun=xxx"] 前缀qiankun 中子应用的 name 属性值是唯一的,这样通过属性选择器的限制,就可实现样式隔离

    到这里,我们就可以解释为什么 html entry 解析子应用 html 模板字符串时需要将外部样式表转化为内部样式表了。如果不转化,我们是无法为外部样式表中的样式添加属性选择器前缀的,也就无法实现样式隔离了。

    严格样式隔离,即使是外部样式表,也可以实现样式隔离,因此不需要将外部样式表转化为内部样式表。另外,严格样式隔离和 scoped 样式隔离不能同时使用,当两者对应的配置项都为 true 时,严格样式隔离的优先级更高

在实际的项目中,我们常常会遇到动态添加 style 的情形,比如没有进行 css 提取react 应用使用 styled-components 等。通常,这些动态添加的 style 会通过 document.head.appendChild 的方式添加到 head 节点中。此时,如果 qiankun 启用严格样式隔离或者 scoped 样式隔离,那 css 隔离是不是会失效呢?

答案是不会的。

qiankun 针对动态添加 style 的情形,也做了相应的处理。为了能获知子应用动态添加 style 的操作qiankundocument.head.appendChild 方法进行了劫持操作,具体如下:

// 原生的 appendChild 方法
const rawHeadAppendChild = document.head.appendChild;
// 重写原生方法
document.head.appendChild = function(newChild) {
    if (newChild.tagName === 'STYLE') {
        // 对 style 节点做处理
        ...
    
    }
    ...
    // 找到子应用对应的 html 片段的根 dom 节点
    const mountDOM = ....;
    // 通过原生的 appendChild 将动态 style 添加到子应用对应的 html 片段中
    rawHeadAppendChild.call(mountDOM, newChild);
}

当子应用调用 document.head.appendChild 动态添加 style 时,会被 qiankun 劫持,然后将 style 添加到子应用对应的 html 片段中。此时如果 qiankun 配置了严格样式隔离,新增的 style 是添加到 shadow dom 中的,css 隔离自然生效;如果 qiankun 配置了 scoped 样式隔离,在将 style 添加到子应用对应的 html 片段之前,会先获取到样式内容,然后为样式内容添加 div["data-qiankun=xxx"] 前缀css 隔离也生效。

子应用卸载副作用清理

每个子应用在工作过程中,或多或少都会产生一些副作用,如 setInterval 生成的定时器widnow.addEventListener 注册的事件修改全局变量 window动态添加 dom 节点等。如果在子应用卸载的时候,不对这些副作用进行处理,那么将会造成内存泄漏,甚至会对下一个子应用造成影响副作用的处理相当重要,然而在实际的项目中,如果靠我们开发人员自行处理,不仅费时费力,还不能面面俱到。

幸运的是 qiankun 可以帮我们在子应用卸载时及时处理副作用

首先是修改全局变量引发的副作用。由于 qiankun 使用了 sandbox 机制,每个子应用工作过程中都有各自独立的全局变量,不会修改 window,因此不会出现修改全局变量 window 的副作用,也就不用处理了,😊。

其次是动态添加 dom 节点引发的副作用。由于 qiankun 劫持了 document.head.appendChilddocument.body.appendChilddocument.head.insertBefore, 动态添加的 dom 节点都是自动添加到子应用对应的 html 片段中。当子应用卸载时,子应用对应的 html 片段会自动移除,动态添加的 dom 节点自然也就一起移除了,😊。

关于 setInterval 引发的副作用,qiankun 是通过劫持原生的 setInterval 方法来解决的,具体代码如下:

const rawWindowInterval = window.setInterval;
const rawWindowClearInterval = window.clearInterval;

function patch(global: Window) {
  // 收集子应用定义的定时器
  let intervals: number[] = [];
  // 重写原生的 clearInterval
  global.clearInterval = (intervalId: number) => {
    intervals = intervals.filter((id) => id !== intervalId);
    return rawWindowClearInterval(intervalId);
  };
  // 重写原生的 setInterval
  global.setInterval = (handler: Function, timeout?: number, ...args: any[]) => {
    const intervalId = rawWindowInterval(handler, timeout, ...args);
    intervals = [...intervals, intervalId];
    return intervalId;
  };
  // free 函数在子应用卸载时调用
  return function free() {
    intervals.forEach((id) => global.clearInterval(id));
    global.setInterval = rawWindowInterval;
    global.clearInterval = rawWindowClearInterval;

    return noop;
  };
}

通过劫持 setInterval,子应用生成的定时器都会被收集,当子应用卸载时,收集的定时器会自动被 qiankun 清除掉。

setInterval 一样, window.addEventListener 引发的副作用qiankun 也是通过劫持原生的 window.addEventListenerwindow.removeEventListener 来处理的。子应用在工作过程中绑定的事件都会被收集,当子应用卸载时,收集的事件会自动被 qiankun 解绑。

子应用重新挂载状态恢复

在实际的微前端项目中,我们除了要在子应用卸载时清除副作用,还需要在子应用重新挂载时恢复子应用的状态

子应用重新加载时,需要恢复的状态包括:

  • 子应用修改的全局变量
  • 子应用动态添加的 style

有些时候,我们需要在子应用重新加载时恢复上一次子应用对全局变量的修改。关于这一点,qiankunsandbox 可以帮完美实现这一点。每个子应用都有一个独属于自己的 fakeWindow,这个 fakeWindow 会一直伴随子应用存在(子应用卸载时也存在)。子应用所有对全局变量的修改,实际上都发生在 fakeWindow 上,当子应用重新挂载时,全局变量自动恢复

在解释恢复动态添加 style 这一点时,我们先通过一个使用 webpack 打包的子应用为例来解释一下为什么子应用重新挂载时需要恢复之前动态添加的 style

webpack 打包子应用时,会将每一个子应用中的组件转化为一个可执行函数,这个可执行函数会返回组件对外的 export。子应用第一次启动执行 js 脚本时,会先执行组件的可执行函数,得到组件的 export 并缓存起来。这个收集组件 export 的缓存会伴随着子应用一直存在。当子应用重新挂载时,可以直接从缓存中获取组件的 export不用再执行组件的可执行函数

另外,webpack 打包子应用时,如果未使用 MiniCssExtractPlugin 提取 css,那么项目中所有的 css 会经过 css-loaderstyle-loader 处理以后也会转化为一个可执行方法,执行这个方法会将动态创建一个 style,并通过 document.head.appendChild 的方式添加到页面中。这个 css 可执行方法,只有在组件可执行方法执行时才会执行,这就意味着子应用重新挂载时 css 可执行方法不会触发,样式也就不会动态添加

综上,所以我们需要在子应用重新挂载时恢复之前动态添加的 style

关于这一点, qiankun 是这样做的:

  • 第一,qiankun 会劫持 document.head.appendChild 方法。当子应用第一次挂载时,遇到动态添加 style 的操作,会被劫持新增的 style 会被缓存起来。这个缓存会一直伴随子应用存在。
  • 第二,子应用重新挂载,将之前缓存的 style 添加到子应用对应的 html 片段中。

子应用通信

父子应用、多个子应用相互通信方面qiankun 也提供了一套完整的方案,不需要开发人员自己实现。

使用方式如下:

// 主应用

import { initGlobalState, MicroAppStateActions } from 'qiankun';

// 初始化 state
const actions: MicroAppStateActions = initGlobalState(state);

actions.onGlobalStateChange((state, prev) => {
  // state: 变更后的状态; prev 变更前的状态
  console.log(state, prev);
});
actions.setGlobalState(state);
actions.offGlobalStateChange();


// 子应用
export function mount(props) {
  props.onGlobalStateChange((state, prev) => {
    // state: 变更后的状态; prev 变更前的状态
    console.log(state, prev);
  });

  props.setGlobalState(state);
}

qiankun 提供的 通信机制 是基于发布订阅模式实现的,非常好理解。主应用通过 initGlobalState 方法创建一个全局的 globalState,并维护一个 deps 列表来收集订阅订阅方法 onGlobalStateChange修改 globalState 的 setGlobalState 方法,会在子应用的生命周期方法执行时传递给子应用。子应用首先通过 onGlobalStateChange 方法绑定 callback,该 callback 会添加到 golbalStatedeps 列表中。当我们通过 setGlobalState 方法修改 globalState 时,qiankun 会遍历 deps 列表,依次触发收集的 callback

子应用卸载时,绑定的 callback 会被卸载,从 deps 列表中移除

子应用预加载

qiankun资源预加载方面也给我们提供一套完整的方案,具体的用法如下:

import { start } from 'qiankun';

...

start({
    prefetch: true // prefetch: boolean | 'all' | string[] | function
})

prefetch 根据配置项的不同,提供了不同的资源预加载策略

  • boolean是否开启资源预加载。如果配置为 trueqiankun 会在第一个子应用挂载完成以后,开始加载其他子应用的静态资源;
  • 'all': qiankun 执行 start 方法以后立即开始预加载所有子应用的静态资源,一般不推荐使用;
  • string[]: qiankun 会在第一个子应用挂载完成以后,预加载指定的子应用
  • function: 用户完全自定义子应用资源的加载时机

qiankun 的整个工作过程

在了解了 qiankun 的用法以及 qiankunsingle-spa 基础上做出的相关优化以后,我们来对 qiankun 的整个工作过程做一个全面梳理,如下:

image.png

结束语

到这里,qiankun 的学习就结束了。本文主要对 qiankun 的用法整个工作流程以及对 single-spa 的改进做了详细介绍。如果你还没有使用过 qiankun 或者对 qiankun 是什么还没有一个了解,那么阅读此文可能对你用处不大,可以移步官网先做一个简单了解。本文的篇幅较长,阅读起来比较花时间,希望能给到大家帮助,如果有疑问或者错误,欢迎大家提出。共同学习,一起进步,😁。

参考资料如下: