尧图精选

Bytebase 前端去 Vue 化迁移实战:基于 react-router 的路由、应用外壳与认证体系重构设计

🕒 发布时间:2026/9/15 21:12:38 📁 来源:尧图网络
Bytebase 前端去 Vue 化迁移实战基于 react-router 的路由、应用外壳与认证体系重构设计【免费下载链接】bytebaseDatabase governance built for humans and agents — controlling changes and access across every major database.项目地址: https://gitcode.com/GitHub_Trending/by/bytebase本文对应仓库设计文档 docs/superpowers/specs/2026-06-03-vue-to-react-routing-shell-auth-migration-design.md并以其为骨架结合当前仓库中已落地的实现代码进行印证与扩展。文章面向希望在大型前端工程中完成框架主权切换Framework Ownership Migration的架构师与资深前端工程师你将看到 Bytebase 如何用一个 big-bang PR把 Vue 壳层vue-router、App.vue、AuthContext.vue、3 套布局、5 个 React 挂载桥、Pinia 认证仓库、路由守卫、认证拦截器整体替换为单一 React 根 react-router 数据路由同时做到“零破坏性变更、路由守卫逐行忠实移植、认证状态以应用仓库为唯一事实来源”。读完本文你可以直接复用这套“路由表平移 loader 守卫 布局路由 惰性加载”的迁移模式。一、迁移背景为什么要动“壳”而不是动“页面”Bytebase 的前端经历了 Vue 与 React 长期共存的阶段业务页面早已用 React 重写但宿主host层仍是 Vue——Vue Router 承担路由匹配App.vue承担全局 provider 与浮层AuthContext.vue承担会话门控3 个 Vue 布局Splash/Body/Dashboard负责页面框架5 个.vue桥文件通过ReactPageMount把 React 页面“岛式”挂进 Vue 树同时用import.meta.glob维护全局页面注册表Pinia 认证仓库配合beforeEach全局守卫和 401 认证拦截器管理会话。这套双框架架构是彻底删除 Vue 的关键路径critical-path track页面已是 React迁移的主体工作其实是机械性的路由表平移 删除 Vue 文件。设计文档据此把交付方式锁定为一个 big-bang 单 PR并在 PR 内部按“每步提交都可构建、可评审”的顺序推进。从当前仓库状态看这条迁移路径已经基本走完frontend/package.json中已看不到vue-router、pinia、vue-i18n、vueuse、vitejs/plugin-vue、vue-tsc的身影取而代之的是react-router ^8.2.0与zustand ^5.0.13frontend/src/app/下已经存在完整的 React 路由壳AppRoot.tsx、RootLayout.tsx、AuthGate.tsx、router/目录而vue3.5.40与pev21.23.0两个依赖仍被保留——这正是设计文档中“pev2 反向岛屿”尚未被移除的体现详见 §3.3。二、锁定决策四条不可动摇的约束设计文档在一开始就锁定了四条硬约束与四项关键决策它们是整个迁移的“宪法”硬约束零破坏性变更纯粹替换宿主不改变任何用户可见行为。守卫逐行忠实移植新的守卫是旧router/index.ts中beforeEach的逐行移植——同样的检查、同样的顺序、同样的重定向。锁定决策策略big-bang 单 PR不搞双路由共存。路由方案react-router 数据路由createBrowserRouter、嵌套路由对象、lazy、loader与嵌套的 vue-router 配置保持 1:1 翻译。设计文档写作时计划的是 react-router-dom v7当前仓库实际落地为react-router ^8.2.0从 frontend/package.json 与源码 importreact-router、react-router/dom可以确认版本在实现过程中完成了升级。守卫实现beforeEach的忠实移植落地为根路由loader。pev2 替换在本 PR 内用 React 重写第三方 Vue 查询计划可视化组件从而彻底移除vue核心依赖实现干净、完整的删除。三、目标架构单一 React 根接管一切3.1 挂载方式从双挂载到单树迁移前main.ts需要createApp(App)挂载 Vue 树再单独挂载一个#react-app覆盖层迁移后main.ts只保留一个挂载动作。设计文档给出了目标形态ReactDOM.createRoot(#app).render( RouterProvider router{createBrowserRouter(routes)} / )这一形态在当前仓库已经实现挂载逻辑收敛在 frontend/src/app/mountApp.ts通过import.meta.glob(./AppRoot.tsx)惰性加载根组件而 frontend/src/app/AppRoot.tsx 中export const appRouter createBrowserRouter(routes); export function AppRoot() { return RouterProvider router{appRouter} /; }这里可以观察到两个值得注意的落地细节setAppRouter(appRouter)把数据路由器实例注册到模块级状态frontend/src/app/router/navigation.ts让非组件代码如认证 slice也能通过实例的navigate()完成命令式导航——这正是设计文档中“router.push→ React Router 实例的router.navigate()”的落点。由于数据路由默认启用部分水合partial hydration初始加载时/会先渲染一帧再执行 loader 的重定向产生控制台警告。实现通过routes[0].HydrateFallback () null定义了一个渲染空内容的 fallback既满足部分水合的契约又因 loader 同步解析而几乎不产生可见闪烁。3.2 组件树RootLayout → AuthGate → Outlet设计文档的目标树形如下RouterProvider router{createBrowserRouter(routes)} / └─ RootLayout // 替换 App.vueproviders 全局浮层 (Toaster, AgentWindow, Watermark, SessionExpiredSurfaceGate — 今天的 ReactApp) └─ AuthGate // 替换 AuthContext.vue以应用仓库会话门控渲染 └─ Outlet/ // 匹配的布局 页面当前仓库的 frontend/src/app/RootLayout.tsx 与设计完全吻合RootLayout聚合了此前散落在 ReactApp 中的全局浮层——Watermark、Toaster、AgentWindow、SessionExpiredSurfaceGate并用AuthGate包裹Outlet /使会话生命周期加载门控、轮询、跨标签页切换、闲置提醒环绕每个页面运行。此外实现还额外补充了两个系统级组件LeaveGuardBlocker用 react-router 的useBlocker还原 vue-router 全局beforeEach的取消语义——任何注册的离开守卫调用next(false)时阻止导航NavigationScrollRestoration滚动位置恢复弥补历史/返回行为一致性详见 §5.3。3.3 布局路由与“布局即页面”岛屿的收编3 个 Vue 布局Splash/Body/Dashboard在 React 侧对应为 frontend/src/app/layouts/ 下的SplashLayout.tsx、BodyLayout.tsx、DashboardLayout.tsx以布局路由形态挂进路由表通过Outlet /渲染子路由DashboardLayout中 vue-router 具名的router-view namebody同样收敛为一个嵌套Outlet /。SQL 编辑器中“布局当页面”的特殊岛屿也被收编为普通布局路由。路由表的组装在 frontend/src/app/router/routes.tsx根路由元素是RootLayout /errorElement指向 frontend/src/app/RouteErrorPage.tsx捕获整棵树中未捕获的渲染/loader 异常展示可恢复页面而非 react-router 的开发者默认屏children 依次展开authRoutes、dashboardRoutes、sqlEditorRoutes最后以path: *的 catch-all 兜底 404确保具体应用路径先被匹配。同一个文件里的buildRouteNameIndex()把嵌套路由表摊平为“路由名 → 完整路径模式”映射注册进navigation.ts供守卫与认证生命周期按名解析重定向目标。四、认证/会话模型与守卫逐行移植的艺术4.1 会话唯一事实来源zustand 应用仓库的 AuthSlice设计文档的核心决定会话状态的唯一事实来源从 Pinia 认证仓库迁到应用仓库app store的AuthSlice方法体原样复制只把导航调用从router.push换成 React Router 实例的router.navigate(...)。需要移植的状态与方法清单来自设计文档状态currentUser、unauthenticatedOccurred、requireResetPassword、authSessionKey、isSelfEmailUpdateGetterisLoggedIn方法login、signup、logout、fetchCurrentUser、setRequireResetPassword、sendEmailLoginCode、updateCurrentUserNameForEmailChange当前仓库的 frontend/src/stores/app/auth.ts 正是createAuthSlice的实现几个值得注意的细节isLoggedIn()的定义是Boolean(currentUserName) name ! UNKNOWN_USER_NAME——用“当前用户名已加载且不是未知用户”判定登录态loadCurrentUser()失败时不缓存失败清空currentUser/currentUserName这样登录后的下一次调用会重试避免“登录前失败导致 React 仓库永远加载不出认证用户”的坑setRequireResetPassword/requireResetPassword通过localStorage以邮箱为维度读写重置密码标记并兼容 private mode / quota 异常非致命失败。认证拦截器 [frontend/src/app/…authInterceptorMiddleware.ts] 在 401 时翻转unauthenticatedOccurred从useAuthStore()改为useAppStore.getState()读取/写入SessionExpiredSurface依旧对该标志做出反应但现在读的是应用仓库对应 frontend/src/app/components/SessionExpiredSurface.tsx。4.2 AuthGateAuthContext.vue 的 React 化身frontend/src/app/AuthGate.tsx 完整替换了AuthContext.vue的四类职责未登录重定向非认证路由 非公开路由403/404且未登录时navigate到 signin 并携带buildSigninRedirectQuery构造的redirect查询参数保留idp/workspace/email/token/invitation等登录专用参数同时从 redirect 值中剥离它们避免回跳时残留就绪门控登录后并行加载工作区数据loadSubscription、fetchWorkspaceIamPolicy、loadWorkspaceList、listRoles、分组批量拉取加载完成前显示 spinner认证路由signin、OAuth/OIDC 回调、MFA 等绕开就绪门控——设计文档特别指出若在加载期间隐藏认证页面会在登录中途卸载 OAuth 回调页login()翻转isLoggedIn恰好触发加载导致首屏白屏会话轮询以 1 分钟dev/ 5 分钟prod间隔调用fetchCurrentUser(true)重新校验会话并伴随刷新 IAM policy 与角色镜像旧AuthContext.vue的CHECK_AUTHORIZATION_INTERVAL跨标签页切换通过currentUserName变化检测另一标签页的用户切换若非自我邮箱更新isSelfEmailUpdate则重定向到工作区根并弹出通知。4.3 守卫beforeEach 的 13 步忠实移植设计文档要求守卫“逐行、同序、同重定向”地复刻旧守卫落地形态是根路由 loader根路由匹配所有 URLloader 每次导航都执行命中分支则throw redirect()React 侧为返回redirect()Response放行则返回null。当前仓库的实现分两层frontend/src/app/AppRoot.tsx 的rootLoader用matchRoutes解析命中叶子路由的handle.name再委托给rootGuard并把 loader 挂到routes[0]frontend/src/app/router/guard.ts 的rootGuard({ name, url })读取useAppStore.getState()按设计文档列出的顺序逐分支执行。对照设计文档列出的 13 步逐一对应到guard.ts的实现分支#设计文档步骤guard.ts 实现1同路由循环守卫由LeaveGuardBlockerRootLayout 内在导航侧处理next(false)取消2403/404 直接访问toName WORKSPACE_ROUTE_403/404且路径匹配 → 放行3OAuth/OIDC 回调直接访问AUTH_OAUTH_CALLBACK_MODULE/AUTH_OIDC_CALLBACK_MODULE→ 放行4OAuth2 consentOAUTH2_CONSENT_MODULE→ 放行需登录但不重定向走人5登录态下允许 2FA 设置/密码重置/资料初始化isLoggedIn (AUTH_2FA_SETUP / AUTH_PASSWORD_RESET / AUTH_SETUP)→ 放行6模块回跳路径追踪由resolveRootRedirect读取 localStorage 的最近访问列表实现DummyRootView等价逻辑7登录态访问认证路由 → 重定向relay_state/redirectisAuthRelatedRoute isLoggedIn !unauthenticatedOccurred→ 重定向且校验relay_state只接受相对路径、拒绝//协议相对地址以防范开放重定向8认证路由的仓库重置resetDatabases()/resetInstances()/resetProjects()并异步重置 AI conversation 仓库9未登录 → signin带 redirect querybuildSigninRedirectQuery(url)→redirect(AUTH_SIGNIN_MODULE query)10强制 2FAhasFeature(FEATURE_TWO_FA) requireMfa !mfaEnabled→ 重定向 2FA 设置11强制密码重置requireResetPassword()→ 重定向密码重置页12允许路由模式ALLOWED_ROUTE_PATTERNS前缀匹配account/environment/instance/project/database/setting/workspace/sql-editor→ 放行13404 兜底未命名匹配布局/重定向壳放行具名未知路由 →redirect(WORKSPACE_ROUTE_404)两个额外分支值得注意AUTH_SIGNUP在disallowSignup限制开启时重定向回 signin裸工作区根/没有自己的页面按DatabaseChangeMode.EDITOR跳 SQL Editor 首页、否则跳最近一次有意义访问、再否则跳 landing——这是设计文档中“模块回跳路径追踪”与根路径处理的合并落地。守卫测试同样齐备frontend/src/app/router/guard.test.ts 覆盖了未登录→signin、登录态访问认证路由→首页、2FA 强制、密码重置强制、403/404、OAuth 回调放行、consent 等分支与设计文档的验证清单一一对应。五、路由表、79 座桥梁与反向岛屿5.1 路由表平移~102 条路由的机械翻译设计文档给出了约 102 条路由、73 个页面名的规模。翻译规则高度机械布局 → 布局路由Outlet /叶子路由 →lazy: () import(/react/pages/.../X)取代component: ReactPageMount, props: { page: X }的岛屿挂载与import.meta.glob注册表既有路由名常量AUTH_SIGNIN_MODULE、WORKSPACE_ROUTE_*等保留挂为handle: { name }让守卫、useRecentVisit、模块回跳路径追踪无需改动继续工作。路由名常量集中维护在 frontend/src/app/router/handles.ts并在 frontend/src/app/router/index.ts 统一 re-export使 React 侧消费方保持从/app/router导入不变。handle上还承载了额外的每路由元数据RouteHandlerequiredPermissionList、title、overrideDocumentTitle由 frontend/src/app/router/index.ts 的assembleRoute聚合成ReactRoute——它的字段刻意镜像 vue-router 的route.metameta: Recordstring, unknown让旧currentRoute.value.meta.*读取在新壳下继续工作。5.2 lazyPage 适配器把路由数据重新注入页面这里有一个设计文档未展开、但落地时必须解决的兼容问题React 页面组件当年是按 Vue 层“以 props 注入路由数据”的方式编写的而 react-router 渲染Component时不传任何 props。解决方式是 frontend/src/app/router/lazyPage.ts 的lazyPage(loader, pick)适配器useParams()→ 把:paramprojectId、instanceId、databaseName、issueId…作为 props 传给页面。源码注释明确记载缺了这一步项目 issue 仪表盘会用undefined拼出project:projectIdscope 而崩溃routeName叶子handle.name、routeQuery解析后的查询对象按原始 search 字符串记忆化以保持引用稳定、routeHash→ 页面据此决定展示哪个 phase/stage/task/spec镜像 Vue 的route.name/route.query并支持规范重定向中的次级锚点参数合并取叶子匹配matches.at(-1)?.params因为 Plan Detail 这类页面可能由持久父路由持有而叶子路由贡献选择参数不声明这些 props 的页面自动忽略多余属性保证兼容。这一层正是“把 ~110 处命令式调用点与 79 座桥梁从 Vue 反应式体系上解绑”的承重墙。router对象在 frontend/src/app/router/index.ts 被实现为一个模块级 vue-router 实例的 drop-in 替身push/replace/resolve/back/go/isReady/currentRoute.value/beforeEach/afterEach/getRoutes全部保留原语义beforeEach注册的守卫由runBeforeEachGuards在useBlocker中执行旧消费方只需改 import 路径即可通过拆迁期。5.3 79 座 useVueState 桥梁迁移中最大的机械面设计文档指出约 79 个useVueState桥梁各自读取一个即将被删除的 Vue 反应式数据源因此必须逐一改写vue-router 读取useRoute/router/currentRoute→useParams/useLocation/useNavigate或 drop-inrouter对象Pinia 认证读取 → 应用仓库的认证选择器应用壳 Pinia 桥读取DashboardFrameShell遗留引导 syncSubscriptionToPinia→ 直接改为应用仓库选择器Vue 壳删除后这些桥本身也被删除。随后删除useVueState.ts、Vue 反应式原语如ProjectSidebar中的effectScope以及页面挂载/glob 注册表。这 ~79 个文件的清扫是单 PR 体积最大的机械部分也是设计文档明确“PR 很大”的主要原因。从当前仓库看frontend/src/app/router/index.ts中保留的 drop-inrouter对象与useCurrentRoute()钩子正是这轮清扫留下的兼容接口——大量既有调用点约 110 处命令式调用与useSyncExternalStore订阅都挂在它们之上。5.4 vue-i18n → react-i18next 与 pev2 反向岛屿国际化vue-i18n5 个导入方整体切换到 React 侧已经在用的 react-i18next当前依赖为i18next ^26.0.10react-i18next ^17.0.0见 frontend/package.jsonReactPageMount的 locale 同步 watcher 随之移除。pev2 反向岛屿Postgres 查询计划可视化PostgresPlanView.tsx目前以createApp(pev2)形式独立挂载需要在本次 PR 中用 React 重写这是唯一真正全新编写的 UI隔离在自己的 commit 中。当前仓库 frontend/package.json 中pev2: 1.23.0与vue: 3.5.40仍在——从依赖结构可以推断这条“反向岛屿”尚未随本 PR 移除vue依赖目前只服务于它这也与设计文档把 pev2 重写列为最后一步的顺序一致。六、单 PR 内部构建顺序每步可构建、可评审设计文档把 big-bang PR 拆成 7 个内部步骤保证任何一步被切出时树都能编译、能评审引入react-router编写嵌套路由表 handle.namelazy导入暂不接到根上移植认证 slice方法体原样 守卫 loader AuthGate 重定向拦截器构建RootLayout 3 个 React 布局路由切换main.ts到单一 React 根RouterProvider删除createApp(App)#react-app清扫 79 个useVueState桥梁 → React Router hooks / 应用仓库选择器删除useVueState Vue 反应式原语 glob 注册表用 React 计划可视化替换 pev2删除 Vue 壳App.vue、AuthContext.vue、3 布局、5 挂载桥、ReactPageMount、Pinia 认证仓库、旧守卫 移除vue/vue-router/pinia/vue-i18n/vueuse/vitejs/plugin-vue/vue-tsc 清理 Vite 配置 新增check-react-no-pinia.mjs门禁脚本。对照当前仓库步骤 1–5 已基本完成AppRoot.tsx/RootLayout.tsx/AuthGate.tsx/router/均已就位Vue 壳文件与上述依赖已从frontend/移除步骤 6 尚未落地pev2/vue仍在依赖中步骤 7 的部分内容依赖清理随实现推进已经完成。七、验证门禁“零破坏性变更”的兑现方式设计文档为“零破坏”承诺定义了可执行验证清单当前仓库可以逐一对应到实际资产守卫单元测试镜像beforeEach的每个分支——未登录→signin、登录态访问认证路由→首页、2FA 强制、密码重置强制、403/404、OAuth 回调放行、consent。对应 frontend/src/app/router/guard.test.ts此外还有 frontend/src/app/AuthGate.test.tsx会话门控、frontend/src/app/router/routes.test.tsx路由表、frontend/src/app/router/lazyPage.test.tsxlazy 适配器等。全量 type-check / check / test页面本身是未改动的 React因此既有测试设计文档记载为 2308 条全部继续成立前端入口命令见 frontend/package.json 的 scriptspnpm test走scripts/run-gate.mjspnpm release走 Vite 构建。关键流程手工 QA登录、登出、会话过期401→surface、OAuth/OIDC 回调、2FA 设置、密码重置、深链受保护路由→signin→回跳、工作区切换、SQL 编辑器入口。会话过期与闲置提醒的表面积聚在 frontend/src/app/components/SessionExpiredSurface.tsx 与AuthGate的InactiveRemindModal。历史/返回按钮一致性createBrowserRouter对应旧createWebHistory实现侧除滚动恢复外还在 frontend/src/app/router/index.ts 用routerGo/subscribeRoute还原了back/afterEach语义。八、风险与缓解、超范围事项风险与缓解设计文档原文归纳守卫最高风险逐字移植 单元测试 认证流程 QApev2 React 重建唯一全新 UI隔离提交 截图对比79 文件清扫机械性工作由 type-check 既有页面测试兜住两阶段回滚安全按内部顺序构建每个 commit 都能通过类型检查。超范围事项projectIamPolicyPinia 仓库其桥梁在权限/工作区层完成迁移前保持不变独立、既有工作——这与当前仓库中vue/pev2仍存留的现状互为印证任何页面级 UI/行为变更本次只做宿主替换。结语Bytebase 的这次“Vue → React 路由/壳/认证”迁移示范了一条可复制的框架主权切换路径页面层早已同构的前提下用数据路由的 loader 承接守卫语义、用handle承接路由元数据、用命令式 drop-in 路由器对象承接存量调用点、用惰性加载适配器承接页面 props 契约最终以一次 big-bang PR、7 步内部提交完成整体替换。从当前仓库的实现看设计文档所规划的架构与约束单一 React 根、应用仓库为会话唯一事实来源、13 步守卫忠实移植、零破坏性变更验证门禁均已逐条落地唯一遗留的 pev2 反向岛屿也在依赖清单中清晰可见。对于同样身陷双框架过渡期的团队这套“设计先行、机械平移、逐行移植、验证门禁”的组合拳值得直接借鉴。如需深入可继续阅读设计文档、路由组装、守卫实现、认证 slice、会话门控、路由兼容层。【免费下载链接】bytebaseDatabase governance built for humans and agents — controlling changes and access across every major database.项目地址: https://gitcode.com/GitHub_Trending/by/bytebase创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联 返回资讯列表 →