1. 为什么今天还在聊乾坤?它不是“过气技术”,而是被低估的工程基建能力
微前端这个词,这两年被讲得太多,也太泛。很多人一提微前端,脑子里立刻蹦出“拆应用”“独立部署”“技术栈无关”这些漂亮话,但真到落地时,要么卡在路由冲突上动弹不得,要么子应用加载后样式全局污染、状态互相覆盖,最后发现:理想很丰满,上线后天天救火。我带过三个中大型项目做微前端改造,其中两个用的是 qiankun,一个试过其他方案又切回 qiankun——不是因为它最炫,而是它把“能跑通、能维护、能交接”这三件事,真正做成了可量化的工程标准。qiankun 不是万能胶,但它是一把被反复打磨过的螺丝刀:不花哨,但拧得紧、不滑丝、换人也能接着拧。它解决的从来不是“要不要微前端”的战略问题,而是“怎么让十个团队写的代码,在同一个页面里互不打架”的战术问题。关键词乾坤、qiankun、微前端,这三个词连在一起,代表的不是某种时髦架构,而是一套经过真实业务高压验证的隔离机制——沙箱、生命周期、通信、资源加载控制。如果你正面临主应用臃肿、新业务不敢加、老系统不敢动、前端团队协作成本越来越高这些具体痛点,那 qiankun 就不是“可选项”,而是“止损线”。它适合两类人:一类是技术负责人,需要快速建立跨团队协作边界;另一类是资深前端工程师,想搞懂现代 Web 应用底层运行时的可控性到底怎么实现。别被“微前端”三个字吓住,qiankun 的核心逻辑其实就一句话:把每个子应用当成一个受控的 iframe,但不用 iframe。这句话背后,藏着对浏览器运行时、JavaScript 执行上下文、CSS 作用域、资源加载链路的全链路干预能力。接下来,我们就从真实踩坑现场出发,一层层剥开它到底怎么做到的。
2. 乾坤不是框架,而是一套“运行时治理协议”
2.1 它不接管你的代码,只接管你的执行环境
很多初学者一上来就问:“qiankun 怎么集成 Vue?React 怎么配?”这个问题本身就错了方向。qiankun 本身不关心你用什么框架写子应用,它只关心一件事:当这个子应用的 JS 被执行时,它的全局变量、DOM 操作、样式注入、资源请求,是否被限制在它自己的“领地”里。换句话说,qiankun 不是像 Vue Router 那样提供一套新 API 让你改写路由逻辑,而是像给每个子应用发了一张“临时工牌”——你只能访问自己工位上的电脑(window 对象)、只能在自己工位区域贴便签(CSS)、只能用自己的打印机(fetch 请求拦截)。这种能力,叫“沙箱隔离”,是 qiankun 的立身之本。它分两种模式:快照沙箱(SnapshotSandbox)和代理沙箱(ProxySandbox)。快照沙箱适用于非单页应用(比如 jQuery 时代的老系统),原理简单粗暴:在子应用 mount 前,先拍一张 window 全局对象的快照;unmount 时,把所有新增或修改的属性,按快照还原回去。实测下来,它在 IE11 上依然稳如老狗,但缺点也很明显——无法处理动态添加的全局监听器(比如 window.addEventListener('resize', ...)),因为快照拍不到函数引用。而代理沙箱是主流选择,它用 ES6 Proxy 包裹 window 对象,所有读写操作都走代理层。比如子应用执行window.foo = 'bar',代理层会把它存到子应用专属的 map 里,而不是真写进全局;当子应用读window.foo时,代理层优先从自己的 map 返回,没找到才查真实 window。这就实现了真正的“变量隔离”。但注意:Proxy 在 Safari 10 以下不支持,所以如果你的用户群还有大量旧版 iOS 设备,就得 fallback 到快照沙箱。这不是配置开关那么简单,而是要提前做 UA 检测 + 动态加载策略——我见过有团队直接硬编码sandbox: true,结果在某银行内部 iPad 上白屏,排查三天才发现是 Safari 版本兼容问题。
2.2 生命周期不是概念,而是强制约定的“入场券”
qiankun 把子应用的启动、挂载、卸载、重载,全部标准化为四个函数:bootstrap、mount、unmount、update。这不是让你“建议实现”,而是必须导出,否则整个子应用根本不会被加载。为什么这么强硬?因为这是唯一能保证主应用和子应用“步调一致”的方式。举个典型反例:某电商后台系统,子应用用了 Vue CLI 默认的main.js启动方式,里面直接 new Vue({ el: '#app' })。结果 qiankun 加载时,它自己还没准备好 DOM 容器(#sub-app),Vue 就报错“找不到挂载点”,整个流程卡死。后来我们逼着团队改写入口:mount函数里才真正初始化 Vue 实例,并把 el 指向 qiankun 提供的容器节点;unmount里主动调用 vm.$destroy() 并清空 DOM。这个过程看似多此一举,实则解决了两个致命问题:一是避免子应用在错误时机操作 DOM(比如主应用还没渲染完,子应用就急着找 #app);二是确保资源可回收(Vue 实例、事件监听器、定时器全被显式销毁)。更关键的是,update函数的存在,让子应用支持热更新成为可能——主应用不用刷新页面,就能触发子应用重新 mount。我们在灰度发布时就靠它:先加载新版本子应用 JS,调用update切换,再对比监控指标,没问题再全量。这套生命周期,本质上是在浏览器原生执行模型之上,人为构建了一层“应用级调度器”。它不改变 JavaScript 运行本质,但通过约定,把不可控的脚本执行,变成了可编排、可中断、可回滚的操作序列。
2.3 路由不是抢夺战,而是协商制
微前端最大的撕逼现场,永远是路由。主应用用 history.pushState('/dashboard'),子应用也 pushState('/user/list'),浏览器地址栏变成/dashboard/user/list,但谁来响应?qiankun 的解法很务实:主应用负责路由分发,子应用只管自己那一段路径。主应用配置一个activeRule,比如/app1/,表示所有以/app1/开头的 URL,都交给 app1 处理。子应用内部的路由(Vue Router 或 React Router)完全不用改,它看到的location.pathname是/user/list,而不是/app1/user/list。qiankun 在底层做了两件事:一是劫持history.pushState和replaceState,自动剥离前缀再调用原生方法;二是监听popstate事件,当浏览器前进/后退时,解析当前 URL,匹配 activeRule,决定哪个子应用该 mount/unmount。这里有个极易忽略的细节:子应用的 base 配置必须和 activeRule 严格对齐。比如主应用配置activeRule: '/app1/',子应用 Vue Router 就必须设base: '/app1/';如果子应用设成base: '/user/',那它永远收不到/app1/user/list的路由变化。我们曾因此导致子应用路由白屏,debug 时发现 console 里一堆NavigationDuplicated错误,根源就是 base 和 activeRule 不一致。qiankun 不会帮你校验这个,它默认你已理解路径前缀的语义——这恰恰说明,它不是黑盒,而是一套需要你深度参与的协议。
3. 从零搭一个可上线的乾坤主应用:避开 90% 的新手坑
3.1 初始化主应用:npm create qiankun ?别信
官方文档推荐npm create qiankun@latest快速生成模板,但实际项目中,我建议手动初始化。原因很简单:模板为了通用性,引入了大量非必要依赖(比如 @qiankunjs/cli、webpack-plugin-qiankun),而真实业务中,你大概率要用已有的 webpack/vite 配置。我们以 Vite 为例,这是目前最主流的选择。第一步,创建主应用:npm create vite@latest main-app -- --template react。第二步,安装核心依赖:npm install qiankun。注意,不要装 @qiankunjs/cli,那个是早期脚手架,现在已被弃用。第三步,修改main.jsx(React)或main.ts(Vue),这是最关键的入口改造:
import { registerMicroApps, start } from 'qiankun'; // 定义子应用列表 const apps = [ { name: 'app1', entry: '//localhost:7100', // 子应用开发服务器地址 container: '#subapp-1', // 主应用中预留的 DOM 容器 activeRule: '/app1', // 激活规则,注意:这里没有尾部斜杠! }, { name: 'app2', entry: '//localhost:7101', container: '#subapp-2', activeRule: '/app2', } ]; // 注册子应用 registerMicroApps(apps); // 启动 qiankun start({ // 关键配置:sandbox 默认 true,但生产环境建议显式声明 sandbox: { strictStyleIsolation: true, // 强制样式隔离,每个子应用样式仅作用于自身 experimentalStyleIsolation: true, // 实验性样式隔离,更彻底(Vite 环境推荐) }, // 路由基础路径,必须和主应用路由 base 一致 prefetch: true, // 预加载子应用资源,提升首屏速度 }); // 注意:这里不能调用 ReactDOM.render() 或 createApp().mount() // qiankun 会接管 DOM 渲染,主应用只负责提供容器节点提示:
activeRule的写法极其关键。'/app1'表示匹配/app1、/app1/、/app1/user,但不匹配/app11;而'/app1/'(带尾部斜杠)会匹配/app1/、/app1/user,但不匹配/app1。线上环境务必统一规范,我们团队约定全部用无尾部斜杠写法,避免歧义。
3.2 子应用改造:不是“加个插件”,而是“重构入口”
子应用改造是最大雷区。很多团队以为只要在 webpack 配置里加个qiankun-webpack-plugin就完事,结果上线后子应用白屏、样式错乱、接口 404。真相是:子应用必须同时满足“可独立运行”和“可被 qiankun 加载”两个条件。以 Vue 3 + Vite 项目为例,原始main.js:
import { createApp } from 'vue'; import App from './App.vue'; createApp(App).mount('#app');改造后必须变成:
import { createApp } from 'vue'; import App from './App.vue'; // 导出 qiankun 要求的生命周期函数 export async function bootstrap() { console.log('app1 bootstrap'); } export async function mount(props) { const { container } = props; // 关键:挂载点不再是 #app,而是 qiankun 传入的 container createApp(App).mount(container ? container.querySelector('#app') : '#app'); } export async function unmount(props) { const { container } = props; if (container) { // 清空容器内容,避免残留 container.innerHTML = ''; } } // 本地开发时,仍需独立运行能力 if (!window.__POWERED_BY_QIANKUN__) { mount({}); }同时,Vite 配置vite.config.js必须增加:
export default defineConfig({ build: { // 关键:设置为 umd 格式,让 qiankun 能正确执行 lib: { entry: path.resolve(__dirname, 'src/main.js'), name: 'app1', formats: ['umd'], }, // 关键:关闭混淆,否则 qiankun 无法识别生命周期函数 minify: false, // 关键:设置公共路径,确保静态资源(图片、字体)能正确加载 assetsDir: 'static', }, // 关键:配置 base,必须和主应用 activeRule 一致 base: window.__POWERED_BY_QIANKUN__ ? '/app1/' : './', });注意:
window.__POWERED_BY_QIANKUN__是 qiankun 注入的全局变量,用于区分运行环境。子应用必须用它来判断是否在 qiankun 环境下,从而决定用哪种挂载方式。这个判断不能省略,否则本地开发和线上环境会行为不一致。
3.3 样式隔离:strictStyleIsolation 不是银弹,但必须开启
样式污染是微前端最直观的灾难。一个子应用写了button { color: red; },另一个子应用的按钮全变红了。qiankun 提供strictStyleIsolation: true,原理是:在子应用 mount 时,把它的<style>标签内容提取出来,用 CSS 选择器重写(比如加前缀.app1-button),再插入到主应用<head>中;unmount 时,把这些动态插入的 style 标签全部移除。这招很有效,但有两个硬伤:一是 CSS 选择器层级过深时,重写可能导致权重失效;二是@import、@font-face等规则无法被重写。我们遇到过真实案例:子应用用了 Ant Design 的 icon 字体,@font-face规则没被隔离,导致所有子应用图标显示异常。解决方案是:在子应用中,所有全局样式(包括 UI 组件库)都必须用 CSS-in-JS 或 CSS Modules 封装。Ant Design 我们强制要求使用ConfigProvider的prefixCls属性自定义前缀,再配合:global()写少量重置样式。另外,experimentalStyleIsolation: true(实验性样式隔离)更激进:它会给子应用容器加一个随机 ID(如>registerMicroApps([ { name: 'app1', entry: '//localhost:7100', container: '#subapp-1', activeRule: '/app1', props: { userInfo: { id: 123, name: '张三' }, onLogout: () => { /* 主应用登出逻辑 */ } } } ]);
子应用接收:
export async function mount(props) { const { userInfo, onLogout } = props; // 直接使用 userInfo 渲染,onLogout 绑定到按钮 click 事件 }为什么不用initGlobalState?因为initGlobalState是全局状态管理,一旦滥用,就会退化成“微前端版 Vuex”,违背了微前端“解耦”的初衷。我们曾有一个项目,所有子应用都订阅同一个 globalState,结果一个子应用调用setGlobalState({ theme: 'dark' }),所有子应用主题瞬间切换,但其中某个子应用根本不支持暗色模式,直接报错崩溃。后来我们强制规定:props只传递与当前子应用强相关的、不可变的数据(如用户身份、权限码、当前语言);跨应用事件通知,必须通过自定义事件(window.dispatchEvent)或消息总线(如 mitt),且事件名必须带子应用前缀(app1:login-success),避免命名冲突。props是桥梁,不是高速公路——它只承载必需品,不运载行李。
4.3 错误监控与降级:子应用崩溃,不能拖垮主应用
微前端最大的风险是“牵一发而动全身”。一个子应用 JS 报错,如果没处理好,可能让整个页面卡死。qiankun 提供errorHandler配置,但它只捕获子应用bootstrap/mount/unmount阶段的同步错误。真正的 JS 运行时错误(比如 React 组件 render 报错),需要子应用自己兜底。我们在所有子应用的根组件里,加了componentDidCatch(React)或errorCaptured(Vue)钩子,捕获后上报 Sentry,并展示友好的降级 UI(如“模块加载失败,请稍后重试”)。更重要的是主应用的降级策略:我们给每个子应用容器加了>