qiankun
是蚂蚁集团开源的一个基于 single-spa
的微前端实现库, 也是目前主流的微前端解决方案之一。针对微前端的定义,qiankun 的具体使用就不多做解释。
既然涉及到微前端,那么就肯定就会存在多个应用,就会存在应用间的数据通信,那么实现通信的手段有哪些呢?
- actions 方式:qiankun 提供的数据共享的方式(qiankun3 要移除该 api)
- props 方式: 利用 props 进行传递
- storage 方式:利用浏览器的本地存储方式,实现数据共享
- router 方式:在 url 上添加一些信息,实现数据共享
这里主要细说前面两种通信方式。后面的两种方式局限性很大,是一种可以实现数据通信的思路。
actions 方式
qiankun 内部提供了一种 actions 的通信方式,是利用发布订阅者模式实现的。主要的实现思路如下:
- 主应用通过
initGlobalState()
创建一个全局的 globalState,并维护一个 deps 列表来收集订阅(也就是利用 onGlobalStateChange 函数利用参数来收集的回调函数 callback)。
- 其中订阅方法 (onGlobalStateChange) ,修改方法 (setGlobalState) 也会在子应用的生命周期方法执行时传递给子应用。
- 当通过 setGlobalData 修改全局 globalState 时,内部就会遍历 deps,依次执行回调函数 callback。
代码演示
主应用:Vue,子应用:React
第一步 :在主应用中创建一个文件 actions.ts
,并在入口文件进行导入。
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
: 修改 globalStateoffGlobalStateChange
: 取消监听
它们的使用方式还是比较简单的,看看官网上的案例
第二步:在主应用中,修改全局 globalState
在创建 actions 时, globalState 是静态的,要使其变成动态的。
那么就需要与全局组件的变量
关联起来,或者与状态管理库(pinia、vuex等)
关联起来(这里以 pinia 为例)。
html
<!-- 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 方法,那么就会造成类似如下的警告 ⚠️:
bashqiankun] 'global-xxxx' global listener already exists before this, new listener will overwrite it.
虽然警告不是错,但是能避免就避免。
第三步:在子应用中,接受 actions 的通信方法。
在前面提及到,执行子应用生命周期函数时,就会把通信方法通过 props 的形式传递下去,所以子应用也需要接收一下。优化:为了整个子应用统一使用,所以还是需要抽离一个文件(actions.ts
)出来,专门保存通信方法。
ts
// actions.ts
export type OnGlobalStateChangeCallback = (
state: Record<string, any>,
prevState: Record<string, any>
) => void;
export type IGlobalStateChange = {
callback: OnGlobalStateChangeCallback;
fireImmediately?: boolean;
};
export type MicroAppStateActions = {
onGlobalStateChange: (
callback: OnGlobalStateChangeCallback,
fireImmediately?: boolean
) => void;
setGlobalState: (state: Record<string, any>) => 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<string, any>) {
return this.actions.setGlobalState(state);
}
}
const actions = new Actions();
export default actions;
在子应用的生命周期中,接受通信方法并保存。
ts
// 这里采用的 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 类的实例对象上方法。
第四步:在子应用中使用变量,和修改,监听等一系列操作。
tsx
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 方式的大致使用流程。
不足之处
- 该方式,在 qiankun3.0 版本会被遗弃。
- 维护过于麻烦。针对一些数据,不是保存在全局的 state 中,就是保存在 store。但是为了实现数据共享,却要把这些数据也要维护在 globalState 中 ,就相当于两份相同的数据在不同的地方保存,维护稍显麻烦。
props 方式
前面的 actions 方式是 qiankun 内部基于发布订阅者实现的功能。其实,无论是 redux 还是 vuex / pinia,一个完整的状态管理库都包含发布订阅的能力。所以就可以采用其他的方式来代替 actions 这种方式,甚至是自己写一个发布订阅者的类似功能代替也行。
在使用 registerMicroApps
注册路由时,也可以手动指定 props 来给子应用传递数据。那么也就可以利用这一特性,实现我们自己的发布订阅者功能,实现应用之间的相互通信。
这里继续以 pinia 为例。
第一步:主应用传递 props
ts
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 里面,就没有这一步的操作。
ts
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
ts
// 获取主应用的数据(包含共享数据,和修改共享数据的方法),放到子应用的 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 方式都可以自己尝试。针对开发,怎么简单怎么来。