如果你使用了 qiankun 的 globalState 的话,那这个警告你应该不陌生:
很多人都会疑惑,啊?不用这个我怎么给子应用传递数据啊?为什么要移除啊?本篇文章就针对这个问题详细介绍一下。
移除原因
我们可以在 qiankun3 的路线图里明确看到 globalState 会被移除(有可能会提供一套新的状态共享方案,但是只是可能)。
qiankun 3.0 Roadmap · Discussion #1378 · umijs/qiankun (github.com)
能看到下面其实也有人表示不解,为什么要移除这个设计。其实 globalState 本质上就是一个发布订阅模式的实现,本身和“前端微服务”这个主题的关系不大,只是在 qiankun 里又重新造了遍轮子。还有一个问题是 globalState 本身的设计比较简单,如果要存放的东西比较多、比较杂,涉及到不同子应用访问不同数据的话,都放在全局也不好管理。
所以回归本质想一下,qiankun 应该提供一个渠道,能够把父应用的实例本身传递给子应用。这一点 qiankun 的 props 已经实现了,那至于状态共享之类的上层设计就可以完全交给用户自己去做,或者直接用现有成熟的前端状态管理方案。
所以 globalState 的地位就很尴尬了,简单场景下用不上,复杂场景下不好用,所以才会有移除的计划。
有人可能会说,为什么简单场景用不上,我现在就在用啊,会这样想是因为你的思维被局限住了,下面的内容会告诉你如何根据情况轻松解决之前的问题。
直接使用 props
首先也是最基础的:有必要使用 globalState 么?如果你的数据在整个子应用的生命周期里都不会变,那就没必要用 globalState。
比如有的人需要传递一个参数,控制是否隐藏子应用的标题栏,那这个字段传进来就不会变。因为几乎不会有需求说我父应用里有个按钮,点一下子应用的标题栏就出来。
// 父应用
loadMicroApp({
name: 'app1',
entry: '//localhost:1234',
container: '#sub-app',
props: { headerVisible: true },
});
// 子应用
function render(props) {
const { container, headerVisible } = props;
console.log('🚀 ~ 子应用获取标题是否显示', headerVisible)
// 挂载子应用...
}
if (!window.__POWERED_BY_QIANKUN__) {
render({});
}
传递引用数据
而如果你的数据会变,但是 子应用里是惰性使用数据的,例如父应用负责统一管理和更新 token,子应用获取 token 请求后台接口,子应用只会在发起接口请求时才会去获取最新的 token 值(惰性使用)。
这时候就可以通过在 props 里传递一个 getter 函数或者引用对象的形式,让子应用可以获取到最新的值:
// 父应用
let token = 'token-default'
export const getToken = () => token
export const setToken = newToken => token = newToken;
loadMicroApp({
name: 'app1',
entry: '//localhost:1234',
container: '#sub-app',
props: { getToken },
});
// 子应用
export const apiController = {
getToken: () => console.error('初始化尚未完成')
}
function render(props) {
const { container, getToken } = props;
apiController.getToken = getToken;
// 挂载子应用...
}
if (!window.__POWERED_BY_QIANKUN__) {
render({});
}
这么一来,子应用在任何位置调用 apiController.getToken()
,获取到的永远是父应用设置的最新 token。
或者另一种方式,props 传递一个引用类型的变量(对象、数组之类的),只要保证引用正确,那也能获取到最新的 token:
// 父应用
export const ApiConfig = {
token: 'token-default'
}
loadMicroApp({
name: 'app1',
entry: '//localhost:1234',
container: '#sub-app',
props: { ApiConfig },
});
// 子应用
export const apiController = {
config: { token: '' },
getToken: () => apiController.config.token
}
function render(props) {
const { container, ApiConfig } = props;
apiController.config = ApiConfig;
// 挂载子应用...
}
// 其他地方使用 token
apiController.getToken() // token-default...
又或者最简单的办法,父应用直接把 token 存到 localstorage 里,子应用初始化时也从 localstorage 里去读。qiankun 出于使用场景考虑,并没有对 localstorage 之类的本地存储进行隔离。
对接双方 store
而如果你需要在父子应用间共享一个 UI 状态,值变化时页面上就要立刻显示出来。以 redux 为例,假如父子组件的 store 都长下面这样(用 RTK 构建,需要共享的值是 store.global.headerVisible
,用于控制标题栏是否显示隐藏):
import { configureStore } from '@reduxjs/toolkit'
import { createSlice } from '@reduxjs/toolkit'
const globalSlice = createSlice({
name: 'global',
initialState: {
headerVisible: true,
},
reducers: {
setHeaderVisible: (state, action) => {
state.headerVisible = action.payload
}
},
})
export const { setHeaderVisible } = globalSlice.actions
export const store = configureStore({
reducer: { global: globalSlice.reducer },
})
这种情况下就可以通过 props 把父子应用的 store 连接起来:
// 父应用
import { store } from './store'
loadMicroApp({
name: 'app1',
entry: '//localhost:1234',
container: '#sub-app',
props: {
defaultState: store.getState(),
onStoreChange: listener => store.subscribe(() => listener(store.getState)),
},
});
上面把 redux 的 store.subscribe
做了简单的封装然后传递给子应用,这样子应用就可以通过这个 onStoreChange
方法“订阅”父应用的 store,然后在状态发生变化时更新到自己的 redux store:
// 子应用
import { store, setHeaderVisible } from './store'
function render(props) {
const { defaultState, onStoreChange } = props;
console.log('使用 defaultState 设置默认状态', defaultState)
onStoreChange && onStoreChange(getState => {
const value = getState();
console.log('子应用监听 value.global.headerVisible', value.global.headerVisible)
store.dispatch(setHeaderVisible(value.global.headerVisible))
});
}
然后子组件内部再通过 useSelector 消费状态即可,最终效果如下:
这种方式是比较推荐的,因为父组件可以通过对 store.subscribe 进行封装,让子组件只能访问某个子状态,例如 onTokenChange
,onUserStateChange
,管理起来更加方便一点。
不过如果你觉得这样不过瘾,还有个变态一点的做法,就是直接把父应用的 redux store 通过 props 传递进来,然后子应用把这个 store 塞给自己的 react-redux Provider。就相当于子应用直接操作和访问父应用的状态了,也是可以正常生效的。不过鉴于这种做法过于粗暴,大家听听图一乐就好了。
并且不只是 redux,你用 vuex 还是其他技术栈的状态管理方案都是可以的,只不过把 redux 的 subscribe 替换成 vuex 的 subscribe。毕竟完整成熟的状态方案基本都包含发布订阅的能力,从另一个角度来说,也完全可以算得上内置的 globalState 的上位替代品。
比如,我完全可以自己手写一个最小化的发布订阅模型,连接到 props 里,子应用里一样可以正常接收数据并在变化时触发回调:
// 父应用
// 包含是否显示标题栏的发布订阅实现
const createSimpleStore = (defaultValue = false) => {
const listeners = new Set();
let headerVisible = defaultValue;
return {
get: () => headerVisible,
set: (value) => {
headerVisible = value;
listeners.forEach((listener) => listener(value));
},
subscribe: (listener) => {
listeners.add(listener);
return () => listeners.delete(listener);
},
};
};
const simpleStore = createSimpleStore(true);
loadMicroApp({
name: 'app1',
entry: '//localhost:1234',
container: '#sub-app',
props: {
onSimpleStoreChange: simpleStore.subscribe,
},
});
// 子应用里还是正常订阅
import { store, setHeaderVisible } from './store'
function render(props) {
const { onSimpleStoreChange } = props;
onSimpleStoreChange && onSimpleStoreChange(value => {
console.log('子应用监听状态', value)
store.dispatch(setHeaderVisible(value))
});
}
转发任何引用
让我们回头再看一遍文章开头的那句话:
qiankun 应该提供一个渠道,能够把父应用的实例本身传递给子应用。
再结合刚才的例子,你是不是已经发现了:子应用从 props 中接收到的数据本身就指向了父应用中的同一对象,子应用从 props 里接受到的和父应用提供到 props 里的就是同一个东西!无论是对象、数组还是函数,只要你是引用类型就是这样。
理解了这一点之后,globalState 对你也就没什么用了,比如我甚至可以传递一个闭包函数,让父子一起使用:
// 父应用
const createCloseureFunc = () => {
let data = 0;
return () => {
data++;
console.log(data);
};
};
const closeureFunc = createCloseureFunc();
loadMicroApp({
name: 'app1',
entry: '//localhost:1234',
container: '#sub-app',
props: { closeureFunc },
});
// 子应用里通过 context 分发给子组件使用
export const CloseureFuncContext = React.createContext();
function render(props) {
const { container, closeureFunc } = props;
const root = ReactDOM.createRoot((container || document).querySelector('#root'));
root.render(
<CloseureFuncContext.Provider value={closeureFunc}>
<App />
</CloseureFuncContext.Provider>
);
}
效果如下:
总结
可以看到,props 其实是一个“窗口”,任何需要共享的东西都可以通过这个窗口来回传递,这个传递的过程是没有类似于 window.postMessage
的数据深拷贝的,子应用中拿到的就是父应用里的数据。所以说很多文章里提到 props 只能转发静态数据实际上是不对的,也造成了很多误导。
通过本文内容可以看到,使用 props 就已经可以支持正常的功能开发工作了。不过就算如此,也不要过分依赖 props 里的数据,不然加剧父子应用之间的耦合也就变成舍本逐末了。