微前端全局状态共享:跨子应用通信的四种方案对比
微前端全局状态共享跨子应用通信的四种方案对比微前端解决了巨石应用的部署耦合问题但引入了一个新难题多个独立构建、独立部署的子应用之间如何共享状态。用户的登录信息、主题偏好、权限配置这些全局数据是每个子应用都复制一份还是通过某种机制共享四种主流方案各有适用场景没有银弹。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 Mapstring, SetEventHandler(); 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: PartialGlobalState): 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 里做派生计算。关键是约定清晰——哪些状态走哪条通路在项目文档里写明白避免三个子应用各用各的通信方式最后谁也找不着谁。