本文基于 qiankun 2.10.16 Vite 8.0.12撰写时vite-plugin-qiankun不兼容 Vite 8。一、背景为什么同时搭了两套微前端我在做一个微前端实验项目目标是同一个子应用既能跑在 qiankun 下也能跑在 wujie 下并且子应用本身不依赖任何微前端框架。exp1-react ──┬── qiankun-main (registerMicroApps proxy sandbox) exp2-vue ──┤ └── wujie-main (startApp Web Component sandbox)这个双胞胎实验的初衷是在真实项目里对比两个框架的行为差异为团队选型提供依据。但 Vite 8 第一个跳出来告诉我这事没那么简单。二、架构差异一览在深入兼容性问题之前先看两组 API 的本质区别维度qiankunwujie注册子应用registerMicroApps([{name, entry, container, activeRule}])startApp({name, url, el})启动start({sandbox, fetch})无需显式启动配置字段4 个路由匹配 容器选择2 个名称 URL容器来源配置中的container选择器静态指定调用时el参数动态传入 DOM 引用沙箱JS Proxy 沙箱本项目使用sandbox: falseWeb Componentiframe 沙箱路由框架内部匹配activeRule由 React Router 在组件层控制qiankun 的container是一个 CSS 选择器字符串如#qiankun-container框架自己去找那个 DOM 节点然后挂载。这让容器页面组件几乎什么都不用做——我们项目里QiankunContainer.tsx直接return null。wujie 反过来你在调用startApp时传入一个el真实的 DOM 引用框架只负责往那个节点里渲染。这让容器可以完全由 React 组件生命周期控制不需要框架去找节点。// qiankun: 容器定义在配置里registerMicroApps([{name:exp1,entry://localhost:7101,container:#qiankun-container,activeRule:/exp1}])// wujie: 容器在调用时传入startApp({name:exp1,url://localhost:7101,el:document.getElementById(container)!})这两个 API 设计的差异是后面一切兼容性问题的根因。三、核心问题qiankun 为什么不兼容 Vite 8qiankun 期望子应用导出一个同步可用的生命周期对象// qiankun 的预期子应用入口导出固定结构exportasyncfunctionbootstrap(){...}exportasyncfunctionmount(props){...}exportasyncfunctionunmount(props){...}但 Vite 8 在开发模式下输出 ESM模块是异步加载的!-- Vite dev server 返回的 HTML --scripttypemodulesrc/src/main.tsx/script这意味着子应用的代码在import()完成之前是不可用的qiankun 拿不到它期望的生命周期函数。时序上出现了断裂qiankun 侧 fetch HTML → 解析 script → 期望立即拿到 mount/unmount 子应用侧 浏览器异步加载 ESM → 模块初始化 → 此时 qiankun 已经等不了了 ↑ 时间差社区方案vite-plugin-qiankun在 Vite 8 下不可用官方文档还在等适配。摆在面前两条路降级 Vite或者自己造桥。四、解法Promise Bridge 机制核心思路用 Promise 作为同步原语在主应用和子应用之间建立一座桥。4.1 主应用侧注入桥接脚本在 qiankun 的start()中我们拦截所有fetch请求。当检测到返回的 HTML 包含typemodule时说明来自 Vite dev server在 HTML 注入一段桥接脚本// packages/qiankun-main/src/main.tsxfunctioncreateLifecycleBridge(appName:string){returnwindow.__bridge window.__bridge || {} var b {} b.mountP new Promise(function(r) { b.mountR r }) b.unmountP new Promise(function(r) { b.unmountR r }) window.__bridge[${appName}] b window[${appName}] { bootstrap: function() { return Promise.resolve() }, mount: function(props) { return b.mountP.then(function(fn) { return fn(props) }) }, unmount: function(props) { return b.unmountP.then(function(fn) { return fn(props) }) }, update: function(props) { return Promise.resolve() }, }}这段脚本做了什么1. 创建两个 PromisemountP 和 unmountP 2. 把它们的 resolve 函数分别存为 mountR 和 unmountR挂在 window.__bridge[appName] 上 3. 在 window 上挂一个同名对象如 window.exp1导出 qiankun 期望的生命周期函数 4. 这些生命周期函数返回 mountP / unmountP —— 此时它们处于 pending 状态 → qiankun 调用 mount() 时拿到的是一个 Promise会等待它 resolve 5. 等到子应用加载完成调用 bridge.mountR(render) → mountP resolve → qiankun 的 mount() 随之拿到真正的 render 函数 → 生命周期接续时序图时间线 → 主应用 (fetch 拦截器): │ ├─ fetch 子应用 HTML ├─ 检测到 ESM ├─ 注入桥接脚本到 HTML ├─ 返回给浏览器 │ ├─ qiankun 解析 window.exp1 ├─ 调用 window.exp1.mount() ────────────┐ │ │ (mountP 处于 pending等待 resolve) │ │ 子应用 (异步加载): │ ├─ import(/src/main.tsx) 完成 │ ├─ 检测到 __POWERED_BY_QIANKUN__ │ ├─ 拿到 bridge.mountR │ ├─ bridge.mountR(props render(props)) ─┘ │ │ │ ▼ │ mountP resolve → mount() 拿到 render │ └─ 子应用正常渲染关键qiankun 调用mount()时不会报错它只是拿到了一个 Promise。等到子应用准备好Promise 兑现一切接续。4.2 子应用侧对接桥接脚本子应用只需要在入口文件中检测环境并挂载// packages/exp1-react/src/main.tsxif((windowasany).__POWERED_BY_QIANKUN__){// qiankun 模式通过 Promise Bridge 接入constbridge(windowasany).__bridge?.exp1if(bridge){bridge.mountR((props:any)render(props))bridge.unmountR((){root?.unmount();rootnull})}}elseif((windowasany).$wujie){// wujie 模式框架自己处理生命周期无需额外代码}else{// 独立运行模式render({})}子应用的package.json里没有任何 qiankun 或 wujie 依赖。五、配套机制Vite HTML 拦截器注入桥接脚本只是第一步。为了让它正常工作还需要处理 Vite dev server 产出的 HTMLfunctiontransformViteHtml(html:string,appName:string,baseUrl:string){// 1. 删除 react-refresh 内联脚本避免热更新脚本干扰 qiankunhtmlhtml.replace(/script\stypemodule[\s\S]*?react-refresh[\s\S]*?\/script/g,,)// 2. script typemodule src... → import(...)// qiankun 无法处理 ESM 模块标签转为动态 import()htmlhtml.replace(/script\stypemodule\ssrc([^])\/script/g,(_,src){constabsoluteSrcsrc.startsWith(http)?src:baseUrlsrcreturnscriptimport(${absoluteSrc})/script},)// 3. 在 /body 前注入桥接脚本htmlhtml.replace(/body,script${createLifecycleBridge(appName)}/script/body)returnhtml}这三步分别解决三个问题React Refresh热更新脚本在 qiankun 的 fetch 拦截器中被二次执行会导致报错直接删除反正开发时刷新页面就行ESM → import()qiankun 内部的模块加载器不认识script typemodule转为import()后浏览器可以正常执行桥接注入在子应用代码执行之前桥接对象已经在 window 上就位完整的 fetch 拦截器start({sandbox:false,fetch(url:string,...args:unknown[]){returnwindow.fetch(url,...args).then(async(res:Response){consthtmlawaitres.clone().text()if(html.includes(typemodule)){constappNameextractAppName(url)||unknownconstbaseUrlnewURL(url).originconsttransformedtransformViteHtml(html,appName,baseUrl)returnnewResponse(transformed,{headers:res.headers})}returnres})},})六、子应用的无依赖适配模式这套方案还有一个有趣的副产品子应用可以完全不依赖任何微前端框架却同时适配 qiankun 和 wujie。通信层面子应用在运行时检测所处的环境走对应的通信通道// exp1-react/src/pages/Counter.tsxconstsendReply(){if((windowasany).__POWERED_BY_QIANKUN__){// qiankun: 通过全局状态通信constapp(windowasany).__QIANKUN_APP__ app?.props?.setGlobalState?.({reply:exp1 reply: count ${count}})}elseif((windowasany).$wujie){// wujie: 通过事件总线通信;(windowasany).$wujie?.bus?.$emit?.(exp1-reply,exp1 reply: count ${count})}}// 接收消息同理useEffect((){if((windowasany).__POWERED_BY_QIANKUN__){app?.props?.onGlobalStateChange?.((state){if(state.message)setReceivedMsg(state.message)},true)}elseif((windowasany).$wujie){constlistener(msg:string)setReceivedMsg(msg);(windowasany).$wujie?.bus?.$on?.(main-msg,listener)return(){;(windowasany).$wujie?.bus?.$off?.(main-msg,listener)}}},[])Vue 子应用同理exp2-vue/src/pages/TodoList.vue不需要任何 qiankun/wujie 的 npm 包。package.json里只有vue和vue-router。这种模式的好处子应用团队不需要学习微前端框架细节可以独立开发、独立构建、独立部署未来如果要换框架比如换成 Micro-app只需在入口层面调整业务代码不改七、选型建议决策因素qiankunwujieVite 8 兼容需手写 Promise Bridge 或等 vite-plugin-qiankun 适配开箱即用社区生态更大文档和问题解答更多较新但 API 更简洁沙箱JS Proxy配置灵活但可能有代理开销Web Component / iframe原生隔离API 复杂度registerMicroApps start配置字段多startApp 一行配置字段少路由控制框架内部 activeRule 匹配由业务层React Router控制学习曲线较高生命周期概念多低概念少如果你的项目已经用 qiankun 且运行稳定不需要迁移。如果是新项目并且选择 Vite 8wujie 会省掉很多适配工作。如果你的场景需要更精细的沙箱控制和社区支持qiankun 仍然是成熟的选择。项目源码完整代码见https://gitee.com/bytesifter/front-example├── packages/ │ ├── qiankun-main/ ← qiankun 主应用含 Promise Bridge 实现 │ ├── wujie-main/ ← wujie 主应用 │ ├── exp1-react/ ← React 子应用零依赖适配 │ ├── exp2-vue/ ← Vue 子应用零依赖适配 │ └── bff/ ← NestJS Fastify BFF 服务 └── articles/ ← 本文及相关文章下一篇《NestJS Fastify Nacos 打造配置实时推送》讲解这个项目的 BFF 层是如何从 Nacos 配置中心拿配置、通过 SSE 实时推送到浏览器的。