1. 微前端选型与整体架构的取舍1.1 为什么这个场景下我最终选了 qiankun先说清楚这套东西解决什么问题。一个后台系统跑了两三年几十个页面、上百个组件某个业务线突然要独立迭代团队也想拆开这时候最痛的不是技术而是改一行代码要全量回归。微前端就是在这种土壤里长出来的把一个大应用按业务域切成若干可独立开发、独立部署的子应用主应用只负责壳、路由分发、登录态和菜单子应用各管各的。能实现这个目标的方案有好几种我逐个说下实际踩下来的感受。iframe 最省事天然隔离但路由同步、弹窗遮罩、父子通信、页面高度自适应全得自己写用户体验上那层框的感觉很难消掉弹窗也永远出不了 iframe 的边界。模块联邦Module Federation更现代编译期共享依赖性能好但它本质是构建时集成子应用必须用兼容的构建器且主应用要能感知子应用的构建配置团队边界一模糊就容易互相拖累。qiankun 走的是运行时集成路线基于 single-spa 封装核心能力是 HTML Entry你只给一个子应用地址它自己去拉 HTML、解析 JS 和 CSS、执行、JS 沙箱、样式隔离、应用间通信。它对子应用的技术栈几乎没有要求Vue、React、甚至祖传的 jQuery 页面都能塞进来。选它的理由很朴素存量系统改造成本最低团队接入门槛最低出问题时排查链路最短。不过这里有个前提得说清楚qiankun 适合一个主应用 若干业务子应用的中后台形态不适合同一页面里要嵌十几个微组件的细粒度场景。如果你要做的是设计器里拖拽运行时组件那应该看的是 Web Components 或无界wujie那一类方案qiankun 的沙箱开销和路由模型会拖累你。1.2 Vue3 Vite 接入 qiankun 的核心矛盾这是整个项目的技术核心也是绝大多数人卡住的地方。qiankun 的加载流程是这样的请求子应用的index.html→ 用import-html-entry解析出 script 标签 → 以UMD/全局脚本的方式执行。也就是说它默认子应用打包出来的 JS 是挂到window上的普通脚本。而 Vite 在开发模式下是原生的 ESMindex.html里是一堆script typemodule模块通过import加载根本没有执行后挂载全局变量这一说。生产构建默认也是 ES Module 输出。两边天然对不上。这个矛盾的具体表现分两层开发态qiankun 拿到 Vite 的 index.html 后试着执行 typemodule 的脚本浏览器环境加上 qiankun 的执行方式基本就是白屏控制台甩一个Failed to fetch dynamically imported module或者干脆什么都不报。生产态ESM 产物被当作全局脚本执行import语法直接语法错误。解决路径有两条。一条是放弃 Vite 开发服务器用 Vite 构建出 UMD 产物再调试但这样就失去了热更新开发体验断崖式下跌。另一条是引入vite-plugin-qiankun它在开发态注入一个伪 UMD包装在生产态把 rollup 的output.format强制改成umd同时提供renderWithQiankun和qiankunWindow两个辅助 API。我选的是第二条因为热更新对日常迭代太重要了。注意vite-plugin-qiankun这类社区插件维护节奏不快版本要锁定别用 latest。我一般会在package.json里写死版本号并提交 lock 文件避免某次重装依赖后整个项目白屏。1.3 主应用与子应用的职责边界怎么划边界划不清后面全是扯皮。我一般按下面这张表来定写进团队的接入文档里谁越界谁改关注点主应用基座子应用登录态 / token统一持有注入子应用只读不自行刷新用户信息与权限拉取并下发消费做按钮级控制路由前缀分配并注册 activeRule只负责前缀内的路由菜单与导航渲染、跳转不渲染全局菜单公共组件库只放壳层组件自己带版本不强求统一全局样式reset、主题变量只写自己的 scoped 样式部署路径决定/xxx/前缀严格对齐 base这里我踩过一个很典型的坑一开始想着公共组件库统一由主应用提供子应用不打进包里用 externals 把 Vue、Element Plus 抽出去。结果 vite-plugin-qiankun 在生产构建时对 externals 的处理很容易和 UMD 包装打架而且子应用独立运行时不走主应用就直接崩了。后来改成shared的思路——不强求去重每个子应用自带自己依赖的版本用浏览器的缓存兜住重复下载的成本。多下几百 KB 换来的是彻底解耦和独立运行能力这笔账在中后台场景里是划算的。真正需要共享的东西只有三类登录态、路由前缀、全局的主题变量。这三样用 qiankun 的 props 和 globalState 传递就够了其他的别管太宽。2. 主应用搭建从零到能加载第一个子应用2.1 工程初始化与依赖版本锁定主应用我用 Vue3 Vite vue-router 4 搭整套依赖不算多但版本要卡死。下面是我实际在用的组合npm create vitelatest main-portal -- --template vue-ts cd main-portal npm i vue-router qiankun npm i -D types/node装完之后package.json里应该能看到qiankun^2.10.16、vue-router^4.3.x、vite^5.x这三个关键项。我想强调一点qiankun 2.x 是当前主流稳定线别去追什么 3.x 的 beta坑多且社区资料少出了问题搜都搜不到。然后处理一下 Vite 对 qiankun 的兼容。qiankun 内部依赖了一些 CJS 的老包比如import-html-entry在 Vite 的 ESM 环境里偶尔会报module is not defined。稳妥的做法是在vite.config.ts里把它预构建进来// vite.config.ts import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], optimizeDeps: { include: [qiankun] }, server: { port: 5173, strictPort: true } })strictPort: true这个配置我强烈建议加上。微前端场景下主应用地址是写死在子应用开发配置里的端口一旦漂移就会静默失败页面能打开但子应用死活加载不出来排查起来极其浪费时间。还有一点主应用的路由必须是history模式不能用 hash。原因是 qiankun 的activeRule是基于location.pathname匹配的hash 模式下 pathname 永远不变子应用永远激活不了。// src/router/index.ts import { createRouter, createWebHistory } from vue-router const router createRouter({ history: createWebHistory(/), routes: [ { path: /, redirect: /home }, { path: /home, component: () import(/views/Home.vue) }, { path: /sub-vue3-vite/:pathMatch(.*)*, component: () import(/layouts/MicroLayout.vue) }, { path: /:pathMatch(.*)*, redirect: /home } ] }) export default router注意最后那条兜底路由。如果不加当子应用内部跳转到/sub-vue3-vite/detail/1后刷新页面主应用的 vue-router 匹配不到任何路由会直接白屏——因为/sub-vue3-vite这条路由在 vue-router 里其实并没有真实匹配上具体子路径。加上:pathMatch(.*)*才能让主应用接住所有子路径再由 qiankun 去激活子应用。2.2 容器页与注册逻辑的完整写法容器页很简单但细节决定成败。它是子应用挂载的宿主节点!-- src/layouts/MicroLayout.vue -- template div classmicro-layout div idsub-container classsub-container/div /div /template script setup langts onMounted(() { console.log([portal] container mounted, id sub-container) }) /script style scoped .sub-container { min-height: calc(100vh - 60px); } /style关键点在于idsub-container这个节点必须在子应用激活时已经存在于 DOM 中。qiankun 是activeRule匹配成功后去container选择器找节点的如果那一刻节点还没渲染出来比如你在onMounted里才动态创建就会报Target container is not a DOM element。所以容器节点要写在模板里不能动态生成。注册逻辑放在主入口// src/main.ts import { createApp } from vue import { registerMicroApps, start, initGlobalState, runAfterFirstMounted } from qiankun import App from ./App.vue import router from ./router const app createApp(App) app.use(router) app.mount(#app) // 全局状态登录态和用户信息 const actions initGlobalState({ token: localStorage.getItem(token) || , user: null as null | { name: string; roles: string[] } }) actions.onGlobalStateChange((state, prev) { console.log([portal] global state changed, prev, -, state) }, true) const isDev import.meta.env.DEV registerMicroApps( [ { name: sub-vue3-vite, entry: isDev ? //localhost:5174 : /sub-vue3-vite/, container: #sub-container, activeRule: /sub-vue3-vite, props: { base: /sub-vue3-vite, appName: sub-vue3-vite } } ], { beforeLoad: [(app) { console.log([qiankun] beforeLoad ${app.name}); return Promise.resolve() }], beforeMount: [(app) { console.log([qiankun] beforeMount ${app.name}); return Promise.resolve() }], afterMount: [(app) { console.log([qiankun] afterMount ${app.name}); return Promise.resolve() }], afterUnmount: [(app) { console.log([qiankun] afterUnmount ${app.name}); return Promise.resolve() }] } ) start({ prefetch: all, sandbox: { experimentalStyleIsolation: true }, singular: false }) runAfterFirstMounted(() { console.log([portal] first micro app mounted) })这段代码里几个参数值得逐个拆开说。entry在开发态指向子应用的 dev server 绝对地址必须以//开头因为 qiankun 内部会拼接协议生产态指向同域的相对路径/sub-vue3-vite/这样能避开跨域并且复用主应用的 HTTP 缓存。activeRule我写的是字符串/sub-vue3-viteqiankun 内部会转成前缀匹配。也就是说/sub-vue3-vite/order/list也会命中。不需要写正则字符串更可读。prefetch: all会在第一个子应用挂载完成后空闲时预取其余子应用的静态资源。这个策略很实用用户点菜单的时候子应用基本是秒开。但如果你的子应用有十几个全部预取会吃满带宽、拖慢首屏那就改成prefetch: all改为策略函数只预取菜单里高频的那两三个。singular: false允许同一时间挂载多个子应用。默认是true切路由时会卸载上一个。做弹窗、抽屉嵌子应用的场景必须开false否则会出现打开弹窗主页面被卸载的诡异现象。代价是内存占用上升要配合手动unmount做好回收。2.3 主应用下发数据与全局状态的双通道主应用给子应用传数据有两条路很多人只用了 props结果遇到主应用 token 刷新了但子应用还在用旧的这类问题就懵了。props 通道适合传不变的配置路由 base、应用名、接口域名前缀、埋点上报函数。它在registerMicroApps时确定子应用在mount生命周期能拿到。globalState 通道适合传会变的数据登录用户、主题色、语言、全局权限列表。主应用调用setGlobalState子应用通过onGlobalStateChange监听。// 主应用token 刷新后广播 actions.setGlobalState({ token: newToken, user: { name: A同学, roles: [admin] } })子应用侧// 子应用在 mount 里注册监听 mount: async (props) { props.onGlobalStateChange((state, prev) { if (state.token ! prev.token) { // 更新 axios 拦截器里的 token request.defaults.headers.common[Authorization] state.token } }, true) render(props) }这里有个必须提醒的细节onGlobalStateChange的第二个参数immediate一定要传true。不传的话子应用挂载时拿不到当前状态只能等下一次变更导致首屏请求缺 token、接口 401。这个问题我见过至少三次每次都是排查半天才发现是参数漏了。另外子应用卸载时 qiankun 会自动清理它注册的onGlobalStateChange回调吗不会。严格来说 qiankun 会在unmount后清掉内部的回调引用但如果子应用自己在其他地方还持有 actions 的引用就会造成内存泄漏。稳妥的做法是在unmount里手动保存offGlobalStateChange返回值并调用。qiankun 给了props.offGlobalStateChange卸载时调一下干净。2.4 手动挂载模式弹窗、抽屉里的子应用registerMicroApps是路由驱动的托管模式适合整页切换。但真实业务里经常遇到在详情页弹一个抽屉里面跑另一个子应用的某个页面这种需求。这时候托管模式就不好使了——地址栏没变activeRule永远不匹配。qiankun 提供了loadMicroApp手动模式来解决import { loadMicroApp, MicroApp } from qiankun let micro: MicroApp | null null export async function openMicroDrawer() { micro loadMicroApp({ name: sub-vue3-vite, entry: /sub-vue3-vite/, container: #micro-drawer-container, props: { base: /sub-vue3-vite, mode: drawer } }, { sandbox: { experimentalStyleIsolation: true }, singular: false }) await micro.mountPromise console.log([portal] drawer micro app mounted) } export async function closeMicroDrawer() { if (!micro) return await micro.unmount() micro null }手动模式有几条纪律。第一同一个子应用不要重复loadMicroApp实例要缓存关闭时unmount后再置空否则会挂出多份 DOM 和多个沙箱。第二容器节点要在调用loadMicroApp之前就渲染到 DOM 上和托管模式一样。第三entry在生产环境用相对路径但如果你配了 CDN 域名要注意 CORS。手动模式下子应用的路由要单独处理。因为地址栏没变子应用的createWebHistory拿到的 pathname 是主应用的地址路由会错乱。这时候要么子应用改用mode: memory的内存路由要么在props里透传一个初始路径。我在实际项目里更倾向于给抽屉场景单独做一个轻量页面组件不硬塞整个子应用路由代价可控。3. 子应用改造Vite 项目接入的完整步骤3.1 插件接入与生命周期导出的标准模板子应用这段是整个搭建里最容易出问题的部分。先把依赖装上npm i vite-plugin-qiankun然后改vite.config.ts这是最关键的一步// vite.config.ts import { defineConfig, loadEnv } from vite import vue from vitejs/plugin-vue import qiankun from vite-plugin-qiankun export default defineConfig(({ mode }) { const env loadEnv(mode, process.cwd()) const isProd mode production return { // 生产环境才设置 base开发态必须留空或 / base: isProd ? /sub-vue3-vite/ : /, plugins: [ vue(), qiankun(sub-vue3-vite, { useDevMode: true }) ], server: { port: 5174, strictPort: true, cors: true, headers: { Access-Control-Allow-Origin: * } }, build: { target: es2015, rollupOptions: { output: { format: umd, name: subVue3Vite } } } } })这里面base的条件判断是我踩过最深的坑没有之一。开发态如果把base设成/sub-vue3-vite/子应用独立访问http://localhost:5174时所有资源都 404因为 Vite dev server 是从根路径服务的。但生产环境又必须设成/sub-vue3-vite/否则部署到子目录后 assets 全挂。所以一定要用mode来做分支别图省事写死一个值。headers: { Access-Control-Allow-Origin: * }是给主应用 fetch 子应用 HTML 用的。开发态主应用在 5173子应用在 5174是跨域的。useDevMode: true告诉插件这个子应用在开发态被主应用加载插件会注入一段兼容代码把 ESM 包成类似 UMD 的执行方式。生产环境构建时它自动走另一套逻辑不用你手动切换。接着是入口文件的改造这个模板基本可以直接抄// src/main.ts import { createApp, App as VueApp } from vue import { renderWithQiankun, qiankunWindow } from vite-plugin-qiankun/dist/helper import App from ./App.vue import { createAppRouter } from ./router import { setupStore } from ./store let app: VueApp | null null let router: ReturnTypetypeof createAppRouter | null null interface QiankunProps { container?: HTMLElement base?: string mode?: string token?: string onGlobalStateChange?: (cb: (state: any, prev: any) void, immediate?: boolean) void setGlobalState?: (state: Recordstring, any) void offGlobalStateChange?: () void [key: string]: any } function render(props: QiankunProps {}) { const { container, base /sub-vue3-vite } props const mountEl container ? container.querySelector(#app) : document.querySelector(#app) if (!mountEl) { console.error([sub] mount element #app not found) return } router createAppRouter(base) app createApp(App) setupStore(app) app.use(router) // 全局状态同步 props.onGlobalStateChange?.((state) { if (state.token) { localStorage.setItem(token, state.token) } }, true) app.mount(mountEl as Element) } renderWithQiankun({ bootstrap() { console.log([sub] bootstrap) }, mount(props) { console.log([sub] mount, props) render(props as QiankunProps) }, unmount(props) { console.log([sub] unmount) props.offGlobalStateChange?.() app?.unmount() app null router null }, update(props) { console.log([sub] update, props) } }) // 独立运行时的兜底 if (!qiankunWindow.__POWERED_BY_QIANKUN__) { render() }renderWithQiankun这个函数做的事情是把上面这个对象挂到window上的约定字段里让 qiankun 能识别到子应用的生命周期。而最后那段if (!qiankunWindow.__POWERED_BY_QIANKUN__)是保命的有了它子应用可以独立启动、独立调试、独立跑单测出问题时能快速判断是子应用自己坏了还是集成坏了。这一条我要求所有子应用必须写。unmount里必须做的事卸载 Vue 实例、清理全局状态监听、清掉定时器和事件监听、取消未完成的请求。少一样切回来的时候就会出现重复的弹窗两个轮询在跑接口返回了旧数据这类灵异问题。3.2 路由 base 的动态注入与 404 处理子应用路由的 base 不能写死因为它有两种运行态独立运行base 是/和被集成base 是/sub-vue3-vite。所以要在创建路由时动态传参// src/router/index.ts import { createRouter, createWebHistory, RouteRecordRaw } from vue-router import { qiankunWindow } from vite-plugin-qiankun/dist/helper const routes: RouteRecordRaw[] [ { path: /, redirect: /order/list }, { path: /order/list, component: () import(/views/OrderList.vue) }, { path: /order/detail/:id, component: () import(/views/OrderDetail.vue) }, { path: /:pathMatch(.*)*, component: () import(/views/NotFound.vue) } ] export function createAppRouter(base /) { return createRouter({ history: createWebHistory(base), routes }) }主应用传过来的base就是/sub-vue3-vite。另一个高频坑是子应用内部跳转方式。在集成态下你用router.push({ name: detail, params: { id } })是安全的vue-router 会自动拼上正确的完整路径。但如果你写window.location.href /order/detail/1那就彻底丢了/sub-vue3-vite前缀浏览器会跳到主应用的根路径下直接 404。这条规则要写进子应用的开发规范里用 lint 规则禁掉location.href的裸用法。还有一种情况子应用里的a href/xxx标签。qiankun 在沙箱里拦截了history.pushState但拦截不了原生链接的默认跳转。所以子应用内部的一切导航要么走 router要么用router-link别手写 a 标签。3.3 独立运行与集成运行的双模式调试我个人的调试流程是这样的先独立跑通再接入主应用。子应用npm run dev起来后先确保自己的页面、路由、接口全正常这一步能排除掉 80% 的其实是子应用自己的 bug。然后再起主应用访问/sub-vue3-vite看控制台的 qiankun 日志。我一般会在主应用加一层日志兜底start({ prefetch: all, sandbox: { experimentalStyleIsolation: true }, singular: false }) // 捕获子应用资源加载失败 window.addEventListener(error, (e) { if (e.target (e.target as HTMLElement).tagName SCRIPT) { console.error([qiankun] script load failed:, (e.target as HTMLScriptElement).src) } }, true)这个错误监听特别有用因为 qiankun 加载子应用资源失败时很多情况下不会在控制台抛红色错误只会静默白屏。加上这段就能看到具体是哪个 script 挂了。开发态还有一个常见现象改了子应用的代码热更新生效了但样式没变。这通常是experimentalStyleIsolation把新注入的样式加了前缀但旧的style标签没被移除导致的。解决办法是在子应用的unmount里清理或者干脆开发态关掉样式隔离把sandbox配置按import.meta.env.DEV分支处理生产再开。我在项目里就是这么干的开发追求效率生产追求隔离。3.4 生产构建与部署路径对齐打包这块vite-plugin-qiankun会把format设成umd但你需要确认两件事资源路径是不是都是绝对路径且带了正确的 base以及CSS 是否被正确抽离并内联进 HTML。构建完打开dist/index.html看一眼引用的script和link路径应该是/sub-vue3-vite/assets/xxx.js这种形式。如果是./assets/xxx.js相对路径集成后就会 404因为 qiankun 是以主应用的 URL 为基准去解析资源的。如果你的构建产物里 script 还是typemodule说明vite-plugin-qiankun没生效检查它是不是放在了plugins数组里、useDevMode是否正确、构建时mode是不是production。部署目录结构推荐是这样/usr/share/nginx/html/ ├── index.html # 主应用 ├── assets/ └── sub-vue3-vite/ # 子应用注意目录名与 base 一致 ├── index.html └── assets/nginx 那边主应用和子应用都要配 history 回退server { listen 80; server_name portal.example.internal; root /usr/share/nginx/html; location / { try_files $uri $uri/ /index.html; } location /sub-vue3-vite/ { try_files $uri $uri/ /sub-vue3-vite/index.html; } # 静态资源加长缓存 location ~* \.(js|css|woff2?|png|svg)$ { expires 30d; add_header Cache-Control public, immutable; } }第二条try_files是子应用能被独立访问的关键。很多人只配了主应用的结果子应用独立刷新 404还得回头查半天。还有缓存的问题子应用更新后用户访问的还是旧版本。原因是 qiankun 会缓存子应用的 HTML 内容且文件名如果没带 hash 就会被浏览器缓存。解决办法是 Vite 默认给 assets 文件名加了 hash只要保证index.html不被强缓存就行上面配置里没有给 html 加长缓存。如果还遇到旧版本残留可以在registerMicroApps的entry后面拼一个版本号 query做缓存击穿。4. 沙箱、样式隔离与全局资源治理4.1 qiankun 的隔离机制到底做了什么qiankun 的沙箱有两个维度JS 沙箱和样式隔离。JS 沙箱在start()时通过sandbox配置。默认sandbox: true会启用ProxySandbox核心思路是给每个子应用创建一个独立的window代理对象子应用里访问window.xxx实际落在代理上卸载时这个代理整个丢弃全局变量污染就被隔离了。多个子应用同时存在时每个都有自己的代理互不干扰——这也是singular: false能工作的基础。但 ProxySandbox 有个明确的局限它拦截不了通过var声明的顶层变量、也拦不住document上的某些操作。比如子应用里写document.title xxx主应用的标题就被改了又比如子应用给document.body加了个 class卸载后这个 class 还在。样式隔离的配置项有三个值实际行为差别很大配置机制优点副作用sandbox: true仅 JS 沙箱开销小样式完全不隔离极易冲突experimentalStyleIsolation: true运行时给样式加div[data-qiankunxxx]前缀改动小兼容性好挂到 body 的弹窗样式失效keyframes处理不完美strictStyleIsolation: true子应用挂进 Shadow DOM隔离最彻底第三方组件库大量报错主题变量穿透困难我现在的默认选择是experimentalStyleIsolation: true。它不是银弹但副作用最小团队改动量最低。strictStyleIsolation我试过一轮最后放弃了。原因是 Shadow DOM 内部拿不到外部的 CSS 变量主题切换要手动往 shadow root 里注入 style而且很多 UI 库尤其是基于document.body定位的弹层直接在 shadow 边界外渲染隔离等于白做。4.2 弹窗被样式隔离打飞的解决思路experimentalStyleIsolation最常见的副作用就是子应用里的 MessageBox、Select 下拉、DatePicker 面板样式全丢出来一堆裸 DOM。原因很简单。这些组件默认把 DOM 挂到document.body下而 qiankun 给样式加的前缀是div[data-qiankunsub-vue3-vite]body 下挂的节点不在这个选择器范围内样式自然不生效。解决方式有三条按推荐度排序第一条改挂载点。让子应用内所有弹层挂到子应用自己的根容器里而不是 body。以 Element Plus 为例很多组件支持append-to属性但一个个改太累。更彻底的做法是在子应用的根节点上配置teleport的目标容器!-- 子应用 App.vue -- template el-config-provider div idsub-root router-view / /div /el-config-provider /template然后弹层组件传:append-to#sub-root。这依然要改代码但至少是可控的。全局配置里可以用ElConfigProvider配合zIndex和teleported属性做统一约束。第二条接受一定程度的隔离失效。把样式隔离关掉改用命名空间约定。所有子应用的根类名加唯一前缀.sub-vue3-vite-CSS 用 scoped 或 BEM全局样式一律禁止。这个方案依赖团队纪律但工程上最省事。中小团队我更推荐这条。第三条用 CSSlayer做层级声明。这个比较新主应用和子应用各自把样式包进layer app-a/layer app-b谁优先级高由主应用统一声明。缺点是老浏览器不支持且需要改造存量 CSS。我的实践是第二条为主、第一条为辅全局样式层面靠命名约定个别确实需要跨容器定位的组件才用append-to精确控制。4.3 全局副作用清单与清理规范除了样式子应用还会在别的地方留下痕迹。我把这些整理成了一份卸载前必查清单接进项目的 code review 流程定时器setInterval、setTimeout全部用变量存 idunmount时统一clear。轮询类请求尤其容易忘。事件监听window.addEventListener、document.addEventListener、resize、scroll、visibilitychange全部用AbortController或配对移除。WebSocket / SSE卸载时必须主动close否则服务端会一直保留连接积累多了就是资源泄漏。第三方 SDK地图、图表库、编辑器实例必须destroy()。ECharts 不dispose会持续占用内存几次切换下来页签就卡了。全局 CSS 变量document.documentElement.style.setProperty设的变量卸载时removeProperty。localStorage / sessionStorage命名加应用前缀避免多个子应用互相覆盖。这份清单看起来琐碎但每一条我都见过对应的线上问题。最典型的是地图组件没销毁用户在两个菜单之间切了二十次页面直接卡死。这类问题在开发阶段不显眼上了生产被用户投诉才发现。5. 常见问题速查与排障实录5.1 高频报错对照表下面这张表是我这两年攒下来的按出现频率排序遇到问题先在这里面找现象大概率原因处理动作子应用完全白屏接口正常entry 地址跨域或返回 404浏览器直接访问 entry 地址看能否拿到 HTML控制台Target container is not a DOM element容器节点未渲染或 id 不对确认registerMicroApps时容器已存在Failed to fetch dynamically imported module开发态未启用useDevMode检查插件配置和mode参数生产环境Unexpected token export产物仍是 ESM强制rollupOptions.output.format umd样式全乱、弹窗裸奔样式隔离前缀未覆盖 body 节点改挂载点或用命名空间方案切回子应用后重复渲染两份容器内旧 DOM 未清空unmount后手动清空容器 innerHTML刷新子应用路由后 404nginx 缺子应用回退规则补子目录的try_files子应用资源 404路径少前缀base未设或构建时 mode 不对检查构建产物中 script 路径第二次进入子应用白屏mount时container.querySelector拿不到节点用container.firstElementChild或检查 DOM 结构主应用 token 更新子应用仍用旧值immediate未设或没监听onGlobalStateChange(cb, true)其中有两条我想多说两句。第二次进入白屏这个问题根源在于子应用的index.html里的挂载点是div idapp/div但 qiankun 二次挂载时可能把节点替换掉了。稳妥的写法是在mount里做兜底先container.appendChild创建挂载点再mount。const rootEl container.querySelector(#app) || (() { const el document.createElement(div) el.id app container.appendChild(el) return el })()重复渲染两份这个更隐蔽现象是页面元素看起来叠了一层阴影交互也怪。原因是上次unmount时 Vue 实例卸载了但 qiankun 注入的 HTML 容器还有残留。解决是在unmount前手动清理unmount(props) { app?.unmount() app null const container props.container if (container) { container.innerHTML } }5.2 通信与刷新丢状态的实战处理第一个场景主应用登录态刷新。用户在主应用点了刷新 token子应用还拿着旧的接口开始 401。这时候靠globalState广播是有效的但要注意子应用里的 axios 实例不能缓存 token 字符串必须每次请求时从全局状态或 localStorage 现取。// 子应用 request.ts request.interceptors.request.use((config) { const token localStorage.getItem(token) if (token) { config.headers![Authorization] Bearer ${token} } return config })把 token 存在 localStorage 里、每次现取是最不容易出错的做法。用内存变量缓存反而容易踩两个实例各存一份的坑。第二个场景子应用内跳转子应用。如果两个子应用之间有跳转比如从订单列表点进商品详情分属不同子应用最忌讳的是各子应用自己去history.pushState。正确做法是统一走主应用暴露的导航方法// 主应用 props 里透传 props: { navigate: (path: string) router.push(path) }子应用调用props.navigate(/product/detail/1)主应用路由变化后 qiankun 自动切应用。这样跳转逻辑只有一处路由守卫、埋点、权限校验都能统一收口。第三个场景页面刷新后的状态恢复。微前端 history 路由刷新时地址栏是/sub-vue3-vite/order/detail/1主应用加载后先匹配到MicroLayout然后 qiankun 激活子应用子应用再用createWebHistory(/sub-vue3-vite)解析出/order/detail/1。这条链路能跑通的前提是主应用的路由兜底写对了也就是前面说的:pathMatch(.*)*。如果刷新后子应用进了 404 页先看地址栏的完整路径对不对再看主应用路由表里有没有兜底最后看子应用的 base 是不是带上了前缀。三步能定位 95% 的问题。5.3 上线前的自检清单集成调试通不等于能上线。每次发版前我会跑一遍下面这些检查项[ ] 主应用和所有子应用的base是否与新版本的部署目录完全一致[ ] 子应用独立访问能否正常打开首页和深层路由不走主应用[ ] 主应用访问每个子应用入口控制台无红色报错、无 404 资源[ ] 在子应用深层路由上按 F5 刷新页面是否正常恢复[ ] 主子应用来回切换 10 次内存无持续增长DevTools Memory 快照对比[ ] 子应用内的弹窗、下拉、日期面板样式是否正常[ ] 登录态过期后跳登录页回来是否还能进入原子应用页面[ ] 手机端 / 小屏下子应用布局是否正常微前端容器宽度变化容易踩坑[ ] 灰度环境的主应用是否能加载灰度子应用entry 域名要按环境切换最后一条特别容易漏。很多项目entry写死成生产域名结果灰度环境的主应用去加载了生产子应用数据和代码版本全错位。正确的做法是把 entry 也做成环境变量const SUB_ENTRY import.meta.env.VITE_SUB_ENTRY || /sub-vue3-vite/不同环境不同.env文件构建时注入。这样切环境不会出错。我个人在这套组合上折腾下来最大的体会是qiankun 本身不难难的是边界管理。主子应用之间哪些数据能共享、哪些样式能全局、哪些依赖能抽离这些约定一旦模糊三个月后就是一团乱麻。所以真正值得投入时间的是那份接入规范和自检清单而不是把配置调通就完事。规范立住了后面接第五个、第十个子应用都是复制粘贴的事规范没立住第三个子应用就会开始让你怀疑人生。
阅读完成 · 觉得有帮助?