主应用和子应用抢路由?实际项目里我用这几招把冲突理清了
微前端里主应用和子应用抢路由,根子在于两套路由系统都在操作同一条 URL,谁也不让谁。解决思路不是让谁闭嘴,而是划定职责边界,让主应用和子应用各管各的 prefix,冲突自然消失。下面是我在几个项目里踩过坑后沉淀下来的 5 个落地方案,按推荐程度从高到低排列。
方案一:prefix 硬隔离(qiankun/无界等框架通用)
核心做法:主应用只注册自己的路由,所有子应用路由统一挂在一个固定 prefix 下,子应用内部的路由 base 也设为同一个 prefix,双方井水不犯河水。
以 qiankun + Vue Router 4 为例。主应用 router:
// 主应用 router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
component: () => import('@/views/Home.vue')
},
{
path: '/about',
component: () => import('@/views/About.vue')
},
// 注意:这里不注册 /app1 开头的路由
{
path: '/:pathMatch(.*)*',
component: () => import('@/views/404.vue')
}
]
})
qiankun 注册子应用时,activeRule 指定 prefix:
// 主应用 micro-apps.ts
import { registerMicroApps, start } from 'qiankun'
registerMicroApps([
{
name: 'app1',
entry: '//localhost:8081',
container: '#subapp-container',
activeRule: '/app1' // 子应用激活规则
}
])
start()
子应用内部 router base 必须与 activeRule 一致:
// 子应用 router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory('/app1'), // 关键:base 设为 /app1
routes: [
{ path: '/', component: () => import('@/views/Dashboard.vue') },
{ path: '/user', component: () => import('@/views/User.vue') }
]
})
这样 URL 变成 /app1/user 时,主应用 router 发现自己没有匹配项,不会干预;qiankun 检测到 /app1 激活子应用;子应用内部 router 再匹配 /user。三方各走各的路径,零冲突。
这个方案能覆盖 90% 的场景。唯一需要注意的点:主应用的 404 路由 /:pathMatch(.*)* 必须放在最后,否则会把 /app1 也吞掉。React Router v6 同理,用 <Route path="*" element={<NotFound />} /> 兜底。
方案二:手动劫持 history 操作,子应用内部走 memory 路由
有些老旧子应用的路由 base 写死在代码里,改不动;或者你用的是 iframe 嵌入而非 qiankun 这类沙箱方案,子应用直接操作 window.history 会把主应用 URL 冲掉。这时候可以让子应用放弃 history 模式,改用 memory 路由,所有路由变化只存在内存里,不碰浏览器地址栏。
子应用侧改造:
// 子应用 router/index.ts
import { createRouter, createMemoryHistory } from 'vue-router'
const router = createRouter({
history: createMemoryHistory(), // 不操作 URL
routes: [
{ path: '/', component: Dashboard },
{ path: '/user', component: User }
]
})
代价是子应用的路由状态不会反映在 URL 上,用户刷新页面后子应用会回到初始路由。要解决这个问题,可以在主应用侧做一层映射:主应用监听自己的路由变化,把子应用需要的路径信息通过 props 或自定义事件传给子应用,子应用在 mount 生命周期里根据传入的初始路径做一次 router.push。
我在一个旧系统迁移项目里用过这招,子应用是 jQuery + Director.js 的老古董,没法改 base,memory 路由是唯一解。缺点很明显:URL 不可分享、浏览器前进后退需要额外处理,能不用尽量别用。
方案三:主应用 404 兜底 + 子应用手动激活
这个方案适合主应用路由极其复杂、不想为每个子应用手动维护 exclude 规则的场景。
思路:主应用不主动避开子应用的路由 prefix,而是让子应用的路由全部命中主应用的 404 兜底路由。在 404 路由的组件里,判断当前路径是否属于某个子应用,如果是,就手动调用 qiankun 的 loadMicroApp 加载子应用。
// 主应用 NotFound.tsx
import { loadMicroApp } from 'qiankun'
import { useEffect, useRef } from 'react'
import { useLocation } from 'react-router-dom'
const subAppPrefixes = ['/app1', '/app2', '/app3']
export default function NotFound() {
const location = useLocation()
const appRef = useRef<any>(null)
useEffect(() => {
const matchedPrefix = subAppPrefixes.find(p => location.pathname.startsWith(p))
if (matchedPrefix && !appRef.current) {
appRef.current = loadMicroApp({
name: matchedPrefix.slice(1), // 去掉开头的 /
entry: `//localhost:8081`,
container: '#subapp-container'
})
}
return () => {
appRef.current?.unmount()
}
}, [location.pathname])
if (!subAppPrefixes.some(p => location.pathname.startsWith(p))) {
return <div>页面不存在</div>
}
return <div id="subapp-container"></div>
}
这个方案的坑在于时序:用户直接访问 /app1/user 时,主应用路由先匹配到 404,然后才加载子应用,子应用内部再解析 /user。如果子应用内部有 redirect 逻辑(比如 / 自动跳 /dashboard),URL 会经历一次变化,体验上会有闪烁。另外 loadMicroApp 返回的实例需要手动管理卸载,否则切走再回来会重复挂载。
我在一个主应用路由由后端配置动态生成的项目里用过,因为没法在前端写死 exclude 规则,这个方案是唯一可行的。但每次路由切换都要走一遍 404 判断,性能不如方案一。
方案四:主应用路由守卫里主动放行
如果主应用用的是 Vue Router,可以在 beforeEach 守卫里写一条规则:遇到子应用 prefix 开头的路径,直接 next(false) 中止当前导航,让子应用去处理。
// 主应用 router/index.ts
const subAppPrefixes = ['/app1', '/app2']
router.beforeEach((to, from, next) => {
const isSubAppRoute = subAppPrefixes.some(p => to.path.startsWith(p))
if (isSubAppRoute) {
// 不阻止,让浏览器原生行为接管,qiankun 会处理
next()
return
}
// 其他主应用的路由守卫逻辑
next()
})
这个方案看起来很简单,但它有一个致命问题:它依赖 qiankun 的沙箱机制在 next() 之前就已经拦截了 URL 变化。在某些框架版本或自定义 history 实现下,next() 后主应用的 history listener 仍然会触发,导致主应用 router 状态错乱。我在 qiankun 2.4.x + Vue Router 4.0.x 的环境下实测可行,但在 2.3.x 版本上出现过主应用当前路由变成 /app1 的 bug。如果你要用这个方案,务必在目标版本上做好回归测试。
方案五:URL 参数传递路由信息,完全避开 path 冲突
这个方案比较野,但特定场景下意外好用。思路是子应用完全不占用 path,路由信息通过 query 参数传递。
主应用侧:
// 主应用激活子应用时,把子应用路径编码到 query 里
// URL 格式:/app1?__sub_path=/user/profile
registerMicroApps([
{
name: 'app1',
entry: '//localhost:8081',
container: '#subapp-container',
activeRule: '/app1'
}
])
子应用启动时从 query 里解析真实路径:
// 子应用 main.ts
function getSubPath() {
const params = new URLSearchParams(window.location.search)
return params.get('__sub_path') || '/'
}
const router = createRouter({
history: createWebHistory('/app1'),
routes: [...]
})
// 挂载后手动跳转
router.isReady().then(() => {
router.replace(getSubPath())
})
这个方案的优势是主应用路由完全不用改,/app1 对主应用来说就是一个合法的叶子路由。缺点是 URL 可读性差、分享链接不直观、子应用内部如果有多个路由跳转需要同步更新 query 参数。我在一个微前端 SDK 封装项目里用过,当时的需求是主应用完全不能感知子应用的内部路由结构,这个方案刚好满足。
选型建议
| 场景 | 推荐方案 |
|---|---|
| 标准 qiankun/无界项目,子应用可控 | 方案一(prefix 硬隔离) |
| 子应用无法改 base,或 iframe 嵌入 | 方案二(memory 路由) |
| 主应用路由由后端动态生成,无法写死 exclude | 方案三(404 兜底手动加载) |
| 快速验证原型,不想改主应用路由结构 | 方案四(路由守卫放行) |
| 主应用对 URL 结构有严格规范,子应用路径不能出现在 path 里 | 方案五(query 传参) |
实际项目里我几乎只用方案一,因为它最简单、最可靠、最符合微前端「各管各的」的初衷。其余四个方案都是在边界条件下不得已的选择,各有各的代价。选型时先问自己一句:子应用的 base 能不能改?能改就直接方案一,别折腾。
常见问题
子应用内部用了 router-link 跳转,URL 变成 /app1/user 后主应用也跟着变了,怎么处理?
这是因为子应用没有设置正确的 base。检查子应用 createWebHistory('/app1') 或 createBrowserHistory({ basename: '/app1' }) 是否正确传入。如果 base 正确,子应用内部的 router-link to="/user" 生成的 href 会自动带上 /app1 前缀,主应用不会感知到这次导航。
多个子应用都用 history 模式,切换子应用时主应用 URL 残留上一个子应用的路径怎么办?
qiankun 在切换子应用时会自动清理 URL,前提是子应用的 activeRule 配置正确且子应用卸载时没有再次操作 window.history。如果出现残留,检查子应用 unmount 生命周期里是否有 router.push 之类的操作,或者在 unmount 里手动调一次 router.destroy()(Vue Router 4.3+ 支持)。
主应用和子应用都用了 Vue Router,scrollBehavior 互相干扰怎么解?
主应用和子应用的 scrollBehavior 是独立生效的,因为它们操作的是各自的 router 实例。如果出现滚动位置异常,多半是子应用切换时主应用的滚动容器也被重置了。在主应用 scrollBehavior 里加一条判断:如果目标路由是子应用 prefix,直接返回 false 不做滚动处理。
不用 qiankun,用 Module Federation 怎么处理路由冲突?
Module Federation 本质上是一个异步加载的 chunk,没有主/子应用的概念,路由都在同一个 router 实例下。不存在抢路由的问题,只需要注意远程模块的路由配置不要和宿主应用的路由 path 重名。可以通过在远程模块路由 path 前统一加 namespace 前缀来规避。