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()}`);
}

几个关键事实:使用 cloneDeep 确保外部修改不会绕过通信机制;初始化时立即触发 emitGlobal 通知已注册的订阅者;时间戳 global-${+new Date()} 用于在 deps 中唯一标识主应用的回调。

6.1.3 发布与通知:emitGlobal

typescript
function emitGlobal(state: Record<string, any>, prevState: Record<string, any>) {
  Object.keys(deps).forEach((id: string) => {
    if (deps[id] instanceof Function) {
      deps[id](cloneDeep(state), cloneDeep(prevState));
    }
  });
}

一个不起眼却至关重要的细节:每次通知都传递深拷贝。如果传递引用,子应用的回调可以直接修改全局状态而不触发其他订阅者的通知——这是灾难性的。深拷贝确保了只有 setGlobalState 才是修改全局状态的合法通道,与 Redux 的"单一数据流"理念异曲同工。

🔥 深度洞察:深拷贝的性能代价与设计取舍

cloneDeep 的时间复杂度是 O(n)。如果全局状态包含大型数组,每次 emitGlobal 会产生 N 次深拷贝(N 是订阅者数量)。对 3 个子应用这完全不是问题,但 20 个子应用加上 10000 条记录的列表,性能就可能成为瓶颈。乾坤选择"安全优先"而非"性能优先",这是正确的默认值——但在极端场景下,你需要意识到这个代价。

6.1.4 MicroAppStateActions:操作句柄的生成

typescript
export function getMicroAppStateActions(
  id: string,
  isMaster?: boolean
): MicroAppStateActions {
  return {
    onGlobalStateChange(callback: OnGlobalStateChangeCallback, fireImmediately?: boolean) {
      if (!(callback instanceof Function)) {
        console.error('[qiankun] callback must be function!');
        return;
      }
      if (deps[id]) {
        console.warn(`[qiankun] bindId: ${id} bindCallback already exists, will be overwrite.`);
      }
      deps[id] = callback;
      if (fireImmediately) {
        const cloneState = cloneDeep(globalState);
        callback(cloneState, cloneState);
      }
    },

    setGlobalState(state: Record<string, any> = {}) {
      if (state === globalState) {
        console.warn('[qiankun] state has not changed!');
        return false;
      }

      const changeKeys: string[] = [];
      const prevGlobalState = cloneDeep(globalState);

      globalState = cloneDeep(
        Object.keys(state).reduce((_globalState, changeKey) => {
          if (isMaster || _globalState.hasOwnProperty(changeKey)) {
            changeKeys.push(changeKey);
            return Object.assign(_globalState, { [changeKey]: state[changeKey] });
          }
          console.warn(
            `[qiankun] globalState does not have the key: ${changeKey}, ` +
            `it's not allowed to add new key after initGlobalState.`
          );
          return _globalState;
        }, globalState)
      );

      if (changeKeys.length === 0) {
        console.warn('[qiankun] state has not changed!');
        return false;
      }
      emitGlobal(globalState, prevGlobalState);
      return true;
    },

    offGlobalStateChange() {
      delete deps[id];
      return true;
    },
  };
}

setGlobalState 中那个 reduce 循环是核心——它实现了权限分级

  • 主应用isMaster === true):可以添加新的顶层 key,拥有完全的状态控制权。
  • 子应用isMaster === false):只能修改已存在的顶层 key,不能添加新 key。

为什么要做这个限制?想象一个没有限制的世界:

typescript
// 子应用 A 添加了一个 key
setGlobalState({ featureFlagA: true });

// 子应用 B 也添加了一个 key
setGlobalState({ featureFlagB: true });

// 子应用 C 又加了一个...
setGlobalState({ tempData: { /* 一大堆临时数据 */ } });

// 三个月后,globalState 变成了一个巨大的垃圾场
// 没人知道哪些 key 还在被使用,哪些已经是僵尸数据

通过限制子应用只能修改已有 key,乾坤确保了主应用是全局状态结构的唯一定义者。这是一种"合同制"——主应用定义了全局状态的"Schema",子应用只能在这个 Schema 内操作。

6.1.5 完整的数据流

下图展示了一次完整的全局状态变更的时序过程,从子应用发起修改到所有订阅者收到通知:

基于 VitePress 构建