qiankun 微前端框架详解

一个前端项目长到几千个文件、几个团队同时改、一个页面卡住全公司跟着加班时,你会想:能不能拆开?qiankun 就是干这个的。

这篇文章只回答三个问题:它是什么、为什么要用、怎么用。中间会把它最核心的隔离机制用一个最小代码重建一遍——看懂那段代码,才算真的懂它。


一、它要解决的真实问题

先不看任何术语,看三个真实发生的场景。

场景 1:全局变量打架。

团队 A 的同事在 window 上挂了一个 window.globalData,团队 B 的同事也挂了同名变量。上线后 A 的页面偶尔读到 B 的数据。谁也说不清是谁先写的,只能靠约定"命名加前缀"来祈祷不冲突。约定靠得住吗?靠不住。

场景 2:技术栈被绑死。

公司早期用 Vue2 起家,几万行代码都写在 Vue2 上。现在想上 React 写新模块,或者想升级 Vue3——整个系统重写?成本大到没人敢提。

场景 3:发布耦合。

改了一行文案,得把整个巨型应用重新构建、重新发布、全量回归。一个团队的小失误,让所有团队一起回滚。

这三个痛点的根子是一个:所有代码挤在同一个运行时里,谁也离不开谁。

微前端(Micro Frontends)要做的,就是把这个大应用拆成多个能独立开发、独立部署、独立运行的小应用,再拼成一个整体。

打个比方:单体应用是一个大办公室,所有人共用一块白板、一个休息室,谁都能改谁的桌子;微前端是一个购物中心,每个店独立装修、独立进货、独立营业,但都开在同一个商场里,共享客流和门牌号。

qiankun 就是这个"购物中心的管理方"。


二、qiankun 是什么

一句话:qiankun 是蚂蚁集团开源的微前端框架,基于 single-spa 之上,把"能用"补成了"好用"。

名字是乾坤——乾是天,坤是地,乾坤是宇宙。寓意"能装下任何东西":不管你子应用是 Vue、React、Angular 还是静态 HTML,都能装进来。

它做的事可以概括成一张表:

能力 解决的问题 对应你关心的点
HTML 入口 子应用不用改成 JS 地址,直接给 URL 接入成本低
JS 沙箱 全局变量、定时器、事件监听互不污染 场景 1
样式隔离 子应用 CSS 不互相污染 视觉错乱
应用间通信 主应用和子应用传数据 场景 3
预加载 切换子应用更快 性能
技术栈无关 Vue/React/原生都能挂 场景 2

关键点:它不是 iframe,但能做到 iframe 那样的隔离效果,同时没有 iframe 的毛病。下面说为什么。


三、为什么不用 iframe,又为什么能像 iframe 一样隔离

iframe 天生隔离:里面的 JS、CSS、DOM 都关在独立环境里,天然不打架。那为什么不直接用 iframe?

因为 iframe 隔离得太彻底了,代价是:

  • URL 不同步:iframe 内部跳转,浏览器地址栏不变,刷新就丢状态,前进后退全乱。
  • DOM 不共享:全局 loading、全局弹窗、顶部导航想跨子应用出现,非常难。
  • 资源重复加载:每个 iframe 都要重新加载字体、公共库。
  • 通信麻烦:只能 postMessage,链路绕。

qiankun 的思路反过来:让子应用跑在同一个 document 里(共享 DOM、共享 URL、天然协同),再用两个机制把"该隔离的东西"挡起来——用代理沙箱隔离 JS 的全局变量,用 Shadow DOM 或选择器前缀隔离 CSS。既保留了 iframe 的隔离,又保留了同一个页面的协作。


四、核心机制拆解

这部分是重点。看懂它,你就知道 qiankun 不是"魔法",只是几个明确的机制。

4.1 HTML 入口

single-spa 要求你注册子应用时给它一个 JS 文件的地址。这意味着子应用要专门为微前端打一个包、暴露生命周期,麻烦。

qiankun 只要一个 URL:

entry: '//localhost:7100'

它内部用 import-html-entry 去请求这个地址的 HTML,把里面的 <script>、<link> 全部抓下来,自己管理加载和执行。对你的意义是:子应用照常部署原来的 index.html,不用为微前端单独改部署方式。

4.2 JS 沙箱:用"替身"隔离全局变量

这是最核心、也最值得看懂的部分。先问一个问题:

子应用 A 执行 window.a = 1,子应用 B 执行 window.a = 2。怎么才能让 A 读到 1、B 读到 2,而真实的 window.a 根本没被动过?

答案是:不让子应用碰到真实的 window,给每个子应用一个"替身"。 子应用以为自己写的是 window,其实写的是它自己的账本。

这件事分两步:第一步,造替身(Proxy 负责拦截读写);第二步,让子应用代码里的 window 指向这个替身。先看第一步——造替身的最小重建版,逻辑和 qiankun 的 ProxySandbox 一致:

// 简化版沙箱:每个子应用拿到一个 proxy 当 window 用
function createSandbox() {
  // 这个账本记录"这个子应用对 window 做过哪些修改"
  const updatedPropsMap = new Map();
  // fakeWindow 是替身,一个空对象
  const fakeWindow = Object.create(null);

  const proxy = new Proxy(fakeWindow, {
    // 读:先翻自己的账本,账本里没有再去真实 window 找
    get(target, prop) {
      if (updatedPropsMap.has(prop)) {
        return updatedPropsMap.get(prop);
      }
      const value = window[prop];
      // window 上的函数要绑回真实 window,否则 this 指向会错
      if (typeof value === 'function') {
        return value.bind(window);
      }
      return value;
    },
    // 写:记在自己账本上,绝不碰真实 window
    set(target, prop, value) {
      updatedPropsMap.set(prop, value);
      return true;
    },
    has(target, prop) {
      return updatedPropsMap.has(prop) || prop in window;
    },
  });

  return proxy;
}

// 两个子应用各拿一个替身
const appA = createSandbox();
const appB = createSandbox();

appA.myGlobal = '我是 A';
appB.myGlobal = '我是 B';

console.log(appA.myGlobal);   // '我是 A'
console.log(appB.myGlobal);   // '我是 B'
console.log(window.myGlobal); // undefined —— 真实 window 没被污染

第二步:让 window 指向替身。

替身造好了,但子应用代码里写的还是 window.xxx,这个 window 标识符默认指向真 window。怎么把它换成替身?靠 with 语句 + 函数参数绑定:

// 子应用代码原样执行,但把 window / self / globalThis 三个标识符都重定向到替身
function runInSandbox(code, proxy) {
  const fn = new Function('window', 'self', 'globalThis', `with(window){ ${code} }`);
  fn(proxy, proxy, proxy);
}

runInSandbox(
  `window.myGlobal = '我是 A'; console.log(window.myGlobal);`,
  appA
);

with(window) 把替身临时插到作用域链最前面,函数参数 (proxy, proxy, proxy) 把 window、self、globalThis 三个变量名都指向替身。于是子应用代码一行不改,执行时它的 window 已经被换成了替身。

为什么是三个,而不是只传一个 window?因为浏览器里 window、self、globalThis 是同一个全局对象的三个名字,子应用或第三方库用哪个都行——老代码写 window.xxx,Worker 风格写 self.xxx,现代打包产物写 globalThis.xxx。只重定向 window,那子应用写 self.xxx 或 globalThis.xxx 就会绕过替身、直接污染真 window。三个都得堵,等于把全局对象的所有入口都换成替身。

到这里,两个步骤就闭环了:Proxy 不负责"替代 window",它只是那个被替代成的替身;真正做"替代"的是 with + 参数绑定,改变的是作用域链。 三个词的分工——with 插作用域、参数绑定改指向、Proxy 拦读写。

看懂这段代码,你就理解了 qiankun 隔离的本质:用 Proxy 拦截读写,写操作进各自账本,读操作优先读账本,兜底读真实 window。 真实环境里它还多处理了定时器、事件监听器、delete 等边角,构造时也会带一个 name(用于报错时提示是哪个子应用出的问题)——但骨架就是这个。

为什么非得用 Proxy,不直接让子应用各用各的命名空间?

因为命名空间要"改代码",Proxy 不用。子应用是已经上线的独立应用,代码里写满了 window.globalData = xxx,还有第三方库也直接读写 window,这些改不动。命名空间方案要求把它们全改成 window.__appA.globalData = xxx,把改造负担转嫁给了每一个子应用作者;而 Proxy 把 window 换成替身,子应用照旧写 window.xxx,写操作被拦截器自动收进它自己的账本——代码一行不用改,隔离就发生了。

更进一步看,那个 updatedPropsMap 其实就是命名空间,差别只在"谁来放变量":命名空间靠子应用代码自己放,Proxy 靠 set 拦截器自动放。所以 Proxy 沙箱本质是"自动化的命名空间"——让隔离这个目的,对代码透明。

另外命名空间还有两件事管不住:一是子应用读 window.location、window.fetch 这类共享 API 时仍要直连真实 window,写的时候一疏忽就污染,命名空间靠自觉、Proxy 靠拦截;二是 addEventListener、setInterval 这类副作用不在变量名层面,卸载后命名空间清不掉,qiankun 的沙箱会一并回收。ES6 之前没有 Proxy 这种"拦截任意属性读写"的能力,所以老浏览器里 qiankun 只能退化成下面的快照沙箱——又慢又糙,这也反过来印证了 Proxy 的价值。

浏览器不支持 Proxy 时,qiankun 降级到 SnapshotSandbox(快照沙箱):进入子应用前把 window 快照一份,离开时恢复。效果类似,性能差一点。

4.3 样式隔离:两种方案

CSS 没有"作用域"这种天然隔离,qiankun 给了两种方案,各有取舍:

方案 配置 做法 代价
Shadow DOM strictStyleIsolation: true 把子应用容器变成 Shadow DOM,样式彻底封死 对部分老组件库不友好
选择器前缀 experimentalStyleIsolation: true 自动给子应用 CSS 选择器加前缀 div[data-qiankun-app1] 只是"缓解",不是"封死"
start({
  sandbox: {
    strictStyleIsolation: true,        // 或 experimentalStyleIsolation: true
  },
});

4.4 生命周期

每个子应用只需导出三个钩子,qiankun 在合适的时机调用:

export async function bootstrap() {}  // 首次加载时,只执行一次
export async function mount(props) {} // 每次进入该子应用
export async function unmount() {}    // 每次离开该子应用

顺序是固定的:加载 → bootstrap → mount;切走 → unmount。这就是为什么子应用能"反复进出而不残留"。


五、怎么用:一个最小可运行例子

5.1 主应用(基座)

import { registerMicroApps, start } from 'qiankun';

registerMicroApps([
  {
    name: 'react-app',        // 唯一标识
    entry: '//localhost:7100',// 子应用部署地址
    container: '#subapp-container', // 挂载点
    activeRule: '/react',     // 路由匹配规则:路径以 /react 开头就激活
  },
  {
    name: 'vue-app',
    entry: '//localhost:7101',
    container: '#subapp-container',
    activeRule: '/vue',
  },
]);

start(); // 开始监听路由,按 activeRule 自动加载/卸载

activeRule 就是那套"门牌号"逻辑:浏览器 URL 变到 /vue 就挂 Vue 子应用,变到 /react 就卸掉 Vue、挂 React。

你可能会问:上面讲的 ProxySandbox 在哪?代码里根本没出现。 因为它不归你管——start() 之后,qiankun 每次挂载一个子应用,都会自动为它 new 一个 ProxySandbox,把子应用的 JS 放进这个沙箱里执行。你写的代码是"在沙箱外",子应用的代码被 qiankun 偷偷搬进了"沙箱里",两边都无感知。所以原理里它是主角,用法里你看不见它。

只有一件事你能控制它:开关。默认开启,start({ sandbox: false }) 可以关掉;但通常没人会关,关了就等于放弃隔离,回到全局变量打架的老路。

5.2 子应用改造(Vue3 为例)

核心是:让子应用既能在 qiankun 里跑,也能独立跑。

import { createApp } from 'vue';
import App from './App.vue';

let app;

function render(props = {}) {
  const { container } = props;
  // 在 qiankun 里要挂到它给的 container 里,独立跑就挂到 #app
  const mountNode = container ? container.querySelector('#app') : '#app';
  app = createApp(App);
  app.mount(mountNode);
}

// 独立运行(不是被 qiankun 加载)时,直接挂载
if (!window.__POWERED_BY_QIANKUN__) {
  render();
}

export async function bootstrap() {
  console.log('vue app 初始化');
}

export async function mount(props) {
  render(props);
}

export async function unmount() {
  app.unmount();
  app = null;
}

React 子应用思路完全一样:mount 里 root.render(),unmount 里 root.unmount()。

5.3 打包配置

qiankun 加载子应用时要拿到它的生命周期函数,所以子应用要打成 UMD 格式并允许跨域。

Webpack:

module.exports = {
  output: {
    library: 'vue-app',
    libraryTarget: 'umd',          // 关键:暴露成 UMD
  },
  devServer: {
    headers: { 'Access-Control-Allow-Origin': '*' }, // 允许跨域
  },
};

Vite 子应用:qiankun 2.x 主要面向 webpack,Vite 生态用社区插件 vite-plugin-qiankun 做适配(qiankun 3.0 已原生支持 ESM)。

5.4 应用间通信

主应用用 initGlobalState 建一个全局状态,子应用通过 props 收发:

主应用:

import { initGlobalState, registerMicroApps, start } from 'qiankun';

// 初始化全局状态,拿到 actions
const actions = initGlobalState({ user: 'kuitos' });

// 监听变化
actions.onGlobalStateChange((state, prev) => {
  console.log('主应用监听到变化', state, prev);
});

// 修改状态,会广播给所有子应用
actions.setGlobalState({ user: 'new-user' });

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

start();

子应用在 mount 里通过 props 拿到通信能力:

export async function mount(props) {
  render(props);

  // props 里带有 onGlobalStateChange 和 setGlobalState
  props.onGlobalStateChange((state, prev) => {
    console.log('子应用监听到变化', state, prev);
  });
}

六、什么时候该用、什么时候别用

不要因为它有名就用。判断标准只有一个:你的痛点是不是"多个团队、多个技术栈、需要独立发布"。

该用:

  • 多个团队并行开发一个大型应用,彼此要解耦
  • 技术栈异构(老 Vue2 想逐步迁到 React/Vue3)
  • 老系统需要增量改造,不想一次性重写
  • 需要子应用独立发布、独立回滚

别用:

  • 项目很小,一个团队几天能改完——引入它反而增加复杂度
  • 子应用之间交互非常紧密、共享大量状态——拆开是硬伤
  • 对性能有极致要求——JS 沙箱的 Proxy 拦截有轻微开销

现状与对比(截至 2026 年):qiankun 2.x 是最成熟、生态最大的方案,生产环境广泛使用;3.0 在开发中(原生 ESM、模块化拆分)。如果你在选型,可以一起看这几个:

方案 出身 隔离方式 一句话判断
single-spa 社区 无沙箱,要自己搞 qiankun 的底座,太底层
qiankun 蚂蚁 Proxy 沙箱 + Shadow DOM/前缀 最完整、生态最大,接入要改造子应用
Module Federation webpack 运行时共享模块 共享依赖、性能好,但绑定 webpack
micro-app 京东 类 Web Component 接入成本低,子应用几乎不用改
wujie 无界 腾讯 iframe + Web Component 隔离最彻底,兼容性最好

七、小结

回到最开头的三个问题:

  • 是什么:蚂蚁开源的微前端框架,在 single-spa 上补齐了 HTML 入口、JS 沙箱、样式隔离、通信、预加载。
  • 为什么用:当多个团队、多个技术栈挤在一个大前端里,互相污染、绑死、耦合,需要拆成能独立开发部署的子应用。
  • 怎么用:主应用 registerMicroApps + start 注册,子应用导出 bootstrap/mount/unmount 三个钩子,打包成 UMD,通信用 initGlobalState。

它最值钱的一行代码,是那段 Proxy 沙箱——"写进各自账本,读时先翻账本"。看懂这一句,比背一百个 API 都有用。

至于你的项目该不该上:先问自己一句——是真的有多团队、多技术栈的痛点,还是只是觉得"大家都用所以我也用"?想清楚这个,再决定。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容