主应用和子应用抢路由?实际项目里我用这几招把冲突理清了

微前端里主应用和子应用抢路由,根子在于两套路由系统都在操作同一条 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 前缀来规避。