Skip to content

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

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

核心关系概述 ​

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

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

核心原理 ​

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

路由劫持机制 ​

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

  1. 事件监听重写

    • 重写浏览器的 pushState、replaceState 事件
    • 监听 hashchange 和 popstate 事件
  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;
}

工作原理:

  • 运行时动态为子应用的 style 和 link 标签添加特殊属性
  • 改写所有 CSS 规则,为每个选择器加上属性前缀
  • 类似于 Vue 的 scoped 或 PostCSS 的能力
  • 限制样式的生效范围
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 中的 script 和 link、style 标签
  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 构建