微前端这个概念刚出来那阵子我其实是不太买账的觉得把好好的应用拆成几块反而增加复杂度。直到接手一个后台系统三个业务团队同时往一个仓库里提代码发版要排期改个按钮颜色都能引发冲突才真正体会到微前端要解决的到底是什么问题。这篇内容就聊 qiankun 这套微前端框架具体是Vue3 Vite qiankun的组合给你一份能直接照着敲的搭建流程。适合的人群很明确已经会写 Vue3但对微前端只停留在听过名词阶段想在自己项目里落地的同学也适合已经搭了一半卡在白屏、资源 404、样式串味这些坑里的同行。我会把每一步为什么这么做讲清楚尤其是 Vite 和 qiankun 这对组合里那些不太显眼但很致命的细节。1. 拆之前先想清楚这套组合要面对的到底是什么动手之前得先把问题定义清楚。微前端不是架构炫技它本质上解决的是组织协作和技术异构两个问题。组织协作是指多个团队共用一个产品外壳但各自独立迭代技术异构是指老系统跑着 Vue2 或者干脆是 jQuery 写的页面新功能想用 Vue3 写又不想推翻重来。如果你手上只是一个三五人的小项目一个人维护那微前端的收益远远覆盖不了它的成本老老实实做单仓多包更舒服。1.1 从主应用、子应用、注册表三个角色理解 qiankunqiankun 的模型说白了就三个角色。主应用是容器负责布局、菜单、登录态、路由分发它自己可以不写任何业务子应用是独立打包、独立部署的一个个前端工程每个都有自己的 index.html 和入口脚本注册表则是主应用里的一份配置告诉 qiankun这个名字叫 xxx 的应用代码从哪个地址加载什么时候激活它。生活化的类比主应用是一栋写字楼的物业管理处负责门禁、水电、楼层指引子应用是各个租户公司自己装修自己招人但共用同一部电梯。qiankun 做的事就是当一个物业经理在你走进某层楼时把那家公司的装修HTML、CSS、JS临时搬进这层楼的空间里并且保证三家公司的装修风格不互相污染、员工不会走错门。理解了这三个角色后面所有配置项都能对号入座。记住一句口诀主应用管规则子应用管自己注册表管调度。1.2 为什么不用 iframe也不用 npm 包有人会问iframe 天然沙箱样式和 JS 完全隔离为什么不直接用iframe 的致命问题有四个一是路由状态无法同步浏览器地址栏和 iframe 内部路由是两套东西刷新就丢二是弹窗、抽屉这些组件只能在 iframe 的矩形范围内显示做不了全局遮罩三是父子通信只能靠 postMessage异步且难以传函数四是首屏体验差iframe 加载是硬性的白屏等待。这几点在后台管理系统里几乎条条踩雷。另一条路是把子应用打成 npm 包主应用 install 进来当组件用。这条路在组件复用层面很香但它要求所有子应用必须用同一套构建工具、同一个 Vue 版本、同一套依赖树一旦有两个团队依赖的 UI 库版本冲突就是死结。而且它做不到独立部署——子应用发版必须连带主应用一起发。qiankun 走的是中间路线运行时加载 独立部署 沙箱隔离。子应用是真正独立的工程构建产物放在自己的服务器上主应用只需要知道 URL。产品迭代节奏可以完全分开这是它最核心的价值。1.3 Vite 遇上 qiankun绕不开的三个硬骨头这是整篇文章最需要重点说的地方因为网上大量教程还停留在 webpack 时代直接套用会撞墙。qiankun 加载子应用的方式是用 fetch 拿到子应用的 HTML解析出 script 标签然后执行脚本执行完从全局拿到bootstrap、mount、unmount这三个生命周期函数。webpack 模式下只要在入口文件里export这三个函数UMD 打包会挂到window[appName]上qiankun 就能取到。Vite 开发模式不打包走的是原生 ESM浏览器直接 import 一堆.vue、.ts模块。这时候 qiankun 拿到的 HTML 里是一堆script typemodule而 qiankun 的沙箱对 ESM 的支持很有限脚本执行时机、模块作用域都是问题。这就产生了第一个硬骨头开发模式下生命周期函数挂不到全局。第二个硬骨头是资源路径。Vite 默认把资源路径写成绝对路径/assets/xxx.js子应用部署在/sub-app/下面主应用一加载就去找主应用根目录的资源直接 404。开发模式下这个问题更隐蔽因为端口不同跨域和路径同时存在。第三个是跨域。主应用在 5173 端口子应用在 5174 端口qiankun 用 fetch 去拉子应用的 HTML浏览器直接拦截。这个必须在子应用的 dev server 里显式开 CORS。对应这三个问题行业里目前最通用的解法是社区插件vite-plugin-qiankun。它做的事情很巧妙在开发模式下把一个模拟的生命周期对象通过window暴露出去同时提供一个renderWithQiankun的高阶函数帮你注册生命周期在生产构建时它把入口改造成 qiankun 能识别的 UMD 风格。我实测下来这个插件在 Vite 4 和 Vite 5 上都能跑是当前比较稳的方案。注意如果你在搜索时看到有人建议用 vite 的 legacy 插件降级成 UMD不要走这条路。legacy 插件是给老浏览器做语法降级的跟模块格式完全是两回事强行用会引入一堆 polyfill 反而更难排查。2. 主应用搭建容器、注册与路由联动的实操主应用是整个体系的骨架它的配置质量直接决定了后面调试的痛苦程度。我的建议是主应用越薄越好——只保留布局、登录、菜单、路由分发这四件事业务逻辑一律下放到子应用。2.1 初始化与依赖安装的取舍用 Vite 起一个 Vue3 TS 的主应用标准命令就行。依赖上只需要装qiankun版本建议锁在 2.10.x这是目前最稳定的一个大版本。npm create vitelatest main-app -- --template vue-ts cd main-app npm i npm i qiankun这里有个细节值得说不要在主应用里装子应用的业务依赖。我见过有人在主应用里装了 Element Plus想着子应用也能共用结果子应用自己也装了一份最终页面里跑着两份不同版本的组件库样式错乱到怀疑人生。公共依赖的正确做法后面第 4 章会展开。主应用的目录结构建议这样组织main-app/ ├── src/ │ ├── micro/ │ │ ├── apps.ts # 子应用注册表 │ │ └── index.ts # qiankun 启动逻辑 │ ├── layout/ │ │ └── BasicLayout.vue # 带 div idsub-container 的容器 │ ├── router/ │ └── main.ts把微前端相关的东西全部收在micro目录里好处是将来如果想换方案比如换成别的微前端框架只需要动这一个目录。2.2 注册表设计把硬编码地址抽成环境变量先看子应用注册表怎么写。很多人喜欢把 entry 地址写死这在本地开发时很省事一上测试环境就全废。我的做法是用环境变量兜底// src/micro/apps.ts export interface MicroAppConfig { name: string entry: string container: string activeRule: string props?: Recordstring, unknown } const isDev import.meta.env.DEV export const microApps: MicroAppConfig[] [ { name: order-app, entry: isDev ? //localhost:5174 : ${import.meta.env.VITE_SUB_BASE}/order-app/, container: #sub-container, activeRule: /order, props: { basePath: /order } }, { name: user-app, entry: isDev ? //localhost:5175 : ${import.meta.env.VITE_SUB_BASE}/user-app/, container: #sub-container, activeRule: /user, props: { basePath: /user } } ]几个配置项的含义必须讲透。name是子应用的唯一标识必须跟子应用里注册的名字完全一致一个字都不能差这是最容易出错的地方之一。container是选择器字符串指向主应用里那个占位 div。activeRule是激活规则qiankun 会拿当前浏览器的 location.pathname 去匹配它匹配上了才去加载和挂载。提示activeRule用字符串前缀匹配时/user会同时匹配/user和/user-detail。如果你有两个应用分别叫/user和/user-detail会被同时激活。稳妥做法是把它写成函数(location) location.pathname.startsWith(/user/)或者干脆加个路径分隔符。接下来是启动逻辑。这里我加了加载态和错误兜底因为线上环境子应用偶发加载失败是常态不能让用户盯着白屏。// src/micro/index.ts import { registerMicroApps, start, setDefaultMountApp } from qiankun import { microApps } from ./apps export function setupMicroApps(router: any) { registerMicroApps(microApps as any, { beforeLoad: [(app) { console.log([qiankun] 即将加载 ${app.name}) return Promise.resolve() }], beforeMount: [(app) { console.log([qiankun] 挂载 ${app.name}) return Promise.resolve() }], afterUnmount: [(app) { console.log([qiankun] 卸载 ${app.name}) return Promise.resolve() }] }) start({ prefetch: all, sandbox: { experimentalStyleIsolation: true }, singular: false }) }prefetch: all的意思是主应用挂载完成后空闲时间预取所有子应用的静态资源。这个选项很值得开用户点菜单时子应用几乎秒开代价是首屏多消耗一点带宽。如果你子应用特别多超过五个建议改成prefetch: all加一个白名单或者干脆关掉按需加载。sandbox.experimentalStyleIsolation: true开启的是实验性样式隔离qiankun 会给子应用的每个样式选择器加上div[data-qiankunappName]前缀。这个方案不完美对body、html选择器无效对动态插入的样式可能失效但比strictStyleIsolation的 Shadow DOM 方案兼容性好得多——Shadow DOM 会让子应用里的弹窗、抽屉全部被限制在容器内也拿不到 document很多组件库直接罢工。2.3 布局容器与路由兜底两个必踩的坑容器写法本身很简单但要配合v-show或者条件渲染。因为 qiankun 卸载子应用时是把容器内容清空容器本身得留着!-- src/layout/BasicLayout.vue -- template div classlayout aside classsidebar router-link to/order/list订单中心/router-link router-link to/user/profile用户中心/router-link /aside main classcontent router-view / div idsub-container classsub-container/div /main /div /template这里第一个坑来了主应用的 router 必须给子应用路径留出位置否则 Vue Router 会因为匹配不到路由而报警告甚至在history模式下把地址重写。正确做法是在主应用路由里加一条通配规则把/order/*和/user/*全部导向一个空组件// src/router/index.ts const routes [ { path: /, redirect: /order/list }, { path: /order/:pathMatch(.*)*, component: () import(/layout/Empty.vue) }, { path: /user/:pathMatch(.*)*, component: () import(/layout/Empty.vue) } ]第二个坑是主应用的根路由重定向。如果你的/重定向到/order/list而/order又要被 qiankun 接管就会形成主应用先把路径改掉qiankun 再去匹配的时序问题。踩过一次之后我的处理方式是主应用只负责在菜单点击时router.push(/order/list)不在路由表里做重定向让 qiankun 自己根据activeRule去接管。加载顺序上registerMicroApps必须在app.mount()之前或者同时调用不然首屏进入子应用路由时容器还没准备好。还有一点容易忽略主应用如果用createWebHistory部署到 nginx 后刷新子应用路径会 404需要在 nginx 里配try_files $uri $uri/ /index.html。这个在第 6 章会给出完整配置。3. 子应用改造让 Vite 产物符合 qiankun 的协议子应用这边的改造量比主应用大但也更机械照着做基本不会错。核心是三件事装插件、改入口、调配置。3.1 vite-plugin-qiankun 的接入与参数含义先装依赖npm i vite-plugin-qiankun -D然后在vite.config.ts里配置。这段配置信息密度很高我逐项解释import { defineConfig } from vite import vue from vitejs/plugin-vue import qiankun from vite-plugin-qiankun const useDevMode process.env.NODE_ENV ! production const APP_NAME order-app export default defineConfig({ base: useDevMode ? / : /order-app/, server: { port: 5174, cors: true, headers: { Access-Control-Allow-Origin: * } }, plugins: [ vue(), qiankun(APP_NAME, { useDevMode }) ] })qiankun(APP_NAME, options)里的 APP_NAME 必须跟主应用注册表里的name严格一致这是第一道坎。useDevMode为 true 时插件走开发模式模拟路径会把生命周期对象挂到window[APP_NAME]上为 false 时走生产构建改造路径把入口输出成 qiankun 可直接消费的格式。base的取值同样关键。开发模式下必须是/因为子应用这时是独立跑在 5174 端口的资源从自己根目录拿生产模式必须是子应用实际部署的目录/order-app/。很多人本地跑得好好的一构建就白屏八成就是这里写死了。server.cors和那个 header 是必须的缺了就是控制台一行红字Access to fetch at http://localhost:5174 from origin http://localhost:5173 has been blocked by CORS policy。这个错误信息很明确但新手容易被跨域两个字吓住其实加一行配置就完事。3.2 入口文件的生命周期导出写错一个字就白屏子应用的main.ts是整个改造里最关键的文件。webpack 时代我们会直接export async function mount()Vite 下必须用插件提供的高阶函数// src/main.ts import { createApp, type App as VueApp } from vue import { renderWithQiankun, qiankunWindow } from vite-plugin-qiankun/dist/helper import App from ./App.vue import { createRouter } from ./router let app: VueApp | null null let router: any null function render(container?: HTMLElement) { const isMicro qiankunWindow.__POWERED_BY_QIANKUN__ router createRouter(isMicro ? /order : /) app createApp(App) app.use(router) app.mount(container ? container.querySelector(#app)! : #app) } renderWithQiankun({ bootstrap() { console.log([order-app] bootstrap) }, mount(props) { console.log([order-app] mount, props) render(props.container) }, unmount() { console.log([order-app] unmount) app?.unmount() app null router null }, update() { console.log([order-app] update) } }) if (!qiankunWindow.__POWERED_BY_QIANKUN__) { render() }几个细节必须强调。第一qiankunWindow.__POWERED_BY_QIANKUN__这个标志位决定了当前是被主应用加载还是独立访问。它必须用来控制路由 base 和是否自动挂载否则子应用单独访问时会因为等不到 mount 而白屏。第二app.mount(target)的 target 在微前端模式下不能是#app字符串选择器因为主应用页面里可能同时存在多个#app主应用自己的 子应用模板里的必须从props.container里找。第三unmount里一定要把app置空不然切换应用时旧实例的响应式引用还活着内存泄漏是一方面更恶心的是定时器和事件监听没销毁切回来会执行两次。注意子应用的index.html里那个div idapp要保留但微前端模式下它会被塞进主应用的容器里。不要给它写全局样式比如#app { height: 100vh }否则会撑破主应用布局。3.3 路由 base 与 keep-alive 的处理策略路由这块子应用必须做成双模式独立运行时 base 是/被主应用加载时 base 是/order。用createWebHistory(base)动态传参就够了上面代码里的createRouter(isMicro ? /order : /)就是干这个的。这里有个隐藏问题主应用的activeRule是/order子应用的路由 base 也是/order那么子应用内部跳转到/order/list时浏览器地址栏变成/order/list主应用的 router 会尝试匹配/order/:pathMatch(.*)*匹配成功页面不跳。整个链路是通的。但如果子应用内部用了router.push(/list)这种相对写法而 base 没生效地址栏就变成/list主应用匹配不到qiankun 也会卸载子应用表现为点个菜单整个页面白了。所以子应用里所有路由跳转都建议用 name 或者带 base 的绝对路径别贪省事。keep-alive方面qiankun 默认在子应用切走时执行unmount整个 Vue 实例被销毁状态全丢。如果产品要求保留筛选条件、分页位置有两个思路一是主应用开singular: false并用loadMicroApp手动管理把容器用v-show藏起来而不是卸载二是在unmount时把关键状态序列化到sessionStoragemount时恢复。第一种体验好但内存占用高第二种实现简单但状态可能穿越比如用户期望的是重新进入时的初始筛选。我个人倾向第二种实现成本低坑也少。4. 通信、样式与公共依赖的治理应用拆开了接下来的问题都是边界问题数据怎么流样式怎么隔依赖怎么分。这三件事处理不好微前端带来的复杂度会超过它解决的问题。4.1 initGlobalState 的用法边界与替代方案qiankun 官方提供的通信方案是initGlobalState主应用调用它创建一个全局状态池子应用通过props.onGlobalStateChange订阅、props.setGlobalState修改。// 主应用 import { initGlobalState, MicroAppStateActions } from qiankun const actions: MicroAppStateActions initGlobalState({ token: , userInfo: null }) actions.onGlobalStateChange((state, prev) { console.log(主应用感知到变化, state, prev) })子应用在mount里接收mount(props) { props.onGlobalStateChange((state, prev) { // 同步 token 到子应用 }, true) props.setGlobalState({ token: xxx }) render(props.container) }这套方案够用但有两个明确的限制。第一它只支持可序列化的数据函数、组件实例传不过去。第二它是全局广播任何一个应用setGlobalState都会通知所有应用应用一多就容易出现我改了一个字段隔壁应用跟着重渲染的连锁反应。我的实际做法是分层全局状态只放三类东西——登录 token、用户基础信息、全局主题配置。这三类是真正需要跨应用共享的。业务数据一律走接口别想着用全局状态池当跨应用 store用那会变成一锅粥。如果确实需要传函数比如子应用要调用主应用的弹窗用props一次性传入在registerMicroApps的 props 里带上props: { basePath: /order, showMessage: (msg: string) { /* 主应用的全局提示 */ }, navigate: (path: string) router.push(path) }这种方式比全局状态池更直接因为 props 在 mount 时传入生命周期清晰不会出现子应用已经卸载了还在监听的问题。4.2 样式隔离的三种方案与真实取舍样式冲突是微前端最直观的痛点。qiankun 提供了三种级别方案配置隔离强度兼容性适用场景无隔离默认无最好子应用少且样式规范统一实验性隔离experimentalStyleIsolation: true中好大多数业务系统Shadow DOMstrictStyleIsolation: true强差展示类、无复杂组件库experimentalStyleIsolation的原理是运行时改写样式选择器给每条规则加上属性前缀。它的问题在于子应用里写在body、html、:root上的样式不会被隔离因为 qiankun 找不到对应的容器还有就是通过 JS 动态插入的style标签改写时机可能晚于样式生效出现短暂的样式闪烁。踩过几次之后我总结出几条硬规矩。第一子应用的全局样式必须收敛* { box-sizing: border-box }这种尽量不要写写成组件内 scoped 或者用 CSS 变量。第二用 CSS 预处理器加命名空间比如子应用所有类名统一加order-前缀这是最土但最有效的办法成本低到你无法拒绝。第三UI 库的样式要么全走 CDN 共享一份要么各应用自己隔离别一半一半那才叫灾难。资源共享方面更麻烦的其实是 CSS 变量。如果主应用定义了--primary-color子应用能不能用答案是可以CSS 变量是继承的只要能拿到根元素就能读到。但这个能用也是双刃剑——主应用改了变量值子应用样式跟着变有时候正是你想要的主题切换有时候是意外主应用换了主色调子应用按钮全变色。我的处理是给子应用的变量加前缀--order-primary跟主应用的--primary分开管理。4.3 公共依赖externals 还是各打各的这个问题每次讨论都能吵起来。子应用各自打包 Vue、各自打包组件库产物大但完全解耦主应用提供 externals产物小但版本强耦合。我的判断标准是看团队规模和发版频率。团队小于三个人、发版节奏同步那 externals 完全没问题配置也简单在主应用的 index.html 里挂上 CDN 的 Vue 和组件库子应用 vite 配置里rollupOptions.external排除掉就行。团队多、发版独立那就各打各的用浏览器缓存对抗体积。// 子应用 vite.config.tsexternals 方案 build: { rollupOptions: { external: [vue, vue-router, element-plus], output: { globals: { vue: Vue, vue-router: VueRouter, element-plus: ElementPlus } } } }externals 方案有个隐蔽的坑开发和生产的模块格式不一样。开发模式下 ESM 直接 import走的是 node_modules生产模式下走全局变量。如果本地开发时用的是 Vue 3.4线上 CDN 挂的是 3.2某些新 API 会在线上莫名报错。所以 external 的版本必须锁死主应用 CDN 地址上带版本号子应用package.json里的版本也锁死两边对齐。我个人现在的倾向是Vue 和 vue-router 用 externalsUI 组件库各打各的。原因是 Vue 本身 API 稳定版本冲突概率低UI 库版本迭代快不同团队需要的组件差异大强行统一反而拖慢迭代。这个结论不绝对看你团队实际情况。5. 常见问题排查实录这一章是我踩过的坑的实录基本都是网上搜不到准确答案、靠自己一行行看源码和网络请求排出来的。整理成速查表放在最后前面先讲几个典型场景的排查过程。5.1 白屏问题的分层定位法白屏是微前端最高频的问题但白屏只是个现象原因可能有十几种。我的排查顺序是固定的三层网络层 → 沙箱层 → 应用层。网络层看 Network 面板。先看子应用的 HTML 请求有没有发出去状态码是多少。如果是CORS error回到 3.1 检查 dev server 配置如果是 404检查entry地址末尾有没有斜杠——http://localhost:5174和http://localhost:5174/在部分服务器上行为不一样我遇到过少一个斜杠导致 HTML 拉不到的情况。再看静态资源请求。HTML 拉到了但 JS 404说明base配错了。开发模式 base 写成了生产路径或者生产模式忘了改都会出现HTML 能拿到JS 全是 404。这个特征非常明显Network 面板里一排红色的资源请求一眼就能确认。沙箱层看控制台报错。如果看到application order-app died in status LOADING_SOURCE_CODE说明脚本执行阶段就挂了。常见原因是子应用的入口脚本里有顶层import了某些浏览器不支持的模块或者window被沙箱代理后某些库不兼容。qiankun 的沙箱会用 Proxy 拦截全局对象个别老旧库直接访问window.top或者window.parent会抛异常。应用层看 Vue 的报错。如果子应用 mount 了但页面空白通常是挂载点找不到。检查props.container.querySelector(#app)有没有返回 null可能是子应用模板里根节点 id 不是app或者被主应用的同名 id 抢了。5.2 样式污染与路由错乱的现场处理样式污染的典型表现是进入 A 子应用B 子应用的按钮突然变形了。原因通常是某个子应用用了不带 scoped 的全局样式或者引入了会重置全局样式的 CSS 文件比如 normalize.css 之类。排查方法是在 DevTools 的 Elements 面板选中出问题的元素看 Styles 里哪条规则生效了规则来源的 URL 就是罪魁祸首。定位到之后要么给那个子应用加命名空间要么把那条全局样式改成 scoped。路由错乱的表现是点击子应用内的菜单地址栏变了但页面没动或者主应用的菜单高亮跟着乱跳。前者多半是子应用的路由 base 没配对createWebHistory(/order)传错或者漏传后者是主应用监听路由变化时没有排除子应用路径。主应用做菜单高亮的逻辑里一定要判断pathname.startsWith(activeRule)别用全等。还有一个比较隐蔽的问题子应用切走再切回来页面状态还在但数据不刷新。这通常是keep-alive或者手动loadMicroApp保留实例导致的业务上要区分切回来应该保留还是切回来应该重载。我的做法是在mount生命周期里根据props传进来的标记判断需要重载的就在 mount 里调一次列表接口。5.3 常见问题速查表现象高概率原因处理方式子应用 HTML 拉取失败dev server 未开 CORSserver.cors: true并加 headerJS/CSS 全部 404base配置与部署目录不符开发/生产子应用部署路径报错died in LOADING_SOURCE_CODE入口未用renderWithQiankun包裹检查 main.ts 生命周期写法生命周期函数找不到插件 name 与注册表 name 不一致两边字符串逐字比对独立访问子应用白屏未判断__POWERED_BY_QIANKUN__加独立挂载分支子应用弹窗被裁切开了 strictStyleIsolation改用 experimentalStyleIsolation切换应用后状态错乱unmount 未销毁实例和定时器置空 app、清理副作用路由跳转后整页白子应用路由 base 缺失createWebHistory(/order)主应用刷新 404history 模式无回退配置nginxtry_files兜底样式串到其他应用全局样式未收敛加类名前缀或 scoped提示遇到问题时先把qiankun的 debug 日志打开start({ sandbox: { ... } })之外加一个console.log在beforeLoad、beforeMount、afterMount里能快速判断卡在哪个阶段。这个做法看起来笨但比在几十个报错里猜要快得多。6. 构建部署与上线前的最后检查本地跑通只是第一步部署才是真正检验配置的地方。微前端的部署有个原则主应用和子应用必须同域或者显式开跨域不然生产环境的 fetch 一样会被拦。6.1 nginx 配置与路径规划假设主应用部署在根目录子应用分别部署在/order-app/和/user-app/下nginx 配置大概是这样server { listen 80; server_name example.internal; location / { root /data/www/main-app; try_files $uri $uri/ /index.html; } location /order-app/ { root /data/www; try_files $uri $uri/ /order-app/index.html; add_header Access-Control-Allow-Origin *; } location /user-app/ { root /data/www; try_files $uri $uri/ /user-app/index.html; add_header Access-Control-Allow-Origin *; } }这里root /data/www配location /order-app/的组合实际访问的物理路径是/data/www/order-app/index.html。路径映射出错的典型表现是 404 或者返回了主应用的 HTML因为兜底到了根目录的 index.html后者更迷惑人Network 面板显示 200 但页面就是不动一定要看清返回内容。add_header那两行是为了兼容子应用部署在独立域名或端口的场景。如果全部同域部署其实可以去掉但留着不影响。还有个细节子应用的index.html里的资源引用必须用相对路径或者带 base 的绝对路径。用base: /order-app/构建后Vite 会自动把资源路径写成/order-app/assets/xxx.js这个是对的。但如果有人手动在 HTML 里写/assets/xxx.js部署到子目录就 404。6.2 上线前的自查清单部署前我会过一遍这几项基本都是血泪教训换来的第一环境变量确认。主应用的VITE_SUB_BASE指向的是不是生产域名子应用的base是不是生产路径。这两个最容易在复制粘贴时忘了改。第二跨域确认。如果子应用独立域名检查响应头里有没有Access-Control-Allow-Origin。用 curl 直接看比浏览器调试快curl -I https://sub.example.internal/order-app/第三缓存策略确认。子应用的index.html绝对不能长期缓存否则主应用更新了注册表用户浏览器里还是旧的 HTML加载的还是旧的 JS 入口出现新功能不生效的诡异现象。index.html设置Cache-Control: no-cache带 hash 的静态资源可以设长缓存。第四降级方案确认。子应用加载失败时主应用得有兜底 UI。registerMicroApps的生命周期里捕捉异常在容器里渲染一个服务暂时不可用请刷新重试的提示。这个能力在真实生产环境里救过我好几次。第五独立访问确认。每个子应用单独打开 URL 必须能正常工作。这既是调试需要也是故障时的隔离手段——主应用挂了用户至少还能通过直连访问子应用的某些功能。// 加载失败兜底示例 registerMicroApps(microApps, { beforeLoad: [(app) { return Promise.resolve() }] }) // 配合 window.addEventListener(error) 或者 loadMicroApp 的 catch这套东西搭完一个基本的 Vue3 Vite qiankun 体系就跑起来了。我个人的体会是微前端的复杂度有七成集中在边界上——路径边界、样式边界、状态边界、部署边界。qiankun 帮你解决了运行时加载和沙箱这两块最硬的部分剩下的边界得靠工程规范去收。所以真正落地之前先跟团队把命名规范、公共依赖策略、路由划分规则定下来比急着写代码重要得多。我见过不止一个项目代码搭得飞快三个月后因为样式和依赖乱成一团又推倒重来那才是真的浪费。最后再分享一个小技巧本地开发时给每个子应用的 dev server 配一个固定的端口并且写进.env.local不要依赖 Vite 自动分配。端口一变主应用的 entry 就失效每次都要手动改烦不胜烦。固定下来之后整个开发链路就稳定了。