QIANKUN globalState tools will be removed 警告解决

1,800 阅读7分钟

如果你使用了 qiankun 的 globalState 的话,那这个警告你应该不陌生:

image.png

很多人都会疑惑,啊?不用这个我怎么给子应用传递数据啊?为什么要移除啊?本篇文章就针对这个问题详细介绍一下。

移除原因

我们可以在 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。

动画.gif

或者另一种方式,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 消费状态即可,最终效果如下:

动画.gif

这种方式是比较推荐的,因为父组件可以通过对 store.subscribe 进行封装,让子组件只能访问某个子状态,例如 onTokenChangeonUserStateChange,管理起来更加方便一点。

不过如果你觉得这样不过瘾,还有个变态一点的做法,就是直接把父应用的 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>
    );
}

效果如下:

动画.gif

总结

可以看到,props 其实是一个“窗口”,任何需要共享的东西都可以通过这个窗口来回传递,这个传递的过程是没有类似于 window.postMessage 的数据深拷贝的,子应用中拿到的就是父应用里的数据。所以说很多文章里提到 props 只能转发静态数据实际上是不对的,也造成了很多误导。

通过本文内容可以看到,使用 props 就已经可以支持正常的功能开发工作了。不过就算如此,也不要过分依赖 props 里的数据,不然加剧父子应用之间的耦合也就变成舍本逐末了。