qiankun微前端跨应用路由跳转实战:三种场景与参数传递全解析

发布时间:2026/10/1 16:42:11
qiankun微前端跨应用路由跳转实战:三种场景与参数传递全解析 凡是把 qiankun 接进生产环境的团队大概率都会在某个迭代里碰到同一个问题跳转。前阵子我们项目刚做完微前端改造群里新来的同事问我从微应用 A 怎么跳到微应用 B我当时一愣——这问题看着简单真要把主应用跳微应用、微应用跳主应用、微应用互跳三种场景全部说清楚还真不是一句话能讲完的。很多人一开始觉得跳转有什么好研究的router.push、window.location、a 标签不都能跳吗真把 qiankun 的主微应用架构跑起来之后你会发现事情远没那么简单地址栏只有一个但路由的主人却在不停切换一个不小心就是白屏、刷新丢状态、菜单不高亮、子应用内部路由跟主应用路由互相打架。这篇文章就把我在实际项目中用到的所有跳转方式完整梳理一遍从原理到代码再到踩坑记录让第一次接触 qiankun 微前端的朋友也能照着落地。1. 先搞懂一件事qiankun 里跳转的路由主人到底是谁1.1 地址栏只有一个路由实例却有两套先说一个最容易被忽略的事实qiankun 把所有微应用都塞进了一个浏览器页面里但地址栏只能显示一个 URL。这个 URL 要同时表达两个信息——当前属于哪个微应用、以及在这个微应用里的哪个页面。这里就引出了问题的核心主应用和每个微应用各自维护着自己的路由实例。主应用有主应用的路由表微应用 A 有微应用 A 的路由表微应用 B 有微应用 B 的路由表。它们在物理上是互相隔离的谁也不能直接调用对方的router.push。所谓跳转本质上是让地址栏的 URL 发生变化然后由对应的应用来响应这个变化。qiankun 官方提供了registerMicroApps注册微应用时每个微应用都有一个activeRule激活规则。这个规则就是 URL 的门牌号当地址栏 URL 满足某个微应用的activeRule时qiankun 就加载并挂载这个微应用不满足时就卸载它。所以跳转的最底层原理无非是修改 URL 让它命中不同的activeRule。1.2 三种跳转场景本质上对应两种匹配机制把业务场景抽象一下其实就三种主应用 → 微应用URL 从未匹配任何微应用到匹配某个微应用。微应用 → 主应用URL 从满足某个微应用的activeRule变为不满足任何微应用规则主应用自己的路由开始接管。微应用 A → 微应用 BURL 从满足 A 的规则变为同时满足 B 的规则。前两种场景的代码写法完全不同第三种场景实际上又可以拆成两种实现思路。接下来逐个展开。判断一种跳转方式是否合格我一般就看两个标准第一跳转后手动刷新浏览器页面能不能回显到同样位置第二跳转后目标微应用的路由能不能正常初始化不白屏。后面讲到的所有方式你都可以拿这两个标准去检验。2. 主应用跳微应用navigateToUrl 与 props 注入两条路线2.1 最直接的官方 APInavigateToUrl主应用跳微应用最简单的做法就是使用 qiankun 官方提供的navigateToUrl方法。// 主应用 import { navigateToUrl } from qiankun; // 跳转到微应用 app1 的某个页面 navigateToUrl(/app1/user/detail?id123);这个方法内部会修改浏览器的 history 状态URL 变化之后触发了activeRule匹配qiankun 发现当前 URL 已经命中微应用 app1 的规则于是自动去加载并挂载 app1。整个过程完全由 qiankun 接管不需要你手动去操作容器 DOM。如果你用的是 hash 路由模式写法略有区别navigateToUrl(#/app1/user/detail);这里我强调一个生产项目里的经验主应用和所有微应用尽量统一路由模式要么全都用 history要么全都用 hash。混用的话hash 路由模式下navigateToUrl传的路径格式跟 history 模式不一样很容易出现URL 变了但微应用没激活或者激活了但内部路由匹配不上的诡异问题。我们项目一开始就是主应用用 history、某个老微应用用 hash结果跳过去之后页面白屏排查了很久才发现是路由模式冲突。2.2 进阶方案把主应用路由实例通过 props 交给微应用navigateToUrl很适合从主应用主动发起的跳转但如果跳转之后还需要微应用内部做一些联动操作比如根据参数去请求接口、更新某个 store那就需要在微应用挂载时拿到主应用传递的导航能力。qiankun 的registerMicroApps支持给每个微应用配置props这些 props 会作为参数传给微应用生命周期函数mount// 主应用 import { registerMicroApps, start } from qiankun; registerMicroApps([ { name: app1, entry: //localhost:7101, container: #subapp-viewport, activeRule: /app1, props: { // 把主应用的路由实例传给微应用 mainRouter, // 或者封装一个统一的跳转方法 goto: (path) mainRouter.push(path), }, }, ]); start();微应用这边在main.js的mount生命周期里接收// 微应用 app1 的 main.js let mainRouter; export async function mount(props) { mainRouter props.mainRouter; // 也可以直接调用主应用传入的 goto 方法 props.goto(/home); } export async function unmount() { mainRouter null; }通过这种方式微应用就有了调用主应用路由能力的入口。这在微应用内部某个操作需要跳回主应用页面时特别有用后面第三部分会详细说。2.3 微应用基础路径 base 必须和 activeRule 对齐主应用跳微应用时最容易踩的第一个坑就是白屏。多数情况下问题出在微应用的路由基础路径没有和activeRule对齐。假设主应用注册微应用时activeRule是/app1那微应用内部的路由也应该把自己的base设置为/app1// 微应用 app1 的路由配置 // Vue Router以 Vue 3 为例 import { createRouter, createWebHistory } from vue-router; const router createRouter({ history: createWebHistory( window.__POWERED_BY_QIANKUN__ ? /app1 : / ), routes, });// React Router以 React Router v6 为例 import { BrowserRouter } from react-router-dom; function App() { return ( BrowserRouter basename{window.__POWERED_BY_QIANKUN__ ? /app1 : /} {/* 应用内容 */} /BrowserRouter ); }window.__POWERED_BY_QIANKUN__是 qiankun 运行时注入的全局标记用来判断当前是否运行在 qiankun 环境里。这样做的目的是微应用独立开发时 base 是/不影响本地调试被 qiankun 托管时 base 自动变成/app1和activeRule保持一致。为什么必须一致因为地址栏 URL 是/app1/user/detail时微应用的路由器需要把/app1作为前缀剥离掉剩下的/user/detail才能在它的路由表里匹配到对应组件。如果 base 没设置成/app1微应用内部会把整个/app1/user/detail拿去匹配自然匹配不到任何路由页面就只剩一个空白容器了。3. 微应用跳回主应用别再用 window.location 了3.1 为什么 window.history / location 是看似最快的坑很多初接触 qiankun 的同事微应用里要跳回主应用时第一反应就是写// 不推荐 window.location.href /home; window.history.pushState({}, , /home);代码确实一行搞定但这行代码有三个隐患第一它绕过了微应用自身路由实例的守卫逻辑。如果跳转前需要清数据、发埋点、或者调用路由的beforeEach钩子这么一跳全被跳过了。第二它没有经过 qiankun 的生命周期管理。qiankun 在每次切换应用时要做卸载和挂载靠的是内部对 URL 变化的监听和activeRule的匹配。你用window.location.href强跳如果目标地址的处理逻辑跟 qiankun 的匹配机制不完全兼容可能会出现主应用挂载了、但微应用没卸载干净的情况DOM 里残留上一个微应用的节点或者事件监听没被清理。第三在 hash 路由模式下window.location.href /home会把整个地址都替换掉微应用原来的 hash 信息也丢了。如果主应用是 history 模式、微应用是 hash 模式这种混用写法几乎必然出问题。3.2 通过 props 回调返回主应用页面的标准姿势推荐的做法是在注册微应用时通过props传入一个返回主应用页面的方法// 主应用 registerMicroApps([ { name: app1, entry: //localhost:7101, container: #subapp-viewport, activeRule: /app1, props: { navigateToMain: (path /home) { if (path.startsWith(/)) { // 使用官方 API确保 URL 变化走 qiankun 的匹配逻辑 navigateToUrl(path); } else { // 如果传的是相对路径这里根据业务做处理 console.warn(path must start with /); } }, }, }, ]);微应用里这样调用// 微应用 app1 export async function mount(props) { window.navigateToMain props.navigateToMain; } // 某个业务页面里 window.navigateToMain(/home);这个方案的好处是跳转逻辑统一封装在主应用侧微应用不直接操作 browser history也不依赖任何路由实例只需要调一个方法。后续如果要加权限校验、跳转埋点只需要改主应用里的navigateToMain实现所有微应用自动生效。3.3 全局状态 主应用路由监听实现受控返回还有一种使用场景微应用内部可能要连续跳转多个主应用页面或者跳转动作是由主应用的一个消息通知触发的这时用全局状态来驱动更合适。qiankun 提供了initGlobalState方法可以创建一个跨应用共享的状态对象// 主应用 import { initGlobalState } from qiankun; const { onGlobalStateChange, setGlobalState } initGlobalState({ nav: { target: , timestamp: 0, }, }); // 主应用监听全局状态里的导航指令 onGlobalStateChange((state, prev) { // 避免重复处理 if (state.nav.timestamp ! prev.nav.timestamp) { if (state.nav.target) { navigateToUrl(state.nav.target); } } });微应用里 下发指令// 微应用 app1 export async function mount(props) { props.setGlobalState({ nav: { target: /home?fromapp1, timestamp: Date.now(), }, }); }这里的timestamp是一个防重复处理的技巧。因为onGlobalStateChange在状态对象的任何属性变化时都会触发加上时间戳之后主应用就能通过比较前后两次的timestamp判断是否是一个新的导航指令避免因为其他无关状态变化导致重复跳转。这种受控返回方式的优势是解耦微应用不需要知道主应用内部怎么实现跳转主应用也不需要知道微应用什么时候会发起跳转双方只依赖一个约定好的状态结构。4. 微应用 A 跳微应用 B三套方案横评与实践4.1 主应用中转适合需要统一收口的业务微应用 A 跳到微应用 B最稳妥的方式是不直接跳而是让主应用来跳。流程是微应用 A 调用主应用传入的回调方法把目标微应用的路由告诉主应用主应用拿到这个路由后执行navigateToUrl跳过去。// 主应用 registerMicroApps([ { name: app1, entry: //localhost:7101, container: #subapp-viewport, activeRule: /app1, props: { navigateToMicroApp: (appName, path) { navigateToUrl(/${appName}${path}); }, }, }, { name: app2, entry: //localhost:7102, container: #subapp-viewport, activeRule: /app2, props: { navigateToMicroApp: (appName, path) { navigateToUrl(/${appName}${path}); }, }, }, ]);微应用 A 中调用// 微应用 app1 的某个页面 props.navigateToMicroApp(app2, /doc/detail?id100);主应用中转方案的核心价值在于收口。所有跨应用跳转都必须经过主应用这一层主应用可以在这里统一做登录态校验、权限判断、统一的埋点、甚至提前预加载目标微应用。如果你所在的公司有中台性质的业务多个微应用之间有复杂的相互跳转需求我强烈建议使用这种方式先把流程跑通再考虑优化。4.2 navigateToUrl 直达最简洁但耦合 URL 规则微应用内部也可以通过navigateToUrl直接跳到另一个微应用不经过主应用代码// 微应用 app1 里 import { navigateToUrl } from qiankun; navigateToUrl(/app2/doc/detail?id100);这样做的好处很直观微应用之间不依赖主应用的任何代码想跳就跳。但代价是微应用 A 必须知道微应用 B 的完整路由前缀和路由规则。一旦 B 改了activeRule前缀比如从/app2改成/app2-new所有直接跳过来的微应用都要跟着改。在微应用数量多、路由结构复杂的情况下这种隐式依赖会成为维护噩梦。我的建议是如果微应用之间跳转频率不高、路径规则相对稳定可以直接用navigateToUrl如果团队协作中经常有路由调整尽量用主应用中转的方案把 URL 规则的维护职责集中到主应用一侧。4.3 全局状态驱动跳转适合带复杂上下文第三种方式是全局状态驱动。微应用 A 通过setGlobalState下发一个跳转指令微应用 B 监听这个指令后在内部做路由跳转。// 微应用 app1 发起跳转 props.setGlobalState({ jump: { from: app1, to: app2, path: /doc/detail, params: { id: 100, fromPage: list, }, timestamp: Date.now(), }, }); // 微应用 app2 监听跳转指令 export async function mount(props) { props.onGlobalStateChange((state, prev) { if ( state.jump state.jump.to app2 state.jump.timestamp ! prev.jump?.timestamp ) { // 使用 app2 自己的路由实例做跳转 router.push({ path: state.jump.path, query: state.jump.params, }); // 处理完记得清理避免重复触发 props.setGlobalState({ jump: { ...state.jump, to: } }); } }); }这种方式的优势是能在跳转过程中携带复杂的上下文数据。上面例子里的params可以是一个对象理论上塞多少字段都行不依赖 URL 编码。缺点是状态清理和重复触发的控制需要额外小心不然很容易出现一个跳转指令被多个微应用同时响应的问题。生产实践中我一般只在跳转同时需要传递较大的业务数据对象时才用全局状态驱动普通的页面跳转用前两种方式就够了。4.4 选型对照表与我的建议把三种方案放到一张表里直接对比方案耦合程度携带数据能力统一收口能力实现复杂度适用场景主应用中转低微应用只依赖主应用回调弱通常只传路由字符串强可统一做权限、埋点低多数业务场景navigateToUrl 直达高需知道目标微应用完整路由前缀弱只能走 URL 参数弱每个微应用各自调用最低路由规则稳定、跳转频率低全局状态驱动中双方约定状态结构强可传复杂对象中可在主应用监听处统一处理中高需要传递复杂上下文我个人在项目里的选择是能用主应用中转就用主应用中转除非目标微应用的路由前缀被公共约定死了。主要原因是收口带来的维护价值太大了等线上出问题需要排查跳转链路时你只需要看主应用一层的代码就能知道所有跳转的来龙去脉而不是在十几个微应用里逐个翻。5. 跳转时参数怎么带query、hash、全局状态、Storage 全对比5.1 四种传参方式在不同场景下的表现跳转不只是换个 URL往往还要把业务参数带过去。比如从列表页跳到详情页要传一个id或者从微应用 A 跳到微应用 B 时要告诉 B 当前用户是从哪个入口进来的。我整理了一张传参方式对比表传参方式示例刷新是否保留参数大小限制安全性适用场景query 参数/app2/detail?id100保留小受 URL 长度限制明文可见简单业务参数hash 参数/app2#/detail?id100保留小明文可见hash 路由模式全局状态setGlobalState({...})不保留刷新即丢失大内存对象相对安全复杂业务数据StoragesessionStorage.setItem(...)保留仅当前标签页中等相对安全需要跨页面步骤共享数据query 参数是使用频率最高的方式因为它天然满足刷新可回显这个标准而且跳转目标微应用在mount之后只需解析自己的路由就能拿到所有参数。这里有个细节值得注意navigateToUrl传 query 参数时参数值如果包含中文或特殊字符需要先做编码。我一般会统一封装避免每个人都写一遍encodeURIComponent。5.2 微应用正确接收跳转参数的三个时机微应用接收跳转参数常见有三个时机第一个是mount生命周期里。qiankun 会把主应用传入的 props 带过来适合接收主应用明确通过 props 传给微应用的数据。如果跳转参数是通过 URL query 带过来的mount阶段其实还拿不到最终的路由 query因为微应用的路由实例此时可能还在初始化过程中。第二个是路由守卫里。这是最推荐的处理位置。以 Vue Router 为例// 微应用 app2 的路由守卫 router.beforeEach((to, from, next) { // to.query 里就是跳转带过来的参数 if (to.query.id) { store.commit(setDetailId, to.query.id); } next(); });React Router 也类似在组件里通过useSearchParams或者useLocation读取import { useSearchParams } from react-router-dom; function DetailPage() { const [searchParams] useSearchParams(); const id searchParams.get(id); // 根据 id 请求详情 }第三个是全局状态监听里。对应前面 4.3 节的方案微应用在onGlobalStateChange里拿到跳转指令和参数后再通过自己的路由push到目标页面同时把参数写入 store。三个时机的选择原则很简单URL 能表达的参数走路由守卫或组件内读取URL 表达不了的复杂对象走全局状态。尽量不要出现参数既放 URL 又放全局状态的做法会让接收方迷茫。5.3 把跳转封装成统一工具函数不管最后选了哪种方案我都建议把跳转封装成一个统一的工具模块而不是在业务代码里到处直接调navigateToUrl或window.location。下面是一个我常用的封装示例// navigation.js import { navigateToUrl } from qiankun; const APP_ROUTES { app1: /app1, app2: /app2, main: , }; /** * 统一的跨应用跳转入口 * param {string} appName - 目标应用名main 表示主应用 * param {string} path - 目标应用内的路由路径如 /detail * param {object} params - query 参数对象 */ export function go(appName, path /, params {}) { const queryString Object.entries(params) .map(([key, value]) ${key}${encodeURIComponent(value)}) .join(); const prefix APP_ROUTES[appName] ?? ; const fullPath ${prefix}${path}${queryString ? ? queryString : }; navigateToUrl(fullPath); } // 使用示例 go(app2, /detail, { id: 100, from: app1 }); // 实际跳转 URL: /app2/detail?id100fromapp1封装之后有几个明显的好处URL 前缀规则只在这个文件里维护其他业务代码完全不用关心目标应用的前缀。参数的 URL 编码逻辑统一处理不会出现某些业务方忘记编码导致中文乱码。将来如果要切换跳转实现比如从navigateToUrl改成主应用中转只需要改这一个文件。如果你们项目里微应用数量多还可以把appName和前缀的映射抽取成独立配置文件用构建变量注入这样微应用的注册顺序调整时也不会影响跳转逻辑。6. 生产环境踩坑实录从白屏到菜单不同步6.1 白屏排查链路base 配置与路由匹配第一个想重点说的坑是白屏。某次联调时从主应用点按钮跳微应用 app1微应用的入口脚本加载了、DOM 容器也创建了但页面区域就是一片空白。完整的排查链路是这样的第一步打开浏览器控制台确认activeRule有没有命中。在主应用的registerMicroApps里打个断点或者直接看 Network 面板确认 app1 的 entry 资源有没有被请求。如果没请求说明activeRule没匹配上URL 前缀不对。第二步资源请求了但页面空白进入微应用侧排查。打开微应用自己的路由实例在beforeEach里打印to.fullPath。结果发现to.fullPath是完整路径/app1/user/detail而路由表里定义的是/user/detail一个都配不上。第三步定位到是base配置问题。微应用的 Vue Router 创建时没加base判断独立开发和 qiankun 托管时用的都是/。把 base 改成window.__POWERED_BY_QIANKUN__ ? /app1 : /后路由路径正常匹配白屏解决。这个坑出现频率非常高而且是微前端项目必踩的建议在新人入职时就把这条排查路径讲清楚。6.2 微应用内部跳转后主应用菜单不高亮第二个坑是坐标不同步。微应用 app1 的某个页面上有个 tab点击后内部router.push切换到了/user/list。这时微应用页面内容变了但主应用侧边栏的菜单还停留在首页高亮状态。原因是主应用和微应用是两个互不感知的路由实例微应用内部router.push只会改变微应用自身的路由主应用根本不知道地址栏里其实已经变成了/app1/user/list。解决方案有两个方向方向一微应用内部跳转后主动通知主应用。通过props.setGlobalState把当前微应用路由路径同步给主应用主应用监听状态后更新自己菜单的高亮逻辑。// 微应用 app1 路由守卫里同步 router.afterEach((to) { props.setGlobalState({ currentMicroRoute: { appName: app1, path: to.fullPath, }, }); });方向二主应用监听地址栏变化。因为 URL 实际上已经变了主应用可以在自己的路由实例或一个全局监听器里根据当前 URL 反向计算出应该高亮哪个菜单。这个方案更通用不需要微应用主动配合但实现上要解析不同微应用的 URL 前缀稍微绕一点。我在生产项目里最终用了方向一因为状态同步的语义更清晰而且微应用本来就掌握自己的路由信息。6.3 history 模式刷新 404 的修复第三个坑和部署强相关。微应用用 history 路由模式时用户在/app2/detail?id100这种深层地址上按了 F5结果直接 404。原因很简单浏览器向服务器请求了/app2/detail这个路径而静态资源服务器上并没有这个文件。微应用是 SPA所有路由都应该回退到index.html由前端路由接管。开发环境下需要给每个微应用的 devServer 配置historyApiFallback// 微应用 app2 的 vue.config.js module.exports { devServer: { historyApiFallback: true, }, };生产环境下则需要 Nginx 层面的try_files回退location /app2 { try_files $uri $uri/ /app2/index.html; }这里有个容易漏的细节Nginx 的location块需要把微应用的静态资源目录和路由回退规则分别配置不能把所有路径都粗暴地回退到主应用的index.html否则微应用自己的 JS、CSS 资源会加载失败。6.4 最终沉淀的跨应用跳转规范踩了这么多坑之后我在团队内部沉淀了一套跳转规范分享出来做个参考第一所有跨应用跳转统一走封装好的go()工具函数。业务代码里不允许直接写navigateToUrl或window.location.href。第二微应用的路由前缀全局统一约定/app1、/app2这种命名规则写进团队文档activeRule、微应用路由base、Nginx 的location三者必须保持一致。第三跳转参数优先走 URL query确保刷新可回显复杂对象走全局状态但必须带timestamp字段防止重复处理。第四主应用作为唯一的跳转收口方。所有跨应用跳转指令由主应用统一执行微应用之间不直接互相感知。这套规范跑了大半年线上基本没再出现过跳转相关的疑难杂症。qiankun 的跳转方式本身不难难的是在没有规则约束的时候每个人按自己的习惯写一套最后汇聚成一个谁都看不懂的跳转大杂烩。你如果正准备做微前端改造建议先花半天时间把跳转方案定下来后面能省下无数个排查白屏的下午。