
1. 为什么今天还得认真学 qiankun不是“又一个前端框架”而是大型应用的生存策略微前端这个词这两年被说烂了。但真正把 qiankun 落地到日均 PV 百万级、团队横跨 5 个业务线、技术栈混杂Vue2、Vue3、React16、Angular9、甚至还有遗留 jQuery 插件的生产环境里我才彻底明白qiankun 不是锦上添花的玩具它是大型 Web 应用在组织复杂性面前唯一能喘口气的呼吸阀。我带过的三个项目都卡在同一个地方新功能上线要等所有模块一起发版一个支付模块的 bug 修复得拉着中台、用户中心、风控、营销四个组的人通宵联调前端工程越来越臃肿yarn build从 3 分钟涨到 12 分钟CI/CD 流水线成了瓶颈新来的同学入职两周还在搞懂“这个按钮的点击事件到底是在哪个仓库的哪个文件里定义的”。这时候qiankun 的价值就不是“技术炫技”了而是直接解决人效、交付节奏和系统稳定性的三重危机。qiankun 微前端入门核心不在于会写几行registerMicroApp而在于理解它如何用“沙箱 生命周期 资源加载隔离”这三板斧把一个庞然大物切成可独立开发、独立部署、独立运行的乐高积木。它不强制你改技术栈——你的 Vue2 项目照跑React 项目照跑甚至你用 PythonDash 写的内部数据看板只要它能输出一个标准的 HTML 入口就能被主应用接入。这才是它比其他方案更务实的地方不颠覆只缝合。适合谁看这篇如果你正面临这些情况中的任意一条这篇就是为你写的你所在的团队超过 10 人前端代码分散在 3 个以上 Git 仓库你每次发版都要协调后端、测试、运维流程长到记不清上次成功发布是什么时候你听说过“微前端”但一查文档全是概念图不知道第一步该删哪行代码、加哪段配置你试过自己手写 iframe 或手动加载 script结果发现样式冲突、路由跳转失灵、全局变量污染最后放弃。别担心接下来我会带你从零开始用一个真实可运行的 demo把 qiankun 的启动、通信、样式隔离、公共依赖提取、错误降级这些关键环节全部拆开揉碎讲清楚。不是照着官网抄一遍而是告诉你每一行代码背后它在解决什么问题、踩过什么坑、为什么非这么写不可。2. qiankun 架构设计与选型逻辑为什么是 qiankun而不是 single-spa、micro-app 或自研2.1 三种主流微前端方案的本质差异决定了你的选型成本很多团队在选型时第一反应是去 GitHub 看 star 数。qiankun 确实星多但数字不能代替判断。我们得回到最根本的问题你要解决什么你的团队现状是什么你的技术债有多厚我把目前主流的三种方案按“对现有项目的侵入性”和“对团队协作的改造成本”两个维度画了个坐标图虽然不能用 mermaid但你可以脑补single-spa像一个极简的“路由器调度器”。它只管一件事当 URL 变化时决定该加载哪个子应用、启动哪个、卸载哪个。它不提供沙箱、不处理样式隔离、不帮你管理公共依赖。好处是轻量、灵活坏处是你得自己实现 JS 沙箱比如用 Proxy 拦截 globalThis、自己写 CSS Scoped 注入逻辑、自己处理子应用间的通信。我们曾在一个 React 主应用里试过光是解决子应用卸载后 React 组件未销毁导致的内存泄漏就花了三天——这不是写业务这是在给框架写补丁。micro-app由京东开源主打“零侵入”。它的思路很激进用 Custom Element 封装子应用所有资源加载、生命周期、通信都通过 Web Components 原生机制完成。理论上你连子应用的入口文件都不用改。但现实是它对浏览器兼容性要求高IE 完全不支持且当你需要深度定制子应用行为比如在某个路由下强制静默加载时它的 API 抽象层反而成了障碍。我们一个面向政企客户的项目因客户内网浏览器版本锁定在 IE11直接 pass。qiankun它走的是“务实中间路线”。它提供了开箱即用的 JS 沙箱基于 Proxy 和快照沙箱双模式、CSS 样式隔离通过动态创建 style 标签并 scoped、完善的生命周期钩子、以及简单直接的通信机制initGlobalState。最关键的是它对子应用的改造要求极低Vue 项目加两行setPublicPathReact 项目加一个render函数导出连 webpack 配置都只需加一个library字段。我们一个存量 Vue2 项目从开始改造到接入主应用只用了半天——这半天里有 40 分钟是在等 CI 流水线跑完。提示选型不是比功能多寡而是比“最小可行改造路径”。qiankun 的优势在于它把 80% 的共性难题沙箱、样式、通信封装好了让你能把精力聚焦在业务本身。如果你的团队没有专职的前端基建工程师qiankun 是风险最低的选择。2.2 qiankun 的核心设计哲学沙箱、生命周期、资源加载三位一体qiankun 的代码并不复杂但它的设计思想非常清晰。理解这三点你就掌握了它的灵魂第一沙箱Sandbox——不是为了“安全”而是为了“可预测”很多人误以为沙箱是为了防 XSS其实不是。qiankun 的沙箱核心目标是确保子应用的 JS 执行不会意外污染主应用或其他子应用的全局环境。比如子应用 A 里写了window.xxx a子应用 B 里写了window.xxx b如果没有沙箱B 的赋值会覆盖 A 的导致 A 运行异常。qiankun 的 Proxy 沙箱会拦截所有对window的读写操作把它们映射到一个独立的fakeWindow对象上。当子应用卸载时这个 fakeWindow 就被丢弃干净利落。快照沙箱则更简单粗暴在子应用 mount 前先拍一张window的快照unmount 时再把所有被修改的属性还原回去。两种模式各有适用场景qiankun 会自动降级。第二生命周期Lifecycle——让“启动”和“卸载”变得可控单页应用最大的痛点是“卸载”。传统 SPA 里页面切换只是组件销毁但微前端里“卸载”意味着要彻底清理子应用注册的所有定时器、事件监听器、全局变量、甚至 DOM 节点。qiankun 强制子应用暴露bootstrap、mount、unmount三个函数。bootstrap是初始化阶段只执行一次适合做预加载mount是挂载阶段每次进入该子应用路由时触发负责渲染 UIunmount是卸载阶段必须在这里清除所有副作用。我们曾有个子应用忘了在unmount里清除setInterval结果用户切到其他模块后那个定时器还在后台疯狂请求接口拖垮了整个系统的性能监控告警。第三资源加载Resource Loading——不只是“加载 JS”更是“加载上下文”qiankun 加载子应用不是简单地document.createElement(script)。它会解析子应用的 HTML 入口文件提取其中所有的script、link标签并按顺序加载执行。更重要的是它会把子应用的publicPath静态资源根路径动态注入到子应用的运行时环境中。这意味着子应用里写的import ./assets/logo.png最终请求的 URL 会是https://subapp.example.com/assets/logo.png而不是主应用的域名。这个细节直接决定了子应用能否正确加载图片、字体、CSS 文件——我们第一个失败的 demo就是因为没配setPublicPath所有图片 404页面一片空白。这三者不是孤立的。沙箱保证了mount和unmount的执行环境纯净生命周期钩子为沙箱的创建和销毁提供了时机而资源加载则是整个流程的起点和数据基础。理解了这个闭环你再去看 qiankun 的源码就会发现它的结构异常清晰。3. 从零搭建 qiankun 实战主应用与子应用的完整配置与调试3.1 主应用Main App不是“壳”而是“操作系统内核”主应用不是空架子它是整个微前端体系的调度中心、状态中枢和错误兜底者。我们用 Vue3 Vite 创建一个主应用目录结构如下main-app/ ├── src/ │ ├── main.js # 入口文件初始化 qiankun │ ├── router/index.js # 主应用路由定义子应用挂载点 │ └── layouts/ │ └── MicroLayout.vue # 微前端专属布局含 router-view 和子应用容器 ├── public/ │ └── micro-apps/ # 子应用静态资源托管目录用于本地开发 └── vite.config.js # Vite 配置需处理子应用资源代理第一步安装与初始化npm install qiankun --save在src/main.js中import { registerMicroApp, start } from qiankun; // 1. 注册子应用这里定义了子应用的名称、入口、挂载节点、激活规则 registerMicroApp({ name: vue-app, // 子应用唯一标识 entry: //localhost:7100, // 子应用 dev server 地址生产环境换成 CDN 地址 container: #vue-app-container, // 主应用中预留的 DOM 节点 activeRule: /vue, // 当主应用路由匹配此规则时激活该子应用 }); registerMicroApp({ name: react-app, entry: //localhost:7101, container: #react-app-container, activeRule: /react, }); // 2. 启动 qiankun注意必须在主应用的 Vue app.mount() 之后调用 start({ sandbox: { strictStyleIsolation: true }, // 开启严格样式隔离 });注意start()必须在主应用的app.mount(#app)之后调用。我第一次写的时候把它放在createApp之前结果 qiankun 找不到#app容器报错信息极其晦涩调试了半小时才定位到。这是新手最容易踩的坑之一。第二步路由与布局在src/router/index.js中我们定义主应用的路由import { createRouter, createWebHistory } from vue-router; const routes [ { path: /, redirect: /vue }, { path: /vue, name: VueApp, component: () import(/layouts/MicroLayout.vue), children: [ { path: , name: VueHome, component: () import(/views/VueHome.vue) } ] }, { path: /react, name: ReactApp, component: () import(/layouts/MicroLayout.vue), children: [ { path: , name: ReactHome, component: () import(/views/ReactHome.vue) } ] } ]; const router createRouter({ history: createWebHistory(), routes }); export default router;关键在MicroLayout.vuetemplate div classmicro-layout !-- 主应用自己的导航栏 -- nav classmain-nav router-link to/vueVue 子应用/router-link router-link to/reactReact 子应用/router-link /nav !-- 主应用自己的内容区域 -- main classmain-content router-view / /main !-- 子应用的挂载容器必须有唯一 ID -- div idvue-app-container classsub-app-container/div div idreact-app-container classsub-app-container/div /div /template这里有两个关键点#vue-app-container和#react-app-container的 ID 必须与registerMicroApp中的container字段完全一致这两个容器必须是position: relative或static不能是position: fixed或absolute否则子应用的router-view渲染位置会错乱。我们曾因为一个全局 CSS 重置了* { position: relative }导致子应用内容全部堆在左上角排查了整整一天。第三步Vite 代理配置开发环境必备在vite.config.js中为子应用的静态资源添加代理避免跨域export default defineConfig({ server: { proxy: { /vue: { target: http://localhost:7100, changeOrigin: true, rewrite: (path) path.replace(/^\/vue/, ) }, /react: { target: http://localhost:7101, changeOrigin: true, rewrite: (path) path.replace(/^\/react/, ) } } } });这样当主应用请求http://localhost:3000/vue/static/js/app.js时Vite 会自动转发到http://localhost:7100/static/js/app.js。生产环境则直接用 CDN 地址无需代理。3.2 子应用Sub App改造的核心就在这三行代码子应用的改造是整个过程中最“轻”的部分。我们以一个 Vue2 子应用为例vue-app目录结构vue-app/ ├── src/ │ ├── main.js # 入口文件需暴露生命周期函数 │ ├── set-public-path.js # 关键设置 publicPath │ └── ... ├── vue.config.js # Vue CLI 配置 └── package.json第一步设置 publicPath生死线创建src/set-public-path.js// 此文件必须在入口文件的第一行引入 if (window.__POWERED_BY_QIANKUN__) { // 如果是 qiankun 环境动态设置 publicPath __webpack_public_path__ window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; } else { // 如果是独立运行使用默认 publicPath __webpack_public_path__ /; }然后在src/main.js的最顶部引入它import ./set-public-path; // 必须是第一行 import Vue from vue; import App from ./App.vue; import router from ./router; let instance null; // 导出 qiankun 要求的生命周期函数 export async function bootstrap() { console.log(vue app bootstraped); } export async function mount(props) { const { container } props; instance new Vue({ router, render: h h(App) }).$mount( container ? container.querySelector(#app) : #app // 关键挂载到传入的 container 内 ); } export async function unmount() { if (instance) { instance.$destroy(); instance.$el.innerHTML ; instance null; } }注意mount函数里的container.querySelector(#app)是重点。子应用的index.html里必须有一个div idapp/divqiankun 会把这个 div 插入到主应用的#vue-app-container里。如果子应用的根节点 ID 不叫app或者你用了#root那这里就必须同步修改。第二步Vue CLI 配置vue.config.jsmodule.exports { configureWebpack: { output: { library: vueApp, // 必须与 registerMicroApp 的 name 一致 libraryTarget: umd, // 输出 UMD 格式供主应用动态加载 jsonpFunction: webpackJsonp_vueApp, // 避免多个子应用的 webpackJsonp 冲突 } }, devServer: { port: 7100, // 开发端口与主应用的 proxy 配置对应 headers: { Access-Control-Allow-Origin: * // 开发时允许跨域 } } };第三步React 子应用的等价改造React 子应用react-app的改造逻辑完全一致只是语法不同。在src/index.js中import(./set-public-path); // 同样第一行 import React from react; import ReactDOM from react-dom/client; import App from ./App; let root null; export async function bootstrap() { console.log(react app bootstraped); } export async function mount(props) { const { container } props; root ReactDOM.createRoot( container ? container.querySelector(#root) : document.getElementById(root) ); root.render(App /); } export async function unmount() { if (root) { root.unmount(); } }webpack.config.js中同样要配置output.library和libraryTarget。到这里主应用和两个子应用的骨架就搭好了。启动命令# 分别在三个终端中运行 cd main-app npm run dev cd vue-app npm run serve cd react-app npm run start打开http://localhost:3000/vue你应该能看到 Vue 子应用正常渲染切换到/reactReact 子应用也应正常显示。恭喜你已经完成了 qiankun 最核心的“能跑起来”阶段。4. qiankun 核心功能详解与避坑指南通信、样式、公共依赖、错误处理4.1 子应用间通信不是“共享状态”而是“受控消息传递”qiankun 不鼓励子应用之间直接访问对方的window或store。它提供了一套轻量的消息总线机制initGlobalState其设计哲学是主应用是唯一的“状态仲裁者”子应用只能向主应用“申请”或“通知”。主应用中初始化状态import { initGlobalState } from qiankun; // 初始化一个全局状态对象 const actions initGlobalState({ user: null, theme: light }); // 监听状态变化 actions.onGlobalStateChange((state, prevState) { console.log(主应用收到状态变更:, state, 之前是:, prevState); // 这里可以触发主应用自身的响应比如更新主题色 }); // 主应用可以主动设置状态 actions.setGlobalState({ user: { name: 张三, role: admin } });子应用中使用状态// 在子应用的 mount 函数中 export async function mount(props) { const { onGlobalStateChange, setGlobalState, offGlobalStateChange } props; // 订阅状态变化 onGlobalStateChange((state, prevState) { console.log(子应用收到状态变更:, state); // 更新子应用自己的局部状态 }, true); // true 表示立即触发一次获取当前值 // 向主应用发送更新 setGlobalState({ theme: dark }); // 卸载时取消订阅防止内存泄漏 return () { offGlobalStateChange(); }; }实操心得不要试图用setGlobalState传递大量数据如整个用户权限树这会引发频繁的序列化/反序列化开销。我们通常只传递关键的、影响 UI 的字段如theme、locale、user.name复杂的业务数据还是走各自独立的 API 请求。另外onGlobalStateChange的回调函数必须是纯函数不能包含异步操作或副作用否则会导致状态更新顺序混乱。4.2 样式隔离strictStyleIsolation与sandbox的协同作战样式污染是微前端最头疼的问题。qiankun 提供了两种方式strictStyleIsolation: true推荐qiankun 会为每个子应用动态创建一个style标签并将子应用的所有 CSS 规则包裹在:host伪类中现代浏览器或通过scoped属性模拟旧浏览器。这意味着子应用的.btn { color: red; }在 DOM 中实际生效的是#vue-app-container .btn { color: red; }。它几乎 100% 隔离但代价是子应用无法通过 CSS 选择器影响主应用的样式这本来就是好习惯。sandbox: { experimentalStyleIsolation: true }这是更底层的隔离它会把子应用的整个head中的style和link标签克隆一份并插入到子应用的容器节点内部。这种方式更彻底但对某些依赖document.head注入样式的第三方库如某些 UI 组件库可能不兼容。我们线上项目统一采用strictStyleIsolation: true。它足够可靠且性能损耗极小。唯一要注意的是如果你的子应用用了 CSS-in-JS如 styled-components需要额外配置StyleSheetManager的target参数指向子应用的容器节点否则样式会注入到document.head逃逸出隔离范围。4.3 公共依赖Common Dependencies抽离 lodash、moment 等减少包体积当多个子应用都用到了lodash、axios、moment时如果不做处理每个子应用打包都会包含一份完整的副本造成巨大的冗余。qiankun 支持在主应用中提供这些公共依赖子应用在运行时直接使用。主应用中提供// 在 main.js 中start 之前 import _ from lodash; import axios from axios; import moment from moment; start({ // ... // 告诉 qiankun这些全局变量名对应哪些模块 getPublicPath: () /, plugins: [ { // 自定义插件用于注入全局变量 beforeLoad: [() { window._ _; window.axios axios; window.moment moment; }] } ] });子应用中使用在子应用的webpack.config.js中将这些库标记为externalmodule.exports { externals: { lodash: _, axios: axios, moment: moment } };这样子应用的 bundle 就不再打包这些库而是直接从window上取。我们一个项目有 5 个子应用抽离lodash和moment后每个子应用的 JS 包体积平均减少了 120KB首屏加载时间缩短了 300ms。注意externals的 key 是模块名lodashvalue 是全局变量名_必须一一对应。如果子应用里写的是import { debounce } from lodash而主应用只挂了window._那debounce就会是undefined。所以要么统一用import _ from lodash要么在主应用中挂载window.lodash _。4.4 错误处理与降级当子应用崩溃时主应用不能跟着死这是 qiankun 最体现工程成熟度的设计。它内置了error钩子当子应用在bootstrap、mount、unmount任一阶段抛出未捕获异常时qiankun 会捕获它并触发error回调。registerMicroApp({ name: vue-app, entry: //localhost:7100, container: #vue-app-container, activeRule: /vue, error: (err) { console.error(Vue 子应用加载失败:, err); // 这里可以 // 1. 上报错误到 Sentry // 2. 显示友好的降级提示 // 3. 自动重试加载 document.querySelector(#vue-app-container).innerHTML div classerror-placeholder h3服务暂时不可用/h3 p我们正在紧急修复请稍后再试。/p button onclicklocation.reload()刷新重试/button /div ; } });我们线上还加了一层自动重试error: (err) { let retryCount 0; const maxRetry 3; const tryLoad () { if (retryCount maxRetry) { retryCount; setTimeout(() { // 重新注册并启动 registerMicroApp({ /* ... */ }); start(); }, 1000 * retryCount); } }; tryLoad(); }这套机制让我们在子应用因 CDN 故障或构建失败导致白屏时主应用依然可用用户体验损失降到最低。5. qiankun 实战常见问题与排查技巧从白屏到性能优化的全链路指南5.1 “白屏”问题排查速查表90% 的白屏都源于这五个原因现象最可能原因排查步骤解决方案主应用能打开但子应用区域一片空白控制台无报错子应用的containerID 与主应用registerMicroApp中的container字段不一致1. 打开主应用 DevTools检查#vue-app-container是否存在2. 检查registerMicroApp的container值是否与之完全相同包括大小写、空格确保 ID 严格一致建议在registerMicroApp中直接写document.getElementById(vue-app-container)而不是字符串让 JS 运行时报错更明确子应用能加载但路由跳转后内容不更新始终显示首页子应用的router没有配置base或activeRule与子应用路由前缀不匹配1. 检查子应用router的base是否为/qiankun 会自动处理应保持为 /2. 检查activeRule是否为/vue而子应用内部路由是否也以/vue开头子应用路由base必须为/activeRule定义的是主应用的激活规则子应用内部路由应使用相对路径如/list、/detail/:id子应用加载后控制台报xxx is not defined子应用的publicPath未正确设置导致 JS/CSS 文件 4041. 打开 Network 面板过滤js、css看是否有 404 请求2. 检查set-public-path.js是否在入口文件第一行引入确保set-public-path.js是第一行检查__webpack_public_path__的赋值逻辑生产环境确认 CDN 地址正确子应用能显示但点击按钮无反应控制台报Cannot read property xxx of undefined子应用的mount函数中container.querySelector(#app)返回null1. 在mount函数开头console.log(container)确认容器存在2. 检查子应用index.html中是否有div idapp/div确保子应用index.html的根节点 ID 与mount中查询的 ID 一致或者在mount中直接使用container作为挂载点app.$mount(container)子应用加载后样式完全错乱按钮变大、文字重叠未开启样式隔离或子应用 CSS 使用了全局选择器如body { margin: 0; }1. 检查start()配置中sandbox.strictStyleIsolation是否为true2. 检查子应用 CSS 是否有html、body、*等全局重置开启strictStyleIsolation将全局重置 CSS 移到主应用中子应用 CSS 使用 BEM 或 CSS Modules实操心得遇到白屏永远先看 Network 面板再看 Console。90% 的问题Network 里一个 404 就定位了。不要一上来就怀疑 qiankun 源码先怀疑自己的配置。5.2 性能优化从 2s 到 300ms 的加载加速实践qiankun 的加载性能主要瓶颈在子应用的 HTML 入口解析和资源加载。我们通过以下四步将子应用首次加载时间从 2s 优化到 300ms第一步预加载Preload在主应用路由守卫中当用户鼠标悬停在某个子应用菜单项上时就提前加载其 HTML 入口router.beforeEach((to, from, next) { if (to.name VueApp) { // 预加载 Vue 子应用 fetch(//localhost:7100/index.html).catch(() {}); } next(); });第二步资源懒加载Lazy Load子应用的index.html中只保留最核心的 JS/CSS将非首屏资源如图表库、富文本编辑器改为动态import()// 子应用中 const loadChart () import(echarts); const loadEditor () import(wangeditor/editor); // 在需要时调用 loadChart().then(({ default: echarts }) { // 初始化图表 });第三步CDN 与 HTTP/2所有子应用的静态资源JS、CSS、图片全部托管在 CDN 上并启用 HTTP/2 多路复用。我们对比过HTTP/2 下10 个子应用资源并行加载比 HTTP/1.1 快 40%。第四步主应用缓存子应用资源利用 Service Worker在主应用中缓存已加载过的子应用资源。下次用户访问时直接从 Cache Storage 读取无需网络请求。这部分代码较长核心是监听fetch事件对子应用资源 URL 进行缓存策略匹配。5.3 生产环境部署 checklist上线前必须核对的 12 项✅ 主应用start()调用时机确保在app.mount()之后。✅ 子应用set-public-path.js必须是入口文件第一行。✅ 子应用webpack配置output.library与registerMicroApp.name严格一致。✅ 子应用devServer.headers开发环境必须设置Access-Control-Allow-Origin: *。✅ 主应用proxy配置开发环境代理路径与子应用activeRule一致。✅strictStyleIsolation生产环境必须开启。✅ 公共依赖externals主应用提供子应用声明名称一一对应。✅error钩子必须实现包含错误上报和降级 UI。✅ 子应用unmount必须清理所有定时器、事件监听器、全局变量。✅ 子应用routerbase必须为/不能是/vue。✅ CDN 地址生产环境entry必须是 HTTPS 的 CDN 地址不能是http://。✅ 沙箱模式生产环境优先使用Proxy沙箱sandbox: { loose: false }。我们把这份清单做成了一个自动化脚本每次 CI 构建时运行任何一项不满足就直接 fail。这避免了人为疏忽导致的线上事故。5.4 与 PythonDash 等非 JS 框架的集成纯前端也能“接住”标题里提到的“pythondash快速web应用开发”很多人以为 qiankun 只能接 JS 应用。其实不然。Dash 应用本质上就是一个 Flask/FastAPI 服务它返回一个标准的 HTML 页面。我们只需要让这个 HTML 页面满足 qiankun 的两个要求提供bootstrap、mount、unmount三个函数在 Dash 的index.html模板中加入一段 JSscript window.dashApp { bootstrap: () Promise.resolve(), mount: (props) { // Dash 应用的根容器是 #dash-container const container props.container || document.getElementById(dash-container); // Dash 会自动渲染到 #dash-container我们只需确保它存在 if (!document.getElementById(dash-container)) { const div document.createElement(div); div.id dash-container; container.appendChild(div); } return Promise.resolve(); }, unmount: () Promise.resolve() }; /script静态资源路径可配置在 Dash 的app Dash(...)初始化时指定requests_pathname_prefix为子应用的activeRule比如/dash。然后在主应用中registerMicroApp({ name: dash