前端项目做技术栈迁移路由从来不是最轰轰烈烈的部分但几乎每次都会成为拖垮进度的隐形杀手。React 组件可以推倒重写状态管理可以重新设计唯独路由因为每个页面入口、每个权限位点、每条跳转逻辑都挂在它身上没法一夜之间替换干净。最近我在帮一个老项目做 Vue 到 React 的迁移顺手把两套路由体系之间的适配逻辑完整趟了一遍沉淀出一套 VuReact 路由适配层的方案今天完整拆给你看。先说清楚这套方案解决什么问题它不是让你在项目里同时维护两套路由表也不是简单地把 Vue Router 的写法硬翻译成 React Router。它做的事是让路由配置成为一份独立于框架的“中间协议”Vue 端和 React 端各有一个适配器消费这份协议业务代码通过统一桥接层调用跳转、读取参数、处理权限不感知自己当前跑在哪个框架里。适合正在做 Vue 3 项目往 React 18 迁移、或者需要双框架并存的团队直接参考。1. 迁移场景下的路由痛点为什么非做适配不可1.1 路由在项目里承担的隐性职责很多人以为路由就是“路径对应组件”迁移时照着翻译就行。实际拆一个真实的后台管理系统会发现路由在项目里承担的隐性职责远远超过跳转本身。老项目里路由配置上挂着大量 meta 信息菜单排序、图标标识、面包屑映射、埋点代码、访问角色列表。这些信息被布局组件、菜单组件、面包屑组件、埋点 SDK 同时消费。Vue 组件迁移到 React 时这些消费方全部要换组件实现但 meta 数据本身不该动。路由守卫承担的可不只是登录校验。我在老项目里见过一个 beforeEach 里塞了七件事登录态检查、角色鉴权、页面标题设置、菜单高亮计算、来源页记录、多标签页缓存策略、甚至还有灰度开关判断。这套逻辑分散在几十个文件里如果迁移时强行把 beforeEach 翻译成 React Router 的 loader会发现中间桩逻辑根本搭不进去。还有一类隐性问题就是业务代码其实到处都在隐式依赖“当前路由长什么样”。比如某个列表页根据 route.query.source 决定是否弹窗某个详情页根据 route.params.id 调初始化接口某个布局根据 router.currentRoute.value.path 高亮菜单。这些读路由的地方不像跳转那样有明确的 API 调用痕迹它们散落在组件生命周期、状态初始化、订阅回调里迁移时最容易漏。真正把这个问题想明白之后结论就出来了路由不能作为 Vue 内的一部分被“替换成” React它必须被抽出来作为一个独立层让两端都去适配它而不是让业务去适配两端。1.2 直接“翻译”老代码会踩哪些坑如果团队决定不做适配层直接在 React 项目里按照 React Router 文档重写路由通常会撞见几类坑。第一类是跳转参数语义离散。Vue Router 的 router.push 可以传路径、传命名路由加 params、传 query 对象React Router 的 navigate 主要认字符串路径复杂参数要么拼在路径里要么塞进 state。老代码里 push({ name: user-detail, params: { id: 123 } }) 这种写法在 React 端没有 name 概念你必须全部改成字符串拼一路参数这个改动量非常惊人。第二类是导航拦截时机完全不同。Vue Router 的 beforeEach 在导航确认前同步执行可以返回 false 或重定向路径来取消导航React Router 没有全局守卫v6 里通常用 loader 做数据预取、用渲染期组件拦截做鉴权。直接翻译会让守卫逻辑的执行顺序、取消行为、异常处理方式都发生变化很难保证行为一致。第三类是嵌套路由的默认子路由语义存在差异。Vue Router 里子路由 path 为空字符串表示在父级路径下渲染默认出口React Router v6 也支持 index 路由或空 path 子路由但两者对 URL 拼接、面包屑生成、高亮匹配的细节处理不同迁移后容易出现菜单高亮错位、默认页空白这类隐蔽 bug。第四类是通配符和动态参数的表达式差异。老项目如果用了 Vue Router 4 的 /:pathMatch(.)做前端兜底React Router 6 的兜底路径是 /*直接翻译时正则部分得逐条改漏一条就会导致 404 页面行为不统一。所以我的建议是别在“翻译式迁移”上浪费时间直接上一个适配层让两端的路由消费协议保持一致这才是真正能落地的迁移路径。2. Vue Router 和 React Router 的核心差异对照2.1 配置声明方式routes 数组 vs RouteObject把两套路由的配置结构摆在一起差异会非常直观。Vue Router 4 的路由表是 RouteRecordRaw 数组每个记录包含 path、name、component、children、redirect、meta 这些字段React Router 6 的 RouteObject 包含 path、element、children、loader、errorElement、index 等字段。看起来很像但有一个关键差异Vue Router 的 component 是直接引用组件对象React Router 的 element 是要渲染的 React 元素。这导致适配层不能简单地把字段名映射一遍而是需要在中间层做一次“组件注册表”定义——每个路由节点提供的组件实现可能是 Vue 组件、可能是 React 组件、也可能两边都有。让我用一个对照表说明关键差异点对比点Vue Router 4React Router 6路由表字段path, name, component, children, redirect, metapath, element, children, loader, errorElement, index动态路径段:id:id兜底匹配/:pathMatch(.*)*/*导航拦截beforeEach / beforeResolve 全局钩子无全局钩子用 loader / 渲染期拦截渲染出口RouterView 组件Outlet 组件跳转 APIrouter.push / router.replacenavigate / Navigate 组件当前路由读取useRoute() 返回响应式对象useLocation() / useParams() / useSearchParams()参数传递query、params、metaquery、params、state2.2 导航拦截模型全局守卫 vs 渲染期校验这是两套路由体系差异最深的点也是把老守卫逻辑迁移过来时最容易翻车的地方。Vue Router 的守卫是真正意义上的“导航守卫”在 URL 变更请求发出后、视图切换完成前按注册顺序执行 beforeEach、beforeResolve、afterEach。beforeEach 里你可以读取目标路由和来源路由的完整信息可以决定放行、重定向到别处、或者直接抛出异常阻止导航。这种模型非常适合做登录态前置校验、页面白名单过滤这类“导航前决策”。React Router 6 的设计哲学不太一样。导航本身几乎不拦截URL 变化后直接进入匹配和渲染流程。你可以在 loader 里做数据预取并 throw redirect也可以在组件渲染时根据当前状态决定是否 Navigate 到别的页面。效果上能做到相似但时机和副作用边界不同。适配时不能把函数签名一对一搬。比如 Vue 的 beforeEach(to, from, next) 这种三参数风格在你的业务代码里可能已经被写得到处都是。为了让迁移初期老代码尽量少改适配层应该提供一个守卫转换器把“声明式的守卫函数”统一转成双端各自的执行机制。2.3 渲染出口与嵌套规则RouterView 与 Outlet嵌套路由的渲染出口看起来就是“父组件里放一个占位组件子路由渲染到占位处”但细节里有个影响很大的差异。Vue Router 的 RouterView 是全局注册的组件它会根据当前路由记录的深度自动决定渲染哪一层的内容React Router 的 Outlet 是父路由元素里的一个占位嵌套层级由 RouteObject 的 children 结构显式决定。两者的匹配深度计算规则不同导致同一个嵌套结构在两端渲染的结果可能有差异。更麻烦的是RouterView 还支持插槽作用域scoped slotReact Router 的 Outlet 在 v6 中也能接收 context 上下文。适配层需要统一这一层让嵌套路由的父子关系、默认子路由、缓存策略Vue 的 keep-alive 与 React 的缓存组件在两端都能正常表达。我在第 5 章会专门讲这块的落地方式。3. VuReact 适配层的方案设计3.1 适配层要解决的四个问题设计 VuReact 适配层之前我先明确了它要解决的四个核心问题后续所有代码结构都围绕这四点展开。第一个问题是配置统一。路由表必须从 Vue 组件代码中抽离出来变成纯数据声明Vue 适配器和 React 适配器都消费同一份配置。这样无论最终跑在哪个框架上业务路由的全貌只有一个来源不会出现两端路由表漂移。第二个问题是组件提供方式。每个路由节点需要渲染的组件可能只有 Vue 版本、可能只有 React 版本、可能两边都已实现。适配层要支持在配置里同时声明两端组件缺失的一端自动生成桥接占位而不是让业务去关心“当前运行环境是哪个端”。第三个问题是导航调用统一。业务代码里的页面跳转、参数读写、后退前进统一走一个代理对象这个代理在内部根据当前运行端把调用翻译成 Vue Router 或 React Router 的对应 API。第四个问题是守卫机制迁移。把 Vue Router 的全局守卫逻辑抽象成与框架无关的纯函数然后分别注入到 Vue Router 的导航钩子和 React Router 的渲染拦截中保证行为一致。3.2 分层结构共享协议、双端适配器、运行时桥方案分成三层。最底层是共享协议层只放纯数据定义和纯函数逻辑不允许 import 任何 Vue 或 React 的包。中间层是双端适配器负责把共享协议翻译成 Vue Router 和 React Router 能消费的结构。最上层是运行时桥Registry 当前激活的 router 实例并暴露统一的跳转 API。src/router/ ├── config/ # 共享协议层 │ ├── routes.config.js # 统一路由配置唯一数据源 │ └── guard.js # 框架无关的权限校验纯函数 ├── adapters/ # 双端适配器 │ ├── vue-adapter.js # 读取 config生成 Vue Router 路由表 │ └── react-adapter.js # 读取 config生成 React Router 路由表 ├── bridge/ # 运行时桥 │ ├── bridge.js # 注册/获取当前 router统一跳转 API │ ├── react-in-vue.js # Vue 侧渲染 React 组件的桥接组件 │ └── vue-in-react.jsx # React 侧渲染 Vue 组件的桥接组件 └── index.js # 对外导出createBridgedVueRouter / createBridgedReactRouter / bridge这个分层的关键在于依赖方向底层不依赖上层适配器依赖配置层bridge 是运行时集合但自身也是纯工具业务只依赖 bridge 和 config不直接接触任何框架 router API。3.3 一份路由配置的完整声明示例统一路由配置是这套方案的核心数据结构。我用一个接近真实后台系统的例子来展示// src/router/config/routes.config.js export const routesConfig [ { path: /login, name: login, component: { react: () import(/views/login/LoginView.jsx), }, meta: { title: 登录, public: true, }, }, { path: /, name: home, component: { vue: () import(/views/home/HomeView.vue), react: () import(/views/home/HomeView.jsx), }, meta: { title: 首页, public: true, }, }, { path: /dashboard, name: dashboard, component: { react: () import(/views/dashboard/DashboardLayout.jsx), }, meta: { title: 控制台, auth: true, }, children: [ { path: , name: dashboard-index, component: { vue: () import(/views/dashboard/IndexPanel.vue), }, meta: { title: 概览 }, }, { path: settings, name: dashboard-settings, component: { react: () import(/views/dashboard/SettingsView.jsx), }, meta: { title: 设置 }, }, ], }, { path: /:pathMatch(.*)*, name: not-found, component: { react: () import(/views/error/NotFoundView.jsx), }, meta: { title: 页面不存在, public: true }, }, ]这个配置文件里有几点值得注意。component 对象以 vue 和 react 作为键分别声明两端实现这个对象可以只有一端有值缺失的一端交给适配器自动生成桥接组件。path 字段沿用 Vue Router 的相对路径习惯React Router 适配时原样接受。catch-all 路由写成 Vue Router 的 /:pathMatch(.)React 适配器要转换成 /*。4. 双端适配器与运行时桥的实现4.1 Vue 侧把统一配置 normalize 成 Vue Router 路由表Vue 适配器的职责比较轻因为统一配置本来就是按 Vue Router 的习惯设计的。核心要做三件事按路由节点提取 component.vue缺失时用 react-in-vue 桥接组件补位把 catch-all 的 path 原样传给 Vue Router注册路由后把实例登记进运行时桥。// src/router/adapters/vue-adapter.js import { createRouter, createWebHistory } from vue-router import { reactInVue } from /router/bridge/react-in-vue import { bridge } from /router/bridge/bridge import { applyVueGuard } from ./vue-guard function resolveComponent(record) { if (record.component?.vue) { return record.component.vue } // 只有 React 组件的路由节点包装成 Vue 可用的占位组件内部挂载对应 React 组件 const ReactComponent record.component?.react if (ReactComponent) { return reactInVue(ReactComponent, { routeMeta: record.meta }) } // 两侧都没有时给一个空占位方便排查路由配置漏项 return { render: () null } } function normalizeRecord(record) { const normalized { path: record.path, name: record.name, meta: record.meta || {}, redirect: record.redirect, } if (record.children) { normalized.children record.children.map(normalizeRecord) } else { normalized.component resolveComponent(record) } return normalized } export function createBridgedVueRouter(config, options {}) { const routes config.map(normalizeRecord) const router createRouter({ history: createWebHistory(options.base || /), routes, scrollBehavior(to, from, savedPosition) { return savedPosition || { top: 0 } }, }) applyVueGuard(router) bridge.register(vue, router) return router }这里把 children 为空的节点和有 children 的节点分开处理是关键。Vue Router 中带 children 的父路由可以不写 component渲染出口交给子路由的 RouterView 决定。如果父路由同时有 component 和 children那 component 渲染的布局里必须包含 RouterView 作为子路由出口这点在配置协议里要约定清楚。4.2 React 侧把统一配置 normalize 成 React Router RouteObjectReact 适配器要做的事更多因为配置协议和 React Router 的 RouteObject 结构有差异。一是要把 component.react 包装成 element二是要把 catch-all 路径从 /:pathMatch(.)转成 *三是组件缺失时用 vue-in-react 桥接组件补位四是要在渲染期权限拦截上做统一处理。// src/router/adapters/react-adapter.js import { createBrowserRouter } from react-router-dom import { VueInReact } from /router/bridge/vue-in-react import { bridge } from /router/bridge/bridge import { RequireAuth } from ./react-auth import { NotFoundPage } from ./react-not-found function normalizePath(path) { if (path /:pathMatch(.*)*) return * return path } function resolveElement(record) { const ReactComponent record.component?.react const VueComponent record.component?.vue let element null if (ReactComponent) { element ReactComponent / } else if (VueComponent) { // 只有 Vue 组件的路由节点用桥接组件把它包成 React 元素 const Bridge VueInReact(VueComponent) element Bridge routeMeta{record.meta} / } else { element NotFoundPage / } if (record.meta?.auth) { element ( RequireAuth meta{record.meta}{element}/RequireAuth ) } return element } function normalizeRecord(record) { const routeObject { path: normalizePath(record.path), element: resolveElement(record), } if (record.children) { routeObject.children record.children.map(normalizeRecord) } return routeObject } export function createBridgedReactRouter(config, options {}) { const routes config.map(normalizeRecord) const router createBrowserRouter(routes, { basename: options.basename || /, }) bridge.register(react, router) return router }注意 React Router 6 要求 element 是一个 React 元素而不是组件类型所以统一配置里的 component.react 在 normalize 阶段就要实例化为 不能等渲染时再解引用。另外带 children 的父路由和 Vue 端一样可以不提供 element子路由内容会渲染到父级的 Outlet 中。如果父布局组件需要作为子路由的外壳那就在父配置的 component.react 里给布局组件。4.3 导航 API 统一桥接业务代码不感知框架迁移期最让人头疼的就是老代码里散落的大量 router.push。为了实现“不同时改几百处调用”适配层提供一个 bridge 对象业务代码直接用 bridge.push 代替原来的 router.pushbridge 内部根据当前激活的运行时把参数翻译给对应 router。// src/router/bridge/bridge.js const state { currentRuntime: null, routers: { vue: null, react: null, }, } export const bridge { register(runtime, router) { state.routers[runtime] router if (!state.currentRuntime) { state.currentRuntime runtime } }, setRuntime(runtime) { state.currentRuntime runtime }, push(to) { if (state.currentRuntime vue) { return state.routers.vue.push(to) } return state.routers.react.navigate(to) }, replace(to) { if (state.currentRuntime vue) { return state.routers.vue.replace(to) } return state.routers.react.navigate(to, { replace: true }) }, back() { if (state.currentRuntime vue) { return state.routers.vue.back() } return state.routers.react.navigate(-1) }, forward() { if (state.currentRuntime vue) { return state.routers.vue.forward() } return state.routers.react.navigate(1) }, get currentRoute() { if (state.currentRuntime vue) { return state.routers.vue.currentRoute.value } const location state.routers.react.state.location return { path: location.pathname, query: Object.fromEntries(new URLSearchParams(location.search)), meta: {}, } }, }从 Vue 往 React 迁移的老代码原来写 router.push({ name: user-detail, params: { id: 1 } }) 这种对象参数在 React 端没法直接翻译。为了避免这部分老代码也要逐处改可以在 bridge.push 里加一层参数归一化把带 name 和 params 的对象转成实际路径再 navigate。这一步虽然有点“脏”但迁移期间非常管用。function resolveTarget(to) { if (typeof to string) return to if (to?.name state.routers.react) { // 从当前 adapter 暴露的路由索引里查 name - path const target routeIndex[to.name] if (target) { let path target.path if (to.params?.id) { path path.replace(:id, to.params.id) } if (to.query) { path serializeQuery(to.query) } return path } } if (typeof to string) return to // fallback return to?.path || / }这个 routeIndex 可以在 adapters 里构建维护一份 name - path 的映射表迁移完后可以逐步去掉这段兼容逻辑。4.4 权限守卫的双端落地守卫逻辑是整个适配层里最不能“两份各写一套”的部分。业务侧权限规则必须收敛到一个纯函数里否则迁移完会出现 Vue 端校验了角色、React 端忘了校验灰度开关这种细节错位。我在 config/guard.js 里定义了一个框架无关的权限校验函数// src/router/config/guard.js export function verifyRouteAccess(meta, ctx) { // ctx: { from, to, authStore } const authStore ctx.authStore if (meta.auth !authStore.isLoggedIn) { return { redirect: /login, reason: NO_AUTH } } if (meta.roles !meta.roles.includes(authStore.role)) { return { redirect: /403, reason: NO_ROLE } } if (meta.title ctx.setTitle) { ctx.setTitle(meta.title) } return null }Vue 端在 vue-guard.js 里把它挂到 beforeEach// src/router/adapters/vue-guard.js import { verifyRouteAccess } from /router/config/guard export function applyVueGuard(router) { router.beforeEach((to, from) { const result verifyRouteAccess(to.meta, { from: from.fullPath, to: to.fullPath, authStore: getAuthStore(), setTitle: setPageTitle, }) if (result) { return result.redirect } return true }) }React 端在 react-auth.js 里实现一个渲染期拦截组件// src/router/adapters/react-auth.jsx import { Navigate, useLocation } from react-router-dom import { verifyRouteAccess } from /router/config/guard export function RequireAuth({ meta, children }) { const location useLocation() const result verifyRouteAccess(meta, { from: location.pathname, to: location.pathname, authStore: getAuthStore(), setTitle: setPageTitle, }) if (result) { return Navigate to{result.redirect} replace state{{ from: location }} / } return children }两边调用同一个 verifyRouteAccess权限规则只需要维护一份。getAuthStore 可以指向 Vue 的 Pinia store 或 React 的 Zustand store反正调用处永远只读它的快照不依赖框架生命周期。5. 组件桥接让路由出口真正把两端页面渲染出来5.1 Vue 侧渲染 React 组件react-in-vue跨框架渲染组件听起来玄乎原理其实很朴素Vue 组件里放一个普通 div 占位然后拿到这个 div 的真实 DOM 节点用 React 的 createRoot 把目标组件挂到节点上。生命周期同步靠 Vue 的 render 函数和 unmounted 钩子来对齐。// src/router/bridge/react-in-vue.js import { h, onBeforeUnmount, ref } from vue import { createRoot } from react-dom/client export function reactInVue(ReactComponent, initialProps {}) { return { name: ReactInVueBridge, setup(props, { attrs }) { const container ref(null) let root null function renderReactComponent() { if (!container.value) return if (!root) { root createRoot(container.value) } const mergedProps { ...initialProps, ...props, ...attrs } root.render(h(ReactComponent, mergedProps)) } onBeforeUnmount(() { if (root) { root.unmount() root null } }) return () h(div, { ref: container, style: { width: 100%, height: 100% }, onMounted: renderReactComponent, onUpdated: renderReactComponent, }) }, } }这个桥接组件要注意的点是根节点不要用自定义标签直接渲染 div。某些 CSS 框架对自定义元素会有样式穿透问题用 div 最稳。另外 createRoot 只能对同一个节点初始化一次组件更新时复用 root.render卸载时 root.unmount这三个状态必须维护好否则会出现“页面还在但控制台报警告”的问题。5.2 React 侧渲染 Vue 组件vue-in-react反向桥接的思路一致React 组件里渲染一个 div用 Vue 的 createApp 把目标 Vue 组件挂上去。因为 Vue 组件实例有完整的响应式系统属性变化时最好通过重新 mount 来保证内部状态同步。// src/router/bridge/vue-in-react.jsx import { createApp, h as vueH } from vue import { useEffect, useRef } from react export function VueInReact(VueComponent) { return function VuePageBridge(props) { const containerRef useRef(null) const appRef useRef(null) useEffect(() { if (!containerRef.current) return const app createApp({ render() { return vueH(VueComponent, { ...props }) }, }) app.mount(containerRef.current) appRef.current app return () { app.unmount() appRef.current null } }, [props]) return div ref{containerRef} style{{ width: 100%, height: 100% }} / } }这里的坑在于 useEffect 依赖数组。如果直接传 props 对象React 每次渲染都会因对象引用变化重新挂载整个 Vue 实例这会丢掉 Vue 组件内部的临时状态体验很差。实际做法是只在关键的 routeMeta、路由路径参数变化时重新 mount其它属性变化尽量用事件或共享 store 同步。5.3 嵌套路由的出口映射RouterView 与 Outlet 的对应关系嵌套路由这块适配层不需要做太多魔法但要约定清楚。统一配置里有 children 的节点在 Vue 端由 RouterView 在父组件内渲染子路由在 React 端由 Outlet 承担同样角色。如果父布局组件是 Vue 实现它内部原本就写好了 RouterView桥接后 React Router 端会把这个 Vue 布局包成 vue-in-react 组件而 React Router 的子路由会尝试渲染到它内部的一个 Outlet 位置这时需要 Vue 布局组件里放一个“react outlet 容器”来承接。我通常的做法是在 Vue 布局组件里加一个普通 div 容器并给这个 div 注入一个标识属性react-in-vue 桥接时把 React Router 的 Outlet 挂到这个 div 上。这个环节比较绕我的建议是迁移初期尽量让带 children 的父布局路由两端的组件都提供Vue 父布局和 React 父布局各写各的子页面逐个替换。等整体迁移完成后再决定是否砍掉其中一端。这样能避免为了强行复用布局组件而引入过度复杂的桥接逻辑。6. 实操一个混合迁移示例从零跑通6.1 目录结构与依赖跑通这套适配层需要的依赖是 Vue 3、Vue Router 4、React 18、React Router 6。两套框架可以共存于同一个构建工程里我用 Vite 做构建它在处理 .vue 和 .jsx 混编时开箱即用不需要额外配置。工程目录在上一章已经给过实际迁移时还会在 src 下有一个 entry 文件决定当前启动的是哪个端。比如开发环境先跑 Vue 端配置入口是 main.js需要验证 React 端时切换到 main.jsx两个入口都调用同一份 routes.config.js只是创建 router 的方式不同。6.2 接入步骤拆解第一步抽取路由配置。把老 Vue 项目 router/index.js 里的路由表原样搬到 routes.config.jscomponent 字段全部改成 component: { vue: () import(...) } 的形式。这一步不改变任何路径和 meta只改结构工作量小但很关键。第二步接入 Vue 适配器。在 main.js 里把原来的 createRouter 换成 createBridgedVueRouter确保页面还能正常运行。因为配置结构兼容 Vue Router 习惯这一步几乎不会破坏现有路由行为。第三步搭 React 壳。新建一个最小可运行的 React 入口 main.jsx引入 createBridgedReactRouter先用一个空壳页面把整个路由结构跑通。React 端组件缺失的路由自动走 vue-in-react 桥把 Vue 老页面原样渲染出来。这一步结束后React 端已经可以完整浏览所有页面只是大部分页面还在跑 Vue 组件。第四步逐页面替换。按访问频次从高到低把高频使用的页面组件从 Vue 迁移成 React。替换完成后把配置里 component.vue 那一条删掉或保留注释保留 component.react。每替换一个页面React 端的真实组件占比就提升一点。第五步切换默认入口。React 端跑通全流程后把构建入口默认指到 main.jsxVue 端入口改成可选参数。再跑一遍全量回归。6.3 渐进迁移的节奏建议渐进迁移最忌讳的是“一次性把路由配置和所有组件一起切”。路由配置是唯一数据源必须先稳定组件可以慢慢替换。我实践的节奏是第一周只抽配置、接 Vue 适配器保证业务无感知第二周搭 React 壳和组件桥让 React 端可以先预览之后按模块每周替换一到两个页面每替换一个模块就把这个模块下的跳转调用从 router.push 改成 bridge.push。顺序上优先替换“路由叶子节点”而不是布局。因为叶子页面耦合面小替换一个就是一个布局节点替换会影响所有子页面风险大建议放到最后。还有一个实际建议在统一配置里给每条路由加一个 migrationStatus 字段标记待迁移、已迁移、桥接中三种状态。配合菜单组件用这个字段显示不同角标开发期间一目了然。7. 常见问题与排坑实录7.1 高频问题速查表现象原因处理方式React 端访问页面空白控制台无报错父路由有 element但子路由没有 Outlet 承接在父布局组件里显式放一个 Outlet或用桥接容器Vue 端 bridge.push 传对象参数不生效bridge 没有把 name params 归一化在 bridge.push 前面加 resolveTarget 转换层React 端渲染 Vue 组件时Vue 组件内部路由跳转失效Vue 组件内使用了没有实例化的 useRouter给桥接组件注入一个 router 实例或在入口处统一注册权限拦截在 React 端重复触发RequireAuth 嵌套层级超过一层检查配置里 auth 标记是否同时出现在父路由和子路由上Vue Router 在迁移后 catch-all 路由吞掉了 React 端的 404 页面两套路由同时监听了同一个 URL双端并存时不要同时启动两个入口用一个入口加载单端 router动态路由段 :id 在 React 端无法从 useParams 读取配置 path 写法混入了 Vue 风格的可选参数检查是否用了 :id? 之类 Vue 特定语法改成 :id7.2 几个值得单独记录的坑第一个坑是 React Router 的 createBrowserRouter 在开发环境下需要服务端配合。Vite dev server 默认支持 SPA fallback问题不大但如果你把适配后的 React 端部署到 CDN直接访问 /dashboard 这个深层路径会 404。解决办法是在部署配置里加一条try_files $uri /index.html规则否则用户刷新页面就会看到白屏。第二个坑是双框架共存时的全局事件冲突。Vue 和 React 都会挂载全局监听如果某个 Vue 页面里用了 keep-alive 缓存组件它内部监听的事件在页面“假卸载”期间依然挂在全局而切换成 React 页面后这些监听可能覆盖同类型事件。排查半天发现是一个全局热键监听同时被两端注册。我的建议是共享 store 用纯 JS 实现不依赖两端任何生命周期。第三个坑是路由懒加载分包冲突。Vue 组件用 dynamic import 生成 chunkReact 组件也生成 chunk如果有两个 chunk 都叫 index构建后的产物在部分浏览器缓存策略下会串。处理方式是在 Vite 的 rollupOptions 里给不同目录的 chunk 加前缀。第四个坑是 React 18 的 StrictMode 双调用问题。开发环境下 StrictMode 会执行两遍 render跨框架桥的 mount 会被调用两次导致 createRoot 警告和重复挂载。在桥接组件里加一个 isMounted 标志第二次 mount 时直接复用已有 root可以有效避免这个问题。我在实际迁移中把上面这些坑逐个踩过之后整个适配层才真正稳定下来。每次踩坑都会回到同一个感悟上跨框架适配的核心不是让两个框架互相兼容而是让业务代码站在一个统一的抽象后面框架细节全部收口到适配层里。这套 VuReact 路由适配方案的价值不在某个具体 API 的实现而在它帮你把迁移期间最重要的数据源和接口面收住了让 Vue 到 React 的切换可以一页一页地推进而不是一夜之间重写全部路由。最后再分享一个小技巧迁移完成后不要急着把适配层删掉把它留着作为双端路由的对照层后续如果再做 SSR 或微前端改造这份统一配置还能继续用。