Skip to content

第3章 乾坤架构总览

"好的框架不是让你什么都能做——而是让你在做对的事情时毫不费力,做错的事情时寸步难行。"

本章要点

  • 理解乾坤的三大设计哲学:HTML Entry、沙箱隔离、标准化生命周期
  • 掌握乾坤的核心依赖关系:qiankun → single-spa → import-html-entry 的三层架构
  • 通过源码走读完整理解子应用从注册到卸载的全生命周期
  • 深入分析 registerMicroApps、loadApp、start 三大核心函数的实现
  • 客观评估乾坤在 2026 年微前端生态中的真实地位与适用场景

2019 年 7 月,蚂蚁集团前端团队在 GitHub 上发布了一个名为 qiankun(乾坤)的开源项目。彼时 single-spa 已经是微前端领域的事实标准,但它有一个让无数开发者头疼的问题:太底层了。single-spa 只负责子应用的注册和生命周期调度,至于子应用怎么加载、JS 怎么隔离、CSS 怎么隔离——全部由你自己解决。

这就像给你一个操作系统内核,告诉你"进程调度做好了,至于内存管理、文件系统、网络协议栈——自己写吧。"

乾坤的回答是:我来封装这一切。

它在 single-spa 的生命周期调度之上,增加了 HTML Entry(通过 import-html-entry 实现子应用加载)、JS 沙箱(Proxy/Snapshot 双模式)、CSS 隔离(Shadow DOM/Scoped CSS)三大核心能力,把微前端从"理论上可行"变成了"开箱即用"。

截至 2026 年初,乾坤在 GitHub 上累计超过 16k star,npm 周下载量仍然稳定在 20k+。它不是最新的,也不是最"酷"的——但它是被最多生产环境验证过的微前端方案。在我们深入任何源码细节之前,先建立对它的全局架构认知,就像在徒步穿越一片森林之前,先在山顶看一眼全貌。

3.1 乾坤的设计哲学

乾坤的设计哲学可以浓缩为三个关键词:HTML Entry沙箱生命周期。这三者分别解决了微前端的三个核心问题:怎么加载子应用、怎么隔离子应用、怎么管理子应用。

3.1.1 HTML Entry:像使用 iframe 一样简单

在 single-spa 中,注册一个子应用需要你手动提供一个 JS Entry——一个 JavaScript 文件的 URL,single-spa 通过动态创建 <script> 标签来加载它。这意味着你需要:

  1. 确保子应用的构建产物是一个 UMD 模块
  2. 手动管理子应用的 CSS 加载
  3. 处理子应用内部的静态资源路径问题
  4. 解决子应用多个 JS chunk 的加载顺序
typescript
// single-spa 的 JS Entry 模式——繁琐且易错
import { registerApplication } from 'single-spa';

registerApplication({
  name: 'app-order',
  // 你需要自己保证这个 JS 文件能正确导出生命周期
  app: () => System.import('http://localhost:7100/app.js'),
  activeWhen: '/order',
});

// 子应用必须是 UMD 格式
// CSS?自己加载。
// 图片路径?自己处理。
// 多个 chunk?自己编排。

乾坤的 HTML Entry 彻底改变了这个局面。它的思路极其朴素——既然子应用本身就是一个完整的 Web 应用,有自己的 HTML 入口页面,为什么不直接获取这个 HTML,从中解析出 JS 和 CSS 资源?

typescript
// 乾坤的 HTML Entry 模式——简洁直观
import { registerMicroApps } from 'qiankun';

registerMicroApps([
  {
    name: 'app-order',
    // 直接给子应用的 URL,就像在浏览器地址栏输入一样
    entry: '//localhost:7100',
    container: '#micro-app-container',
    activeRule: '/order',
  },
]);

这个设计的精妙之处在于:子应用完全不需要为接入微前端做任何构建配置上的妥协。它的 HTML 文件里引用了什么 JS、什么 CSS、什么字体文件——乾坤全部自动解析、自动加载。子应用可以继续作为独立应用运行,也可以作为微前端子应用被加载。

HTML Entry 的底层实现依赖 import-html-entry 这个库,它的核心逻辑我们将在 3.2 节详细分析。这里先建立一个直觉:

typescript
// import-html-entry 的核心能力(简化)
interface HtmlEntryResult {
  // 子应用的 HTML 模板(移除了 script 标签)
  template: string;
  // 一个函数:执行所有提取出的 JS 脚本,返回子应用导出的生命周期
  execScripts: () => Promise<{
    bootstrap: () => Promise<void>;
    mount: (props: any) => Promise<void>;
    unmount: (props: any) => Promise<void>;
  }>;
  // 用于获取外部样式表的内容
  getExternalStyleSheets: () => Promise<string[]>;
  // 用于获取外部脚本的内容
  getExternalScripts: () => Promise<string[]>;
}

💡 深度洞察:HTML Entry 的设计思想本质上是"逆向 iframe"。iframe 直接加载整个页面但无法与主应用深度通信;HTML Entry 解析页面但在主应用的上下文中执行代码——它取了 iframe 的便利性(给一个 URL 就够了),又避免了 iframe 的隔离过度问题(无法共享登录态、路由、DOM 通信)。这个设计决策奠定了乾坤"简单接入"的核心竞争力。

3.1.2 沙箱:隔离是微前端的生命线

如果说 HTML Entry 解决了"怎么加载",沙箱则解决了一个更根本的问题:多个子应用同时运行时,如何防止它们互相污染?

JavaScript 的全局变量是所有微前端方案的噩梦。一个子应用在 window 上挂了一个 __APP_CONFIG__,另一个子应用也挂了同名属性——后者悄无声息地覆盖了前者。更隐蔽的是定时器:子应用 A 设了一个 setInterval,卸载时忘了清理,这个定时器就像幽灵一样在后台持续运行,污染后续加载的子应用。

乾坤为此设计了三种沙箱机制:

typescript
// 乾坤的三种沙箱模式
type SandboxType =
  | 'LegacyProxy'    // 单例 Proxy 沙箱(兼容模式)
  | 'ProxySandbox'   // 多例 Proxy 沙箱(推荐)
  | 'SnapshotSandbox'; // 快照沙箱(降级方案,兼容 IE)

// Proxy 沙箱的核心思想
class ProxySandbox {
  private updatedValueSet = new Set<PropertyKey>();
  private fakeWindow: Record<PropertyKey, any>;
  private running = false;

  proxy: WindowProxy;

  constructor() {
    const rawWindow = window;
    // 创建一个假的 window 对象
    this.fakeWindow = Object.create(null);

    this.proxy = new Proxy(this.fakeWindow, {
      get: (target, prop) => {
        // 优先从 fakeWindow 获取(子应用设置的变量)
        if (target.hasOwnProperty(prop)) {
          return target[prop];
        }
        // 否则从真实 window 获取(原生 API)
        const value = rawWindow[prop as any];
        // 如果是函数,绑定到真实 window(如 setTimeout)
        return typeof value === 'function' ? value.bind(rawWindow) : value;
      },

      set: (target, prop, value) => {
        if (this.running) {
          target[prop] = value;
          this.updatedValueSet.add(prop);
        }
        return true;
      },
    });
  }

  active() {
    this.running = true;
  }

  inactive() {
    this.running = false;
  }
}

这段代码展示了 Proxy 沙箱的核心思想:每个子应用看到的 window 其实是一个代理对象。 子应用往 window 上写属性,实际写入的是 fakeWindow;读属性时先查 fakeWindow,找不到再查真实 window。这样多个子应用可以同时运行,各自拥有独立的"全局变量空间",互不干扰。

快照沙箱(SnapshotSandbox)则是面向不支持 Proxy 的旧浏览器的降级方案:

typescript
// 快照沙箱的简化实现
class SnapshotSandbox {
  private windowSnapshot: Map<string, any> = new Map();
  private modifyPropsMap: Map<string, any> = new Map();

  active() {
    // 激活时,拍下 window 的快照
    for (const prop in window) {
      this.windowSnapshot.set(prop, (window as any)[prop]);
    }
    // 恢复上次子应用运行时的修改
    this.modifyPropsMap.forEach((value, prop) => {
      (window as any)[prop] = value;
    });
  }

  inactive() {
    // 失活时,记录子应用的修改,然后恢复 window
    for (const prop in window) {
      if ((window as any)[prop] !== this.windowSnapshot.get(prop)) {
        // 记录修改
        this.modifyPropsMap.set(prop, (window as any)[prop]);
        // 恢复原值
        (window as any)[prop] = this.windowSnapshot.get(prop);
      }
    }
  }
}

💡 深度洞察:快照沙箱有一个致命限制——它是单例的。因为它直接操作真实的 window 对象,同一时刻只能有一个子应用处于激活状态。而 Proxy 沙箱通过虚拟 window 实现了多例隔离,可以同时运行多个子应用。这就是为什么乾坤文档中建议在需要多个子应用同时展示的场景下使用 Proxy 沙箱。理解这个区别,能帮你避开生产环境中最常见的沙箱配置陷阱。

3.1.3 生命周期:子应用的生老病死

微前端中的子应用不是"加载一次就完事"的静态资源——它有完整的生命周期。乾坤(通过 single-spa)定义了三个核心生命周期钩子:

typescript
// 子应用必须导出的三个生命周期函数
export async function bootstrap(): Promise<void> {
  // 初始化:只在子应用第一次加载时调用一次
  // 适合做一次性的初始化工作,如加载 polyfill
  console.log('[order-app] bootstrapped');
}

export async function mount(props: MicroAppProps): Promise<void> {
  // 挂载:每次子应用被激活时调用
  // 在这里创建根组件、渲染 DOM
  const { container } = props;
  ReactDOM.createRoot(
    container.querySelector('#root')!
  ).render(<App />);
}

export async function unmount(props: MicroAppProps): Promise<void> {
  // 卸载:每次子应用被切走时调用
  // 在这里销毁根组件、清理副作用
  const { container } = props;
  ReactDOM.createRoot(
    container.querySelector('#root')!
  ).unmount();
}

这三个钩子看起来简单,但它们的调用时机和语义是整个微前端协调的基础。乾坤在 single-spa 的基础上增强了这些生命周期:

typescript
// 乾坤增强的生命周期钩子(框架侧,非子应用侧)
interface FrameworkLifeCycles {
  beforeLoad?: (app: RegistrableApp) => Promise<void>;   // 加载前
  beforeMount?: (app: RegistrableApp) => Promise<void>;  // 挂载前
  afterMount?: (app: RegistrableApp) => Promise<void>;   // 挂载后
  beforeUnmount?: (app: RegistrableApp) => Promise<void>; // 卸载前
  afterUnmount?: (app: RegistrableApp) => Promise<void>;  // 卸载后
}

// 使用示例
registerMicroApps(apps, {
  beforeLoad: async (app) => {
    console.log(`[主应用] ${app.name} 即将加载...`);
    // 可以在这里做权限校验、加载提示等
  },
  afterMount: async (app) => {
    console.log(`[主应用] ${app.name} 已挂载`);
    // 可以在这里做埋点、性能监控等
  },
});

下图展示了乾坤三大设计哲学如何分别解决微前端核心问题:

这三个设计哲学——HTML Entry、沙箱、生命周期——构成了乾坤的三根支柱。接下来我们俯瞰乾坤的依赖架构,理解这三根支柱是如何在代码层面组织起来的。

3.2 核心依赖关系:qiankun → single-spa → import-html-entry

3.2.1 三层架构

乾坤的代码架构可以用一张依赖图概括:

┌─────────────────────────────────────────────────────┐
│                    qiankun (乾坤)                     │
│                                                       │
│  ┌─────────────────┐  ┌────────────┐  ┌───────────┐ │
│  │  JS/CSS 沙箱     │  │  HTML Entry │  │  全局状态  │ │
│  │  (Proxy/Snapshot)│  │   适配层    │  │  通信管理  │ │
│  └────────┬────────┘  └─────┬──────┘  └───────────┘ │
│           │                 │                         │
├───────────┼─────────────────┼─────────────────────────┤
│           │       single-spa│                         │
│           │   ┌─────────────┴────────────┐            │
│           │   │  应用注册 / 路由匹配      │            │
│           │   │  生命周期调度              │            │
│           │   │  状态机管理                │            │
│           │   └──────────────────────────┘            │
├───────────┼───────────────────────────────────────────┤
│           │       import-html-entry                    │
│           │   ┌──────────────────────────┐            │
│           │   │  HTML 获取与解析          │            │
│           │   │  Script/Style 资源提取    │            │
│           │   │  JS 执行(with 沙箱)     │            │
│           │   └──────────────────────────┘            │
└───────────┴───────────────────────────────────────────┘

每一层的职责非常清晰:

  • import-html-entry(底层):负责获取子应用的 HTML,从中提取 JS 和 CSS 资源,并提供在沙箱环境中执行 JS 的能力
  • single-spa(中层):负责子应用的注册、路由监听、生命周期状态机管理——它决定何时加载、挂载、卸载子应用
  • qiankun(上层):在前两者之上,添加了沙箱隔离、预加载、全局状态管理、错误处理等生产级能力

3.2.2 single-spa:生命周期的调度中枢

single-spa 是整个微前端生命周期调度的核心。它维护了一个应用状态机,定义了子应用从注册到卸载的完整状态流转:

typescript
// single-spa 的应用状态枚举
enum AppStatus {
  NOT_LOADED = 'NOT_LOADED',             // 已注册,未加载
  LOADING_SOURCE_CODE = 'LOADING_SOURCE_CODE', // 正在加载代码
  NOT_BOOTSTRAPPED = 'NOT_BOOTSTRAPPED', // 已加载,未初始化
  BOOTSTRAPPING = 'BOOTSTRAPPING',        // 正在初始化
  NOT_MOUNTED = 'NOT_MOUNTED',           // 已初始化,未挂载
  MOUNTING = 'MOUNTING',                  // 正在挂载
  MOUNTED = 'MOUNTED',                    // 已挂载(可见)
  UNMOUNTING = 'UNMOUNTING',              // 正在卸载
  UNLOADING = 'UNLOADING',               // 正在卸载资源
  LOAD_ERROR = 'LOAD_ERROR',             // 加载失败
  SKIP_BECAUSE_BROKEN = 'SKIP_BECAUSE_BROKEN', // 致命错误,跳过
}

// 状态流转:
// NOT_LOADED → LOADING_SOURCE_CODE → NOT_BOOTSTRAPPED
//   → BOOTSTRAPPING → NOT_MOUNTED
//     → MOUNTING → MOUNTED
//       → UNMOUNTING → NOT_MOUNTED(可重新挂载)
//     → UNLOADING → NOT_LOADED(完全卸载,需重新加载)

下图是 single-spa 子应用状态机的完整流转:

single-spa 的路由监听机制是子应用自动切换的基础:

typescript
// single-spa 路由监听的核心实现(简化)
function setupRouteListening() {
  // 拦截 pushState 和 replaceState
  const originalPushState = window.history.pushState;
  const originalReplaceState = window.history.replaceState;

  window.history.pushState = function (...args) {
    const result = originalPushState.apply(this, args);
    // 路由变化后,重新评估哪些子应用应该被激活
    reroute();
    return result;
  };

  window.history.replaceState = function (...args) {
    const result = originalReplaceState.apply(this, args);
    reroute();
    return result;
  };

  // 监听 popstate 事件(浏览器前进/后退)
  window.addEventListener('popstate', () => {
    reroute();
  });

  // 监听 hashchange 事件(hash 路由模式)
  window.addEventListener('hashchange', () => {
    reroute();
  });
}

// reroute:微前端调度的心脏
function reroute() {
  const {
    appsToLoad,    // 需要加载的应用
    appsToMount,   // 需要挂载的应用
    appsToUnmount, // 需要卸载的应用
  } = getAppChanges(); // 根据当前 URL 和 activeWhen 计算

  // 先卸载不再需要的应用
  const unmountPromises = appsToUnmount.map(toUnmountPromise);
  // 加载需要的应用
  const loadPromises = appsToLoad.map(toLoadPromise);

  return Promise.all(unmountPromises).then(() => {
    // 卸载完成后,挂载新的应用
    const mountPromises = appsToMount.map(toMountPromise);
    return Promise.all([...loadPromises, ...mountPromises]);
  });
}

💡 深度洞察:single-spa 的 reroute 函数是整个微前端调度的"心跳"。每次路由变化都会触发一次 reroute,它负责计算哪些应用需要卸载、哪些需要加载、哪些需要挂载。这个设计有一个精妙之处:卸载一定在挂载之前完成(通过 Promise 链保证)。这避免了新旧子应用同时存在时的资源竞争问题。但这也意味着应用切换不可能是"无缝"的——新应用挂载之前,旧应用一定已经从 DOM 中消失。

3.2.3 import-html-entry:HTML 的拆解与执行

import-html-entry 是乾坤"HTML Entry"能力的底层实现。它做了三件事:

基于 VitePress 构建