ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Bytebase 前端 Vue→React 迁移实战:Project Release 详情页的 Strangler Fig 改造设计与落地

Bytebase 前端 Vue→React 迁移实战:Project Release 详情页的 Strangler Fig 改造设计与落地 Bytebase 前端 Vue→React 迁移实战Project Release 详情页的 Strangler Fig 改造设计与落地【免费下载链接】bytebaseDatabase governance built for humans and agents — controlling changes and access across every major database.项目地址: https://gitcode.com/GitHub_Trending/by/bytebase导读本文以 2026-04-22-project-release-detail-react-migration 设计文档为主体完整还原 Bytebase 前端将「项目 Release 详情页」PROJECT_V1_ROUTE_RELEASE_DETAIL从 Vue 迁移到 React 的技术方案包括路由层的绞杀者Strangler Fig接缝选择、自包含 React 页面的数据流设计、共享叶子组件ReleaseFileTable的双实现共存策略、i18n 与测试落地以及最后的 Vue 文件删除计划。读完本文你将掌握一条可复用的单路由级 Vue→React 渐进迁移方法论如何在不停机、不重写的情况下把一条路由一个页面原子地替换为 React 实现并处理好跨子树依赖的共存问题。一、迁移背景release 表面的半 React 半 Vue状态Bytebase 前端正按照 React 迁移 playbook 从 Vue 渐进迁移到 React。在迁移本页面之前release 这一业务面已经处于部分迁移状态列表页已完成迁移ProjectReleaseDashboardPagefrontend/src/react/pages/project/ProjectReleaseDashboardPage.tsx已经取代旧的 Vue releases 看板通过ReactPageMount.vue挂载即PROJECT_V1_ROUTE_RELEASES路由。详情页仍是 Vue用户在看板中点击一行进入的详情页仍然由frontend/src/components/Release/ReleaseDetail/ReleaseDetail.vue这个 Vue 组件树承载路由直接挂载/components/Release/ReleaseDetail/而非走 React 挂载约定。1.1 被迁移的 Vue 页面树8 个文件约 730 LOC根据 definition.md 中的盘点旧的 Vue 实现由以下文件组成文件LOC职责frontend/src/components/Release/ReleaseDetail/index.ts3ReleaseDetail.vue的默认导出frontend/src/components/Release/ReleaseDetail/ReleaseDetail.vue74页面根组件提供ReleaseDetailContext负责文档标题与文件抽屉状态frontend/src/components/Release/ReleaseDetail/context.ts68provideReleaseDetailContext——通过useProjectV1Store/useReleaseStore拉取 project release用hasPermissionToCreateChangeDatabaseIssueInProject推导allowApplyfrontend/src/components/Release/ReleaseDetail/BasicInfo.vue50创建时间 VCS 来源行frontend/src/components/Release/ReleaseDetail/NavBar/index.ts3Barrel 导出frontend/src/components/Release/ReleaseDetail/NavBar/NavBar.vue82标题、应用按钮、abandon/restore 下拉、删除态ArchiveBannerfrontend/src/components/Release/ReleaseDetail/NavBar/ApplyToDatabaseButton.vue25打开应用面板frontend/src/components/Release/ReleaseDetail/NavBar/ApplyToDatabasePanel.vue155带DatabaseAndGroupSelector的抽屉创建 plan rollout 并跳转buildPlanDeployRoutefrontend/src/components/Release/ReleaseDetail/ReleaseFileTable/index.ts-Barrel 导出frontend/src/components/Release/ReleaseDetail/ReleaseFileTable/ReleaseFileTable.vue155NDataTable渲染Release_Fileemitrow-click/update:selected-filesfrontend/src/components/Release/ReleaseDetail/ReleaseFileTable/ReleaseFileDetailPanel.vue80通过sheetServiceClientConnect.getSheet拉取 sheet在 Monaco 中渲染1.2 关键约束跨子树依赖Vue 组件树中有一个叶子被外部导入frontend/src/components/Revision/CreateRevisionDrawer.vue:414导入了ReleaseFileTable.vue在 299、328、414 行分别以选择/只读两种模式渲染两次。而CreateRevisionDrawer.vue不在本次迁移范围内。这个耦合直接决定了迁移策略ReleaseFileTable.vue不能删必须与新的 React 实现共存一段时间直到 revision 抽屉也完成迁移。这正是 playbook删除规则只有没有任何剩余调用方的 Vue 文件才能删除的典型应用场景。二、行业基线为什么是 Strangler Fig而不是重写设计文档明确引用了两个业界基准Martin Fowler 的 StranglerFigApplication绞杀者模式在接缝处逐个替换组件新旧共存过渡不做 big-bang 重写。对于本页面接缝就是Vue router 入口 页面的数据输入。React 官方两条指南共同收窄了设计空间Passing Data Deeply with ContextContext 是最后手段。本页面只有单一消费者子树、仅共享三个值直接用 props 传递不需要createContext。You Might Not Need an Effect页面级数据获取使用 Effect 是合法的但必须带清理标志let ignore false; … return () { ignore true; }处理竞态单一资源获取不需要引入 TanStack Query。仓库内最接近的参照是先例Plan Detail 迁移2026-04-13-project-plan-detail-react-migration.md#20081它通过ReactPageMount.vue路由、直接从 React 侧消费 Pinia store、用useEffectuseVueState观察 Vue 响应式状态并让 Vue 树存活到所有叶子调用方消失。其 ~600 LOC 页面 plan-detail/子树的取舍对 ~4000 LOC 的表面积是合适的而本页面仅 ~730 LOC设计上选择更轻量的单文件方案。三、设计目标六条可验证的验收标准设计文档给出了按优先级排序的六个目标每一条都附带明确的验证手段与 Vue 页面行为对等归档横幅已删除态、apply 抽屉创建 planrollout 并跳转buildPlanDeployRoute、abandon 确认后删除、restore 撤销删除、文件行点击打开 Monaco 渲染的 sheet 详情面板、路由匹配时重置文档标题。验证方式迁移前后对照 Vue 树手工走查。被替换的 Vue 文件在合并后无存活调用方ReleaseFileTable.vue除外用rg对ReleaseDetail.vue、BasicInfo.vue、NavBar.vue、ApplyToDatabaseButton.vue、ApplyToDatabasePanel.vue、ReleaseFileDetailPanel.vue、context.ts的导入做全仓库扫描命中数应为 0。路由入口与兄弟路由PROJECT_V1_ROUTE_RELEASES对等PROJECT_V1_ROUTE_RELEASE_DETAIL用component: () import(/react/ReactPageMount.vue)props: (route) ({ page: ProjectReleaseDetailPage, ...route.params })注册用grep验证。不引入新的共享 React 状态抽象无createContext、无新 zustand store、无新 tanstack/query 入口Pinia 是唯一的 release 数据 IO。React 语言包 i18n 全覆盖pnpm --dir frontend check与check-react-i18n通过React 页面所有 locale key 走useTranslation()。前端 CI 门禁全绿pnpm --dir frontend fix、check、type-check、test全部返回 0。四、非目标Non-Goals迁移的边界在哪里设计文档用两份清单划定了边界其中研究过程中新发现的部分尤其体现了对 React 官方指南的遵循继承自 definition 的非目标不删除ReleaseFileTable.vueCreateRevisionDrawer.vue:414仍在使用不迁移CreateRevisionDrawer.vue或任何 revision 表面不把ArchiveBanner.vue、VCSIcon.vue、HumanizeDate.vue、EllipsisText.vue、/components/v2的Drawer/CopyButton/DrawerContent移植为通用 React 原语只做本页面所需的最小量不抽取共享的 ReactDatabaseAndGroupSelector复用现有内联副本不改变 apply RPC 序列或buildPlanDeployRoute调用不重新设计页面布局、文案或流程不为 abandon/restore 增加新的权限校验不修改PROJECT_V1_ROUTE_RELEASE_DETAIL名称常量及其辅助调用点。研究过程中发现并追加的非目标不为此表面引入 TanStack Query 或 zustandplaybook §State and Data GuidanceReact 官方建议单资源获取用自定义 hook 而非库不用React.createContext包裹 release / project / allowApplyReact — Passing Data Deeply with Context。五、方案设计从路由接缝到删除计划的完整链路5.1 路由改写把接缝切在 router 入口frontend/src/router/dashboard/projectV1.ts:512-520的旧注册被替换为{ path: :releaseId, name: PROJECT_V1_ROUTE_RELEASE_DETAIL, meta: { requiredPermissionList: () [bb.releases.get], }, component: () import(/react/ReactPageMount.vue), props: (route: RouteLocationNormalized) ({ page: ProjectReleaseDetailPage, ...route.params, }), },这与兄弟路由PROJECT_V1_ROUTE_RELEASES第 500-511 行的注册方式一致满足设计目标 3。这一改动是原子性的路由层面完成换页而文件表格叶子继续为CreateRevisionDrawer服务——这正是 Strangler Fig 模式中一个接缝、一次替换的体现。5.2 React 页面入口自包含的ProjectReleaseDetailPage新文件frontend/src/react/pages/project/ProjectReleaseDetailPage.tsx导出签名export function ProjectReleaseDetailPage({ projectId, releaseId, }: { projectId: string; releaseId: string; }) { ... }mount.ts:59-77会按照./pages/project/ProjectReleaseDetailPage.tsx的路径约定解析page: ProjectReleaseDetailPage无需修改注册表。页内职责每一条都对应 Vue 原实现的具体行为内联拼接资源名逐字移植自context.ts:42-44releaseName \${projectNamePrefix}${projectId}/releases/${releaseId}projectName \${projectNamePrefix}${projectId}在同一个useEffect带ignore标志中通过useProjectV1Store().getOrFetchProjectByName(projectName)拉取项目通过useReleaseStore().fetchReleaseByName(releaseName)拉取 release用useVueState(() releaseStore.getReleaseByName(releaseName) ?? unknownRelease())及对应的 project getter 观察 Vue 响应式状态让 Pinia 更新流入 React与 plan-detail 先例一致用hasPermissionToCreateChangeDatabaseIssueInProject(project)计算allowApply该 helper 是纯 TSReact 可直接调用在挂载路由名匹配PROJECT_V1_ROUTE_RELEASE_DETAIL时通过观察project.title的useEffect重置文档标题对应ReleaseDetail.vue:65-73用useStateRelease_File | undefined持有selectedReleaseFile驱动文件详情 sheet对应ReleaseDetail.vue:46-53。全程不引入React.createContext值一律通过 props 传给子组件。5.3 页内组件分解单文件优先按需拆分按 definition 对开放问题 2 的默认答案页面文件保持在 ~500 LOC 以内超过才拆release-detail/components/子树。单文件内用局部组件组织React 局部组件取代实现要点ReleaseHeaderNavBar.vueArchiveBanner条件渲染标题、ApplyToDatabaseButton、含 abandon/restore 的DropdownMenu。用/react/components/ui/{dropdown-menu,alert-dialog,button}abandon 流程用AlertDialogfrontend/AGENTS.md §Dialog vs Sheet 规定破坏性确认用 Dialog归档横幅就地渲染bg-warning/10 text-warning等不新建共享组件非目标。ReleaseBasicInfoBasicInfo.vue用HumanizeTs/react/components/HumanizeTs。VCS 来源行用局部VCSLinkRow仅在无 React VCS 图标等价物时才通过VueComponent复用 Vue 的VCSIcon.vue当前没有 React VCS 图标所以用 lucide 的Link2 文案即可达到对等。ApplyToDatabaseSheetApplyToDatabaseButton.vueApplyToDatabasePanel.vueSheet widthwide开放问题 3 的默认值。复用DatabaseAndGroupSelector时在本页面文件内联第四份副本非目标不抽共享组件。RPC 序列逐字移植自ApplyToDatabasePanel.vue:103-154。底部按钮组按 frontend/AGENTS.md 使用gap-x-2。5.4 共享叶子ReactReleaseFileTable新文件frontend/src/react/components/release/ReleaseFileTable.tsx。为什么放在react/components/release/而不是内联在页面文件里开放问题 5 的默认答案是——它未来会被CreateRevisionDrawer迁移时复用现在给 React 一个归宿可以避免将来再内联这也符合 plan-detail 用独立components/目录的惯例。Props 完整镜像 Vue 的 emits 模型type Props { files: Release_File[]; releaseType: Release_Type; showSelection?: boolean; // default false rowClickable?: boolean; // default true selectedFiles?: Release_File[]; // default [] onRowClick?: (file: Release_File) void; onSelectedFilesChange?: (files: Release_File[]) void; };实现要点表格用/react/components/ui/table选择列渲染 shadcnCheckbox行点击在选择模式下切换选中否则调用onRowClick对应ReleaseFileTable.vue:104-137类型文本 helpergetReleaseFileTypeText逐字移植为局部函数。Vue 版ReleaseFileTable.vue不删除非目标 playbook §删除规则——CreateRevisionDrawer.vue:414仍导入它。两个实现并存这与 playbook 中BytebaseLogo/UserPassword的共存先例是同一模式。5.5 文件详情面板Monaco 竞态保护的 sheet 拉取新文件frontend/src/react/components/release/ReleaseFileDetailPanel.tsx与ReleaseFileDetailPanel.vue1:1 对齐Monaco 用/react/components/monaco/ReadonlyMonaco已在其他 React 页面使用覆盖readonly、auto-heightsheet 内容拉取sheetServiceClientConnect.getSheet({ name, raw: true })放在带ignore清理标志的useEffect中竞态语义遵循 React — You Might Not Need an Effect复制按钮复用frontend/src/react/components/revision/RevisionDetailPanel.tsx:46已有的内联CopyButton模式非目标不抽共享 ReactCopyButton。面板渲染在Sheet widthwide中开放问题 4 的默认目标是抽屉形状的只读语句查看器Monaco 占主导用wide而非narrow由页面的selectedReleaseFile状态驱动。5.6 数据流总览Vue router └─ ReactPageMount.vue (pageProjectReleaseDetailPage) └─ ProjectReleaseDetailPage (fetch, useState, useVueState) ├─ ReleaseHeader (release, project, allowApply, onArchive, onRestore) │ ├─ AlertDialog (abandon confirm) │ └─ ApplyToDatabaseSheet (release, project) ├─ ReleaseBasicInfo (release) ├─ ReleaseFileTable (files, releaseType, onRowClick) └─ Sheet (selectedReleaseFile) └─ ReleaseFileDetailPanel (release, releaseFile)无 Context。所有值通过 props 传递Pinia 是 release 与 project 状态的唯一事实源useVueState负责把更新桥接进 React。5.7 i18n五种语言包的 key 补齐所有 key 已存在于frontend/src/locales/本迁移只需把被该表面消费的子集复制进 React 语言包frontend/src/react/locales/{en-US,es-ES,ja-JP,vi-VN,zh-CN}.jsonrelease.releasescommon.version、common.type、common.apply-to-database、common.abandon、common.restore、common.confirm、common.cancel、common.create、common.statementdatabase.revision.filename、database.selected-n-databasesissue.title.change-databasebbkit.confirm-button.sure-to-abandon、bbkit.confirm-button.can-undo执行计划plan.md §T4给出了部分新增 key 的逐语言译文Keyen-USes-ESja-JPvi-VNzh-CNcommon.versionVersionVersiónバージョンPhiên bản版本common.apply-to-databaseApply to databaseAplicar a la base de datosデータベースに適用Áp dụng cho cơ sở dữ liệu应用到数据库common.abandonAbandonAbandonar破棄Hủy bỏ废弃database.revision.filenameFilenameNombre del archivoファイル名Tên tệp文件名issue.title.change-databaseChange databaseModificar base de datosデータベースの変更Thay đổi cơ sở dữ liệu变更数据库bbkit.confirm-button.sure-to-abandonAre you sure to abandon?¿Estás seguro de abandonar?本当に破棄しますか?Bạn có chắc chắn muốn hủy bỏ?确定要废弃吗bbkit.confirm-button.can-undoYou can undo this action later.Podrás deshacer esta acción más tarde.この操作は後で元に戻すことができます。Bạn có thể hoàn tác hành động này sau.此操作可以撤销。check-react-i18n负责校验完整性设计目标 5。common.restore、common.cancel、common.confirm等已存在的 key 不重复添加。5.8 测试策略页面级契约 叶子测试页面级测试ProjectReleaseDetailPage.test.tsx设计文档 §8 的设计方案传入projectIdreleaseIdrelease 状态ACTIVE断言标题、apply 按钮、abandon 下拉项、无归档横幅状态DELETED归档横幅出现下拉只有 restore点击文件行打开带文件名的 sheetApply 流程mockplanServiceClientConnect.createPlanrolloutServiceClientConnect.createRolloutrouter.push断言三者按顺序以预期 payload 被调用。叶子测试ReleaseFileTable.test.tsx——选择、行点击、showSelectionfalse时隐藏选择列ReleaseFileDetailPanel.test.tsx——mocksheetServiceClientConnect.getSheet断言 Monaco 收到解码后的内容、复制按钮反映同一内容。测试遵循 playbook §Testing Guidance测试 wrapper 的契约、mock 仓库自有的接缝*ServiceClientConnect不碰 Monaco 内部实现。5.9 删除计划只有调用方清零的 Vue 文件才删除路由切到 React 且用rg验证所有调用方后在同一个 PR中删除frontend/src/components/Release/ReleaseDetail/index.tsfrontend/src/components/Release/ReleaseDetail/ReleaseDetail.vuefrontend/src/components/Release/ReleaseDetail/context.tsfrontend/src/components/Release/ReleaseDetail/BasicInfo.vuefrontend/src/components/Release/ReleaseDetail/NavBar/整个目录frontend/src/components/Release/ReleaseDetail/ReleaseFileTable/ReleaseFileDetailPanel.vue保留frontend/src/components/Release/ReleaseDetail/ReleaseFileTable/ReleaseFileTable.vuefrontend/src/components/Release/ReleaseDetail/ReleaseFileTable/index.ts验证命令rg Release/ReleaseDetail frontend/src——只应命中ReleaseFileTable的存活调用方。5.10 上线策略一个 PR、无特性开关、路由级回滚采用与 plan-detail#20081和 release-dashboard 先例一致的方式单 PR、单提交链、不挂 feature flag。回滚就是路由入口 revert 加叶子恢复爆炸半径始终控制在路由层。六、仓库落地现状设计与实现的印证从当前仓库源码看该设计已经落地这也印证了设计目标全部可验证的价值路由注册PROJECT_V1_ROUTE_RELEASE_DETAIL现注册于 frontend/src/app/router/routes/dashboard.tsx通过lazyPage(() import(/routes/project/ProjectReleaseDetailPage), (m) m.ProjectReleaseDetailPage)懒加载requiredPermissionList: () [bb.releases.get]与设计文档一致兄弟路由PROJECT_V1_ROUTE_RELEASES也同处一个文件第 883 行起。页面组件frontend/src/routes/project/ProjectReleaseDetailPage.tsx285 行完整实现了设计中的自包含页面导出签名第 37-43 行、带cancelled清理标志的拉取useEffect第 59-78 行、按project.title重置文档标题的useEffect第 80-84 行、归档横幅 DropdownMenuarchive/restoreReleaseBasicInfoReleaseFileTable 文件详情SheetAlertDialog确认第 143-232 行。共享叶子frontend/src/components/release/ReleaseFileTable.tsx161 行的 Props 契约与设计 §4 基本一致且增加了onRowClick?: (file, e: MouseEvent) void的 event 参数选择模式/行点击/类型文本DECLARATIVE → SDL、VERSIONED → issue.title.change-database均按设计实现。详情面板frontend/src/components/release/ReleaseFileDetailPanel.tsx83 行实现了带竞态保护的 sheet 拉取第 21-41 行sheetServiceClientConnect.getSheet({ name, raw: true })new TextDecoder().decode(sheet.content)ReadonlyMonaco渲染语句内容并展示sheetSha256前 8 位哈希。叶子测试frontend/src/components/release/ReleaseFileTable.test.tsx120 行的 3 个用例渲染单元格 / 选择列显隐 / 行点击回调与设计 §8 一致采用IS_REACT_ACT_ENVIRONMENTcreateRootact的真实渲染而非浅渲染并 mockreact-i18next的t。需要指出的是落地过程中实现细节与设计文档存在少量演进这些属于设计落地时的合理偏差路由基础设施从设计时的ReactPageMount.vuepage字符串约定演化为lazyPage直接懒加载页面组件状态获取从useReleaseStore/useProjectV1Store模块演化为统一的useAppStore/stores/app提供fetchRelease、deleteRelease、undeleteRelease、getReleaseByName加useProjectByNamehook破坏性操作文案从 abandon 演进为 archive页面使用common.archive/common.confirm-archive/common.archive-description设计时不存在的 React 原语CopyButton、Checkbox现已成为frontend/src/components/ui/copy-button.tsx、frontend/src/components/ui/checkbox.tsx并被直接复用。这些偏差恰好验证了设计文档把非目标写得足够清晰的价值只要数据流形态Pinia 唯一事实源 props 传递 无 Context、路由级接缝、删除规则三类核心决策不变实现层的小幅演进不会破坏整体架构。七、总结一次可复制的单路由 React 迁移范本这条迁移路径值得沉淀的工程要点可以归纳为四条接缝越小回滚越安全把替换点收敛在路由入口一次 PR 原子换页回滚也只是路由 revert遵循框架官方建议减少依赖膨胀单资源获取用 Effect 清理标志不引入 query 库三个值的共享用 props不用 Context删除规则比一步到位更重要跨子树依赖的叶子ReleaseFileTable允许新旧并存直到最后一个 Vue 调用方迁移完毕目标必须可验证六条设计目标全部附带了rg/grep/CI 命令级别的验证手段使得迁移完成不是一个主观判断而是一组可以机器执行的检查。对于 Bytebase 这类正从 Vue 全面迁移到 React 的大型前端仓库本文记录的方案——从 route seam 的选择、页面数据流的组织到共享叶子的共存策略与删除计划——可以作为后续其他页面迁移的通用操作手册。【免费下载链接】bytebaseDatabase governance built for humans and agents — controlling changes and access across every major database.项目地址: https://gitcode.com/GitHub_Trending/by/bytebase创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表