Skip to content

第6章 乾坤的应用间通信

"微前端架构中,沙箱隔离的目的是让应用互不干扰——但业务永远需要它们彼此对话。如何在隔离与协作之间找到精确的平衡点,是每个微前端方案必须回答的核心问题。"

本章要点

  • 深入 initGlobalState 源码,理解乾坤基于发布订阅模式实现全局状态通信的完整机制
  • 掌握 Props 传递的实现原理,理解主应用与子应用之间直接通信的数据流向
  • 剖析 loadMicroApp 手动加载模式下通信的差异与适用场景
  • 对比 CustomEvent、BroadcastChannel、共享 Store 等替代方案,在性能与复杂度之间做出理性权衡

微前端的沙箱机制——无论是 SnapshotSandbox 还是 ProxySandbox——都在做一件事:隔离。它们用精心设计的代理拦截和快照恢复,确保子应用之间的全局变量不会互相污染。前几章我们已经深入理解了这些隔离机制的实现原理。

但隔离只是硬币的一面。

回到现实业务场景:用户在主应用的导航栏点击头像,弹出个人信息面板——这个面板属于"用户中心"子应用。用户修改了头像,主应用的导航栏需要立即更新。与此同时,"消息中心"子应用也需要知道头像变了,因为它在消息列表里展示了用户头像。

三个独立部署的应用,需要在同一个浏览器窗口中实时同步一份数据。沙箱把它们隔开了,但业务又要求它们协作。这就像你精心设计了一栋公寓——每户都有独立的门锁和隔音墙——然后住户们说:"我们需要一个公共公告板。"

乾坤对此的回答是三种通信机制:全局状态(initGlobalState)、Props 传递、以及 loadMicroApp 的手动加载模式。它们各自适用于不同场景,背后的实现原理也截然不同。这一章,我们将逐行阅读这些机制的源码,理解它们的设计意图,然后跳出乾坤本身,对比更广泛的微前端通信方案——因为只有理解了全部选项,你才能为自己的项目做出正确的架构决策。

6.1 initGlobalState:基于发布订阅的全局状态

6.1.1 从使用方式开始

先看 initGlobalState 的典型使用方式,带着"它要解决什么问题"的思维去阅读实现代码。

typescript
// 主应用 - main-app/src/micro.ts
import { initGlobalState, MicroAppStateActions } from 'qiankun';

const actions: MicroAppStateActions = initGlobalState({
  user: { name: '杨艺韬', avatar: '/default.png' },
  theme: 'light',
  locale: 'zh-CN',
});

actions.onGlobalStateChange((state, prevState) => {
  console.log('主应用感知到状态变化:', state);
  updateNavbar(state.user);
});

actions.setGlobalState({
  user: { name: '杨艺韬', avatar: '/new-avatar.png' },
});
typescript
// 子应用 - sub-app/src/main.ts
export function mount(props) {
  const { onGlobalStateChange, setGlobalState } = props;

  onGlobalStateChange((state, prevState) => {
    console.log('子应用感知到状态变化:', state);
    store.commit('updateUser', state.user);
  });

  setGlobalState({ theme: 'dark' });
}

API 很简洁——初始化一个全局状态对象,主应用和子应用都可以监听变化、修改状态。但简洁的 API 背后,隐藏着几个值得深入思考的设计决策:

  1. 为什么全局状态只能由主应用初始化? 子应用不能调用 initGlobalState。如果任何子应用都能初始化全局状态,状态的"起点"就变得不可预测——你永远不知道哪个子应用先加载、先初始化。
  2. 子应用通过 props 获取通信能力,而不是直接导入。 这意味着通信能力是由主应用"授予"的,子应用没有办法在主应用不知情的情况下参与全局通信。
  3. setGlobalState 是合并(merge)而非替换(replace)。 子应用修改 theme 不会丢失 user 数据。这降低了子应用之间的协调成本——你不需要知道全局状态的完整结构就能安全地修改自己关心的部分。

带着这些问题,我们进入源码。

下图展示了乾坤全局状态通信的整体架构,包括主应用和子应用之间的数据流向:

6.1.2 initGlobalState 的核心实现

乾坤的全局状态管理核心逻辑不到 100 行,但信息密度极高。

typescript
// qiankun/src/globalState.ts

import { cloneDeep } from 'lodash';

let globalState: Record<string, any> = {};
const deps: Record<string, OnGlobalStateChangeCallback> = {};

type OnGlobalStateChangeCallback = (
  state: Record<string, any>,
  prevState: Record<string, any>
) => void;

第一个值得注意的设计:globalStatedeps 都是模块级变量。它们不在 window 上,也不在任何类实例上。这意味着不受子应用沙箱的影响——ProxySandbox 代理的是 window,而不是乾坤内部的模块变量。放在模块作用域,是最简单也最安全的位置。

typescript
// qiankun/src/globalState.ts

export function initGlobalState(state: Record<string, any> = {}) {
  if (state === globalState) {
    console.warn('[qiankun] state has not changed!');
    return getMicroAppStateActions(`global-${+new Date()}`);
  }

  const prevGlobalState = cloneDeep(globalState);
  globalState = cloneDeep(state);
  emitGlobal(globalState, prevGlobalState);
  return getMicroAppStateActions(`global-${+new Date()}`);
}

基于 VitePress 构建