Skip to content

single-spa 与 qiankun 深度对比:微前端架构核心原理解析

微前端作为前端架构的重要发展方向,single-spa 和 qiankun 是其中最具代表性的两个解决方案。理解它们的原理和区别,是掌握微前端架构的关键所在。

核心关系概述

qiankun 是一个基于 single-spa 的、更完善的微前端解决方案。 可以理解为 qiankun 在 single-spa 的核心机制之上,封装了更多企业级的、开箱即用的功能,解决了 single-spa 本身没有解决的一些通用痛点。

single-spa:微前端的基础内核

核心原理

single-spa 的核心原理可以概括为"路由劫持"和"生命周期管理"

路由劫持机制

single-spa 通过以下方式实现路由劫持:

  1. 事件监听重写

    • 重写浏览器的 pushStatereplaceState 事件
    • 监听 hashchangepopstate 事件
  2. 活动函数判断

    • 当 URL 发生变化时,single-spa 会监听到变化
    • 根据预先注册的活动函数 (activity function) 判断哪个子应用应该被激活
    • 活动函数通常判断 window.location.pathname 是否匹配某个前缀
  3. 动态加载与挂载

    • 匹配成功后,动态加载(load)并挂载(mount)对应的子应用
javascript
// 示例:注册子应用
registerApplication({
  name: 'app1',
  app: () => System.import('app1'),
  activeWhen: (location) => location.pathname.startsWith('/app1'),
  customProps: {}
});

生命周期管理

single-spa 为每个子应用定义了标准化的生命周期钩子:

生命周期函数

  • bootstrap:引导阶段,只执行一次,用于应用初始化
  • mount:挂载阶段,每次激活时执行
  • unmount:卸载阶段,每次停用时执行
javascript
// 子应用导出的生命周期函数
export async function bootstrap(props) {
  // 应用初始化逻辑
}

export async function mount(props) {
  // DOM 渲染逻辑
}

export async function unmount(props) {
  // 清理逻辑
}

生命周期管理特点

  • 主应用通过调用这些钩子控制子应用的整个生命周期
  • 确保在任何时候只有一个(或一组)子应用处于活动状态
  • 避免多个应用同时运行造成的冲突

single-spa 的局限性

single-spa 只提供了最核心的应用加载和切换机制,但对于以下复杂问题并没有提供官方解决方案:

  • JS 沙箱:全局变量污染问题
  • CSS 样式隔离:样式冲突问题
  • 应用间通信:数据共享问题
  • 资源预加载:性能优化问题

qiankun:企业级微前端解决方案

qiankun 正是为了解决 single-spa 留下的这些痛点而生,它提供了三大核心能力:JS 沙箱、CSS 隔离和资源加载器。

1. JS 沙箱 (Sandbox)

目的

解决多个子应用同时运行时,全局 window 对象被污染的问题(比如 window.onpopstate 被覆盖、全局变量冲突等)。

实现方案

快照沙箱 (SnapshotSandbox)
javascript
class SnapshotSandbox {
  constructor() {
    this.proxy = window;
    this.modifyPropsMap = {};
    this.active();
  }

  active() {
    // 记录当前快照
    this.windowSnapshot = {};
    for (const prop in window) {
      if (window.hasOwnProperty(prop)) {
        this.windowSnapshot[prop] = window[prop];
      }
    }
  }

  inactive() {
    for (const prop in window) {
      if (window.hasOwnProperty(prop)) {
        // 记录变更
        if (window[prop] !== this.windowSnapshot[prop]) {
          this.modifyPropsMap[prop] = window[prop];
          // 还原window
          window[prop] = this.windowSnapshot[prop];
        }
      }
    }
  }
}

特点

  • 在应用挂载前为 window 创建快照
  • 应用卸载时通过比对快照恢复 window 状态
  • 适用于不支持 Proxy 的旧版浏览器
  • 性能较差,只支持单实例
代理沙箱 (ProxySandbox)
javascript
class ProxySandbox {
  constructor() {
    const rawWindow = window;
    const fakeWindow = {};
    
    const proxy = new Proxy(fakeWindow, {
      get(target, prop) {
        return target[prop] || rawWindow[prop];
      },
      set(target, prop, value) {
        target[prop] = value;
        return true;
      }
    });
    
    this.proxy = proxy;
  }
}

特点

  • 利用 ES6 的 Proxy 特性
  • 为每个子应用创建独立的 fakeWindow
  • 子应用在代理对象上操作,实现完全隔离
  • 性能更好,支持多实例
  • 是目前的主流方案

2. CSS 隔离

目的

防止不同子应用之间的样式互相污染。

实现方案

动态样式表 (Dynamic Stylesheet)
javascript
// qiankun 默认策略示例
function scopedCSS(styleNode, prefix) {
  const css = styleNode.textContent;
  const scopedCSS = css.replace(/([^{}]+){/g, (match, selector) => {
    return `${prefix} ${selector.trim()}{`;
  });
  styleNode.textContent = scopedCSS;
}

工作原理

  • 运行时动态为子应用的 stylelink 标签添加特殊属性
  • 改写所有 CSS 规则,为每个选择器加上属性前缀
  • 类似于 Vue 的 scopedPostCSS 的能力
  • 限制样式的生效范围
Shadow DOM
javascript
// Shadow DOM 隔离示例
const shadowRoot = container.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = template;

特点

  • 将整个子应用渲染到独立的 Shadow DOM 节点中
  • 实现近乎完美的 CSS 和 DOM 隔离
  • 存在兼容性问题和穿透样式复杂的问题

3. 资源加载与解析 (HTML Entry)

设计理念

single-spa 通常要求子应用打包成一个 JS 文件(JS Entry),而 qiankun 创新地采用了 HTML 作为入口

实现原理

javascript
// HTML Entry 加载流程
async function loadApp(entry) {
  // 1. 获取 HTML 内容
  const html = await fetch(entry).then(res => res.text());
  
  // 2. 解析 HTML
  const { template, scripts, styles } = parseHTML(html);
  
  // 3. 加载资源
  const jsCode = await loadScripts(scripts);
  const cssCode = await loadStyles(styles);
  
  // 4. 创建沙箱环境
  const sandbox = createSandbox();
  
  // 5. 执行 JS 代码
  const exports = sandbox.execScript(jsCode);
  
  return { template, exports };
}

解析过程

  1. HTML 解析:通过 fetch 获取子应用的 index.html 文件
  2. 资源提取:提取 HTML 中的 scriptlinkstyle 标签
  3. 资源加载:分别加载这些 JS 和 CSS 资源
  4. 代码执行:将 JS 代码包裹在沙箱环境中执行
  5. 内容挂载:将解析后的内容挂载到主应用指定容器

优势

  • 子应用的开发和部署体验与独立应用几乎完全一样
  • 无需做过多的打包配置改造
  • 接入成本极低
  • 更贴近传统 Web 开发模式

应用通信机制

qiankun 内置通信方案

javascript
// 主应用
import { initGlobalState } from 'qiankun';

// 初始化全局状态
const actions = initGlobalState({
  user: 'admin',
  token: 'xxxxx'
});

// 监听全局状态变更
actions.onGlobalStateChange((value, prev) => {
  console.log('[onGlobalStateChange - master]', value, prev);
});

// 设置全局状态
actions.setGlobalState({
  user: 'newUser'
});
javascript
// 子应用
export async function mount(props) {
  // 监听全局状态变更
  props.onGlobalStateChange?.((value, prev) => {
    console.log('[onGlobalStateChange - sub]', value, prev);
  });
  
  // 设置全局状态
  props.setGlobalState?.({
    token: 'newToken'
  });
}

性能优化特性

预加载机制

javascript
// qiankun 预加载配置
import { prefetchApps } from 'qiankun';

// 预加载子应用
prefetchApps([
  { name: 'app1', entry: '//localhost:8081' },
  { name: 'app2', entry: '//localhost:8082' }
]);

预加载优势

  • 在空闲时间预先加载子应用资源
  • 减少应用切换时的加载时间
  • 提升用户体验

核心区别对比

特性single-spaqiankun
定位微前端基础库、内核基于 single-spa 的上层框架
接入方式JS Entry (需要子应用打包成单个JS)HTML Entry (直接使用HTML做入口,更简单)
JS隔离不提供,需自行实现内置JS沙箱 (ProxySandbox, SnapshotSandbox)
CSS隔离不提供,需自行实现内置CSS隔离方案 (Dynamic Stylesheet, Shadow DOM)
应用通信不提供,需自行实现 (如 props, EventBus)提供内置的 initGlobalState API
资源预加载不提供提供 prefetch API,提升性能
开发体验需要较多配置和自建基础设施开箱即用,配置简单
易用性较低,更像一个"轮子",需要大量自建非常高,"生产就绪"的解决方案
灵活性极高,可以完全自定义相对较低,但满足大部分场景需求
学习成本较高,需要深入理解微前端原理较低,按文档配置即可使用
适用场景需要极度定制化的项目大部分企业级微前端项目

技术选型建议

选择 single-spa 的场景

适合以下情况:

  • 项目相对简单,不需要复杂的隔离机制
  • 团队技术能力强,有足够能力自建基础设施
  • 追求最大灵活性,需要深度定制微前端架构
  • 特殊需求,qiankun 无法满足的定制化场景
javascript
// single-spa 适合的简单场景
import { registerApplication, start } from 'single-spa';

registerApplication({
  name: 'simple-app',
  app: () => import('./simple-app.js'),
  activeWhen: '/simple'
});

start();

选择 qiankun 的场景

适合以下情况:

  • 复杂的企业级项目,需要完整的隔离和通信机制
  • 追求开发效率,希望快速搭建微前端架构
  • 团队技术栈多样,需要统一的接入标准
  • 稳定性要求高,需要经过大规模验证的解决方案
javascript
// qiankun 的典型使用场景
import { registerMicroApps, start } from 'qiankun';

registerMicroApps([
  {
    name: 'vue-app',
    entry: '//localhost:8081',
    container: '#vue-container',
    activeRule: '/vue'
  },
  {
    name: 'react-app',
    entry: '//localhost:8082',
    container: '#react-container',
    activeRule: '/react'
  }
]);

start({
  sandbox: { experimentalStyleIsolation: true },
  prefetch: 'all'
});

架构演进路径

从 single-spa 到 qiankun

很多团队的微前端架构演进路径:

  1. 起步阶段:使用 single-spa 快速验证微前端可行性
  2. 发展阶段:遇到沙箱、隔离等问题,开始自建解决方案
  3. 成熟阶段:发现自建成本高,迁移到 qiankun 或其他成熟方案

技术债务考量

  • single-spa:前期投入小,后期维护成本高
  • qiankun:前期投入适中,后期维护成本低

最佳实践总结

通用最佳实践

  1. 应用划分原则

    • 按业务域划分,而非技术栈
    • 保持应用间的低耦合
    • 避免过度拆分造成的复杂性
  2. 状态管理策略

    • 避免全局状态过度使用
    • 优先考虑 URL 状态管理
    • 建立清晰的数据流向
  3. 性能优化建议

    • 合理使用预加载机制
    • 避免重复加载公共资源
    • 监控和优化包体积

qiankun 特定建议

  1. 沙箱配置

    javascript
    start({
      sandbox: {
        strictStyleIsolation: true,  // 严格样式隔离
        experimentalStyleIsolation: true  // 实验性样式隔离
      }
    });
  2. 生命周期优化

    javascript
    export async function mount(props) {
      // 缓存 DOM 节点,避免重复创建
      const container = props.container;
      if (!container.querySelector('#app')) {
        // 创建应用
      }
    }

总结

对于绝大多数复杂的、追求开发效率和稳定性的企业级项目,qiankun 无疑是更好的选择。 它帮助开发团队解决了微前端架构中最棘手、最繁琐的几个问题,让团队可以更专注于业务逻辑的开发。

而 single-spa 更适合作为学习微前端原理的入门工具,或者在需要极度定制化的特殊场景中使用。

无论选择哪种方案,深入理解微前端的核心原理——应用隔离、状态管理、资源加载——都是构建稳健微前端架构的基础。

基于 VitePress 构建