微前端全局状态共享:跨子应用通信的四种方案对比
微前端全局状态共享:跨子应用通信的四种方案对比
微前端解决了巨石应用的部署耦合问题,但引入了一个新难题:多个独立构建、独立部署的子应用之间如何共享状态。用户的登录信息、主题偏好、权限配置这些全局数据,是每个子应用都复制一份,还是通过某种机制共享?四种主流方案各有适用场景,没有银弹。
flowchart TB A[跨子应用状态共享方案] --> B[方案一: Props 传递] A --> C[方案二: 全局事件总线] A --> D[方案三: 共享 Store] A --> E[方案四: 本地存储 + 轮询] B --> F{选型决策} C --> F D --> F E --> F F -->|简单场景| B F -->|事件驱动| C F -->|复杂状态| D F -->|最低侵入| E一、Props 传递:最朴素也最可靠
主应用加载子应用时,通过 Props 传递全局状态。qiankun 和 Micro-app 都支持这种模式。
// 主应用 - 注册子应用时注入全局 Props import { registerMicroApps, start } from 'qiankun'; interface GlobalState { user: UserInfo | null; theme: 'light' | 'dark'; locale: 'zh-CN' | 'en-US'; permissions: string[]; } const globalState: GlobalState = { user: null, theme: 'light', locale: 'zh-CN', permissions: [], }; registerMicroApps([ { name: 'app-dashboard', entry: '//localhost:3001', container: '#subapp-container', activeRule: '/dashboard', props: { globalState, onGlobalStateChange: (callback: (state: GlobalState) => void) => { // 注册状态变更回调 }, }, }, ]); start();子应用侧接收:
// 子应用入口 export async function mount(props: SubAppProps) { const { globalState } = props; // 初始化时获取全局状态 render({ user: globalState.user, theme: globalState.theme }); // 注册状态变更监听 props.onGlobalStateChange?.((newState) => { console.log('全局状态变更:', newState); updateApp(newState); }); }优点:类型安全、数据流向清晰、无额外依赖。缺点:状态更新需要主应用主动通知,子应用不能反向修改,适合"读多写少"的场景。
实际场景:一个后台管理系统中,侧边栏子应用需要根据权限动态显示菜单,权限数据由主应用维护。使用 Props 传递方案,主应用在权限变更时调用子应用暴露的update方法传入新数据。但问题出现了——用户从"订单管理"页面切换到"用户管理"页面时,侧边栏的权限数据没有更新,因为只有 mount 时获取了一次。解决方式是在子应用的 mount 中注册onGlobalStateChange回调,而不仅仅在 mount 时读取一次值:
export async function mount(props: SubAppProps) { render({ user: props.globalState.user }); props.onGlobalStateChange?.((newState) => { if (newState.user?.permissions !== currentPermissions) { updateMenu(newState.user.permissions); } }); }二、全局事件总线:松耦合的双向通信
基于 CustomEvent 或自建 EventBus 实现跨应用的事件驱动通信:
// shared/event-bus.ts type EventHandler = (payload: unknown) => void; class MicroEventBus { private handlers = new Map<string, Set<EventHandler>>(); on(event: string, handler: EventHandler): () => void { if (!this.handlers.has(event)) { this.handlers.set(event, new Set()); } this.handlers.get(event)!.add(handler); return () => this.handlers.get(event)?.delete(handler); } emit(event: string, payload: unknown): void { this.handlers.get(event)?.forEach((handler) => { try { handler(payload); } catch (error) { console.error(`事件处理异常 [${event}]:`, error); } }); } once(event: string, handler: EventHandler): void { const wrapper: EventHandler = (payload) => { handler(payload); this.off(event, wrapper); }; this.on(event, wrapper); } off(event: string, handler: EventHandler): void { this.handlers.get(event)?.delete(handler); } clear(event?: string): void { if (event) { this.handlers.delete(event); } else { this.handlers.clear(); } } } export const eventBus = new MicroEventBus();子应用 A 发布状态变更:
// 子应用 A - 用户修改了主题 import { eventBus } from '@shared/event-bus'; function changeTheme(theme: 'light' | 'dark') { localStorage.setItem('app-theme', theme); eventBus.emit('theme-changed', { theme }); }子应用 B 订阅:
// 子应用 B - 响应主题变更 import { eventBus } from '@shared/event-bus'; export function mount(): void { const unsubscribe = eventBus.on('theme-changed', (payload) => { const { theme } = payload as { theme: string }; applyTheme(theme); }); // 卸载时清理订阅 return unsubscribe; }优点:灵活、无中心化瓶颈、子应用可独立演进。缺点:事件名需要约定管理,容易出现"幽灵事件"(谁发的、谁在监听不清楚),调试困难。
踩坑记录:一个项目中某个子应用在卸载时忘记调用unsubscribe(),导致该应用被重新加载后,同一个事件被旧的回调和新回调同时处理,UI 上出现了"闪烁刷新"。排查过程花了 2 小时——因为控制台没有报错,只是数据更新了两遍。建议在事件总线中增加事件日志:记录每次 emit 的调用栈和所有 handler 的执行时间,开发环境下输出到控制台,方便追踪事件来源。
emit(event: string, payload: unknown): void { if (process.env.NODE_ENV === 'development') { console.debug(`[EventBus] emit "${event}"`, payload, new Error().stack); } this.handlers.get(event)?.forEach((handler) => { try { handler(payload); } catch (error) { console.error(`事件处理异常 [${event}]:`, error); } }); }三、共享 Store:当全局状态变得复杂
当全局状态超过 5 个字段、需要计算派生状态时,轻量级的共享 Store 比事件总线更合适。方案是让主应用创建一个 Store 实例,通过 Props 或全局变量暴露:
// shared/store.ts interface GlobalStore { state: GlobalState; subscribe: (listener: (state: GlobalState) => void) => () => void; dispatch: (action: GlobalAction) => void; } function createGlobalStore(initialState: GlobalState): GlobalStore { let state = { ...initialState }; const listeners = new Set<(state: GlobalState) => void>(); return { get state() { return state; }, subscribe(listener: (state: GlobalState) => void) { listeners.add(listener); return () => listeners.delete(listener); }, dispatch(action: GlobalAction) { state = reducer(state, action); listeners.forEach((fn) => { try { fn(state); } catch (error) { console.error('Store listener 异常:', error); } }); }, }; } // 挂载到 window,所有子应用共享同一实例 (window as any).__GLOBAL_STORE__ = createGlobalStore({ user: null, theme: 'light', locale: 'zh-CN', permissions: [], });优点:状态可追溯、支持派生计算、类型安全。缺点:引入了额外的数据流概念,子应用需要理解 Store 的使用方式,学习成本略高。
需要注意的是,共享 Store 方案要求所有子应用都能访问同一个 JavaScript 全局对象。在使用 qiankun 等微前端框架时,如果子应用运行在 JS Sandbox 中,可能会导致 Store 实例隔离。解决方案是在主应用中将 Store 挂载到window对象,并配置沙箱允许访问该全局变量,或者通过使用setGlobalState和getGlobalState的 API 包装函数来避免直接访问全局对象。
四、localStorage + 轮询:最低侵入的方案
当子应用技术栈不统一(一个 React、一个 Vue、一个 jQuery 老项目),前三种方案都可能遇到适配问题。最兼容的做法是把全局状态存在 localStorage,子应用轮询检测变化:
// shared/storage-sync.ts const STORAGE_KEY = '__micro_global_state__'; export function syncGlobalState(state: Partial<GlobalState>): void { const current = JSON.parse( localStorage.getItem(STORAGE_KEY) || '{}', ); const updated = { ...current, ...state, _version: Date.now() }; localStorage.setItem(STORAGE_KEY, JSON.stringify(updated)); } export function watchGlobalState( callback: (state: GlobalState & { _version: number }) => void, interval = 500, ): () => void { let lastVersion = 0; const timer = setInterval(() => { try { const raw = localStorage.getItem(STORAGE_KEY); if (!raw) return; const state = JSON.parse(raw) as GlobalState & { _version: number }; if (state._version > lastVersion) { lastVersion = state._version; callback(state); } } catch (error) { console.error('状态读取异常:', error); } }, interval); return () => clearInterval(timer); }优点:零框架依赖、跨技术栈、兼容性最好。缺点:序列化限制(不能存函数和复杂对象)、有轮询延迟、不适合高频更新场景。
五、总结
跨子应用状态共享没有最好的方案,只有最合适的组合。Props 传递做初始数据注入,事件总线做反向通知,共享 Store 管理复杂状态,storage + 轮询做异构兼容。实际项目中往往是多种方案并存:用户信息用 Props 注入,主题切换用事件广播,权限数据放 Store 里做派生计算。关键是约定清晰——哪些状态走哪条通路,在项目文档里写明白,避免三个子应用各用各的通信方式最后谁也找不着谁。
