简述 qiankun 的通信方式

简述 qiankun 的通信方式

qiankun 是蚂蚁集团开源的一个基于 single-spa 的微前端实现库, 也是目前主流的微前端解决方案之一。针对微前端的定义,qiankun 的具体使用就不多做解释。

既然涉及到微前端,那么就肯定就会存在多个应用,就会存在应用间的数据通信,那么实现通信的手段有哪些呢?

  1. actions 方式:qiankun 提供的数据共享的方式(qiankun3 要移除该 api)
  2. props 方式: 利用 props 进行传递
  3. storage 方式:利用浏览器的本地存储方式,实现数据共享
  4. router 方式:在 url 上添加一些信息,实现数据共享

这里主要细说前面两种通信方式。后面的两种方式局限性很大,是一种可以实现数据通信的思路。

actions 方式

qiankun 内部提供了一种 actions 的通信方式,是利用发布订阅者模式实现的。主要的实现思路如下:

  1. 主应用通过 initGlobalState() 创建一个全局的 globalState,并维护一个 deps 列表来收集订阅(也就是利用 onGlobalStateChange 函数利用参数来收集的回调函数 callback)。
  1. 其中订阅方法 (onGlobalStateChange)修改方法 (setGlobalState) 也会在子应用的生命周期方法执行时传递给子应用。
  1. 当通过 setGlobalData 修改全局 globalState 时,内部就会遍历 deps,依次执行回调函数 callback。

代码演示

主应用:Vue,子应用:React

第一步:在主应用中创建一个文件 actions.ts,并在入口文件进行导入。

// actions.ts
import { initGlobalState } from "qiankun";
import type { MicroAppStateActions } from "qiankun";

// 共享全局主题和语言
export interface GlobalState {
  theme: "light" | "dark";
  language: "en" | "zn";
}

export const globalState: GlobalState = {
  theme: "light",
  language: "zn",
};

// 其实这里写不写类型都一样的,因为 state 内部的类型:Record<string, any>,根本都不取决于你的类型
const actions: MicroAppStateActions = initGlobalState(globalState);

export default actions;

导出的 actions 是一个对象,其内部存在如下函数:

  • onGlobalStateChange: 监听 globalState 发生变化
  • setGlobalState: 修改 globalState
  • offGlobalStateChange: 取消监听

它们的使用方式还是比较简单的,看看官网上的案例

第二步:在主应用中,修改全局 globalState

在创建 actions 时, globalState 是静态的,要使其变成动态的。

那么就需要与全局组件的变量关联起来,或者与状态管理库(pinia、vuex等)关联起来(这里以 pinia 为例)。

<!-- App.vue -->

<script setup lang="ts">
  import { watchEffect } from 'vue'
  import { storeToRefs } from 'pinia'
  import actions from '@/utils/actions'
  
  const languageStore = useLanguageStore()
  const themeStore = useThemeStore()
  const { language } = storeToRefs(languageStore)
  const { theme } = storeToRefs(themeStore)
  
  // 获取 pinia 中的数据,绑定在 globalState 中
  watchEffect(() => {
    actions.setGlobalState({theme: theme.value, language: language.value})
  })
  
  // 监听子应用改变 globalState,然后改变主应用的状态变量
  onMounted(() => {
    actions.onGlobalStateChange((state, prevState) => {
      languageStore.changeLanguage(state.language)
      themeStore.changeTheme(state.theme)
    })
  })
 
</script>

针对主应用使用 onGlobalStateChange 方法,主要在全局组件中使用(类似 App),而不要去路由组件中使用,每次切换到该路由,都会重新执行 onGlobalStateChange 方法,那么就会造成类似如下的警告 ⚠️:

qiankun] 'global-xxxx' global listener already exists before this, new listener will overwrite it.

虽然警告不是错,但是能避免就避免。

第三步:在子应用中,接受 actions 的通信方法。

在前面提及到,执行子应用生命周期函数时,就会把通信方法通过 props 的形式传递下去,所以子应用也需要接收一下。优化:为了整个子应用统一使用,所以还是需要抽离一个文件(actions.ts)出来,专门保存通信方法。

// actions.ts
export type OnGlobalStateChangeCallback = (
  state: Record<stringany>,
  prevState: Record<stringany>
) => void;

export type IGlobalStateChange = {
  callback: OnGlobalStateChangeCallback;
  fireImmediately?: boolean;
};

export type MicroAppStateActions = {
  onGlobalStateChange: (
    callback: OnGlobalStateChangeCallback,
    fireImmediately?: boolean
  ) => void;
  setGlobalState: (state: Record<stringany>) => boolean | void;
  offGlobalStateChange?: () => boolean;
};



class Actions {
  actions: MicroAppStateActions;
  constructor() {
    this.actions = {
      onGlobalStateChange: () => {},
      setGlobalState: () => {},
    };
  }
  /**
   * 覆盖默认actions(从子应用的 mount 方法中调用)
   * @param actions MicroAppStateActions
   */

  setActions(actions: MicroAppStateActions) {
    this.actions = actions;
  }

  /**
   * onGlobalStateChange 映射
   * @param callback: {OnGlobalStateChangeCallback}
   * @param fireImmediately: {boolean} 是否立即执行
   * @returns
   */

  onGlobalStateChange(
    callback: OnGlobalStateChangeCallback,
    fireImmediately: boolean = false
  ) {
    return this.actions.onGlobalStateChange(callback, fireImmediately);
  }

  /**
   * setGlobalState 映射
   * @param state: {Record<string, any>}
   * @returns {boolean}
   */

  setGlobalState(state: Record<stringany>) {
    return this.actions.setGlobalState(state);
  }
}

const actions = new Actions();

export default actions;

在子应用的生命周期中,接受通信方法并保存。

// 这里采用的 vite,使用的是 vite-plugin-qiankun 插件提供的方法
renderWithQiankun({
  bootstrap() {
    console.log("vue child: bootstrap");
  },
  mount(props: any) {
    // 保存通信方法(props 里面存在)
    actions.setActions(props);
    const { container } = props;
    render(container);
  },
  unmount() {
    app.unmount();
  },
  update() {},
});

这样在子应用使用通信方法,都是同一个 Actions 类的实例对象上方法。

第四步:在子应用中使用变量,和修改,监听等一系列操作。

import { useEffect, FC } from 'react'
import actions from '@/utils/actions'

const App: FC = () => {
useEffect(() => {
  // 第二参数传递为 true, 表示立即执行  
     actions.onGlobalStateChange((state) => {
       // 把 state 保存在状态管理 store 中
    }, true)
  }, [])
}

当在子应用修改共享数据时,不仅需要修改状态管理 store 的数据,也需要通过 actions.setGlobalState()改变 globalState。

这就是 actions 方式的大致使用流程。

不足之处

  1. 该方式,在 qiankun3.0 版本会被遗弃。

简述 qiankun 的通信方式

  1. 维护过于麻烦。针对一些数据,不是保存在全局的 state 中,就是保存在 store。但是为了实现数据共享,却要把这些数据也要维护在 globalState 中 ,就相当于两份相同的数据在不同的地方保存,维护稍显麻烦。

props 方式

前面的 actions 方式是 qiankun 内部基于发布订阅者实现的功能。其实,无论是 redux 还是 vuex / pinia,一个完整的状态管理库都包含发布订阅的能力。所以就可以采用其他的方式来代替 actions 这种方式,甚至是自己写一个发布订阅者的类似功能代替也行。

在使用 registerMicroApps 注册路由时,也可以手动指定 props 来给子应用传递数据。那么也就可以利用这一特性,实现我们自己的发布订阅者功能,实现应用之间的相互通信。

这里继续以 pinia 为例。

第一步:主应用传递 props

import { registerMicroApps, start } from "qiankun"
import { useShareStore } from "@/store/useShareStore"
import { storeToRefs } from "pinia"

export default function registerAndStartQiankun({
  // useShareStore 收集所有要共享的 store, 下面会解释到
  const shareStore = useShareStore()
  const shareData = storeToRefs(shareStore)
  // 订阅函数
  const onChangeStore = (fn: (store: any) => void) => {
    // 采用 store 内部提供的订阅函数 $subscribe, 收集依赖 fn, 当 store 发生变化时,就会执行 fn 
    return shareStore.$subscribe(() => {
      fn(shareData)
    })
  }

  registerMicroApps([
    {
      name: "react_demo"
      entry: import.meta.env.VITE_SUB_ENTRY, 
      container: "#__qiankun_container"
      activeRule: "/react"
      // 利用 props 的机制,传递共享的数据,订阅函数  
      props: {
        store: {...shareData, shareStore.changeTheme},
        onChangeStore,
      },
    },
  ])
  start({ prefetch: "all" })
}

useShareStore.ts

收集要共享数据的 store,存放在一个 store 中,实现一个订阅者函数。不然就会存在多个订阅者函数。

当然这个设计有关。如果提前设计,把共享的数据放在一个 store 里面,就没有这一步的操作。

import { defineStore, storeToRefs } from "pinia"
import { useLanguageStore } from "./useLanguageStore"
import { useUserStore } from "./useUserStore"
import { ref } from "vue"
import { Theme } from "@/data"

export const useShareStore = defineStore("share"() => {
  const languageStore = storeToRefs(useLanguageStore())
  const userStore = storeToRefs(useUserStore())
  const theme = ref<Theme>(Theme.LIGHT)
  const changeTheme = (isDark: boolean) => {
    theme.value = isDark ? Theme.DARK : Theme.LIGHT
  }
  return {
    token: userStore.token,
    userInfo: userStore.userInfo,
    language: languageStore.language,
    theme,
    changeTheme,
  }
})

第二步:子应用接收 props

// 获取主应用的数据(包含共享数据,和修改共享数据的方法),放到子应用的 store 中(RTK)
function handleParentStore(parentStore: any{
  const { userInfo, token, theme, language } = parentStore
  // 保存在 store 的逻辑
}

function initQiankun({
  renderWithQiankun({
    bootstrap() {
      console.log("react: bootstrap")
    },
    mount(props: any) {
      // 接受props, 并保存
      handleParentStore(props.store)
      // 调用主应用的订阅函数,依赖收集,当依赖发生变化,就会执行
      props.onChangeStore?.((parentStore: any) => {
        handleParentStore(parentStore)
      })
      render(props)
    },
    unmount() {
      root.unmount()
    },
    update() {
      console.log("react: update")
    },
  })
}

qiankunWindow.__POWERED_BY_QIANKUN__ ? initQiankun() : render({})

也可以实现应用之间的通信。

总结

通信方式的选择有多种,就看自己的选择。

当然针对上面的两种方式,更加推荐 props 的方式,书写简单,思路清晰,还不用维护额外的数据。

只有其他的通信方式,storage 方式,router 方式都可以自己尝试。针对开发,怎么简单怎么来。


原文始发于微信公众号(石膏银程序员):简述 qiankun 的通信方式

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/187187.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!