资讯动态

Bytebase SQL Editor React 迁移 Stage 3:GutterBar/TabItem 组件化与 Pinia 桥接实战

发布时间:2026/9/15 17:37:11 来源:尧图企业网站定制
Bytebase SQL Editor React 迁移 Stage 3GutterBar/TabItem 组件化与 Pinia 桥接实战【免费下载链接】bytebaseDatabase governance built for humans and agents — controlling changes and access across every major database.项目地址: https://gitcode.com/GitHub_Trending/by/bytebase导读本文基于 Bytebase 仓库中的官方实施计划文档 2026-04-20-sql-editor-react-migration-stage-3.md 与其配套设计规格 2026-04-20-sql-editor-react-migration-stage-3-design.md完整还原 SQL Editor 左侧 GutterBar图标工具条从 Vue 迁移到 React 的第三阶段实施过程。该阶段的核心技术价值在于它是整个迁移路线图中第一个由 React 组件通过useVueState对 Pinia store 做响应式读取的消费方打通了 React ↔ Vue ↔ Pinia 双向数据流。读完本文你将掌握React 孤岛island挂载模式、useVueState桥接原理、TDD 驱动的组件迁移流程、跨 5 种语言环境的 i18n 键补齐与校验以及一套可复用的替换—删除—全量验证的 Vue 迁移方法论。一、Stage 3 的目标与边界1.1 要做什么Stage 3 的目标是把AsidePanel/GutterBar/这个 Vue 子系统约 136 行、4 个文件迁移为一对 React 组件组件职责GutterBar.tsx容器渲染 Logo 链接 分隔线 4 个条件渲染的TabItem零 propsTabItem.tsx单个图标 Tooltip按钮接收{ tab, onClick }props通过useVueState读取asidePanelTab计算激活态样式整个子系统变成一个单一 React 挂载点AsidePanel.vue第 8 行的GutterBar sizemedium /替换为ReactPageMount pageGutterBar /React 与 Vue 的边界收敛到这一个挂载点之下不再有更细粒度的跨框架通信。1.2 明确不做的事Non-goals设计文档用 Non-goals 划清了本阶段的边界防止范围蔓延不迁移AsidePanel.vue本身——父壳保持 VueGutterBar 只是它的一个子组件不迁移其他 aside-panel 子树SchemaPane、WorksheetPane、HistoryPane、AccessPane不移植 Vue 的useButtoncomposable——激活态样式直接内联决策编号 Q2a不支持small/large尺寸——只有唯一调用方传了medium遵循 YAGNI 原则直接砍掉 size prop不移植自定义DatabaseIcon.vue——改用lucide-react自带的Database图标决策编号 Q3.2a。这些决策的共性是一条工程原则只为当前真实消费者做最简实现YAGNI把自定义图标、尺寸变体、hook 抽象都推迟到真正出现第二个消费方时再做。二、架构React 孤岛挂载与 Pinia 桥接2.1 状态流全景Stage 3 的数据流分为三条线全部围绕 Stage 2 引入的useSQLEditorUIStorePinia setup store展开需求方数据来源React 获取方式asidePanelTabTabItem 激活态READuseSQLEditorUIStore().asidePanelTabuseVueState(() uiStore.asidePanelTab)响应式订阅asidePanelTabGutterBar 点击WRITE同一 store直接赋值uiStore.asidePanelTab targetPinia setup store 实例上会自动解包 refJIT 门控用的 projectuseSQLEditorStore().project字符串名→useProjectV1Store().getProjectByName(name)useVueState(() projectStore.getProjectByName(editorStore.project))Logo 链接的目标路由router.currentRoute.value.params.projectuseVueState(() router.currentRoute.value.params.project as string \| undefined)路由名常量PROJECT_V1_ROUTE_DETAIL、WORKSPACE_ROUTE_LANDING直接从/router/dashboard/projectV1、/router/dashboard/workspaceRoutes导入i18n 标签4 个 key见 §4.2useTranslation()2.2 模块级 store 实例的坑设计文档记录了一个 Stage 1 踩过的关键坑不能把use*Store()的调用放进useVueState的回调里否则会触发react-hooks/rules-of-hooks的 lint 报错。正确做法是把 store 实例提升到组件顶层const editorStore useSQLEditorStore(); const projectStore useProjectV1Store(); const uiStore useSQLEditorUIStore(); const project useVueState(() { const name editorStore.project; return name ? projectStore.getProjectByName(name) : undefined; });这是React 中消费 Vue/Pinia 状态必须遵守的铁律任何后续迁移阶段都会沿用这一模式。2.3 为什么这是关键一役设计文档特别强调Stage 3 是 React 对useSQLEditorUIStore的第一次真实响应式 READ。测试套件验证了这一闭环把 store mock 成普通对象断言修改asidePanelTab会触发 TabItem 激活类的重新渲染。这验证了useVueState订阅 Pinia setup-store 字段的能力是整个 React 迁移能否继续进行的前提。Stage 1Welcome 叶子节点和 Stage 2ConnectionHolderuseSQLEditorUIStorePinia 桥只是单向或写方向的验证而 Stage 3 的 READ 方向一旦打通后续 Stage 47 的大规模面板迁移才有地基。三、技术栈与 i18n 键补齐Task 13.1 技术栈清单实施计划开篇列出的技术栈直接决定了代码形态React 18base-ui/reactshadcnButton、Tooltip原语lucide-react图标库react-i18next国际化Pinia 通过useVueState桥接class-variance-authority/cn()类名合并vitest 测试3.2 三个缺失的 i18n keyReact 语言包在 Stage 3 之前的状态common.schema已存在但worksheet.self、common.history、sql-editor.jit三个 key 全部缺失。Task 1 要求把它们以逐字节一致byte-exact的值填入全部 5 个 React locale 文件值必须从对应的 Vue localefrontend/src/locales/*.json拷贝Localeworksheet.selfcommon.historysql-editor.jiten-USWorksheetHistoryJust-In-Time Accesszh-CN工作表历史记录即时访问es-ESHoja de trabajoHistorialJust-In-Time Accessja-JPワークシート履歴Just-In-Time Accessvi-VNBảng tínhLịch sửJust-In-Time Access操作步骤TDD 前置先跑一段 Python 脚本逐语言逐 key 与 Vue locale 交叉核对确认表内值与源码值一致若不一致以源码实际值为准而不是表格然后在各 React locale 文件的父级块内按字母序插入这 3 个 key再运行排序脚本node frontend/scripts/sort_i18n_keys.mjs原地重排最后运行一致性检查脚本node frontend/scripts/check-react-i18n.mjs。校验时机很关键Task 1 结束时3 个 key 因为还没有消费者会被check-react-i18n.mjs标记为 unused 警告——这是预期内的中间态不是错误真正的清零要等 Task 3 完成、TabItem消费这 3 个 key 之后。这正是仓库 scripts/check-react-i18n.mjs 所执行的 1:1 parity 强制React locale 与 Vue locale 的 key 集合必须完全对齐防止迁移过程中漏译。四、TabItem 组件TDD 先行Task 24.1 先写失败的测试Task 2 采用严格的测试驱动开发先创建测试文件运行确认失败Cannot find module ./TabItem再写实现。测试文件 TabItem.test.tsx 的骨架揭示了本仓库 React 测试的统一模式( globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean } ).IS_REACT_ACT_ENVIRONMENT true; const mocks vi.hoisted(() ({ useTranslation: vi.fn(() ({ t: (key: string) key })), useVueState: vi.fn(getter: () unknown) unknown(), useSQLEditorUIStore: vi.fn(), })); vi.mock(react-i18next, () ({ useTranslation: mocks.useTranslation, })); vi.mock(/react/hooks/useVueState, () ({ useVueState: mocks.useVueState, })); vi.mock(/store, () ({ useSQLEditorUIStore: mocks.useSQLEditorUIStore, })); // Stub out Tooltip to render children directly — tooltip positioning is a // primitive concern, not this components. vi.mock(/react/components/ui/tooltip, () ({ Tooltip: ({ children }: { children: React.ReactNode }) {children}/, }));三个值得注意的测试工程细节vi.hoistedmock 工厂函数被提升到 import 之前定义避免 TDZ暂时性死区报错手动createRoot渲染测试不依赖 Testing Library而是直接用react-dom/client的createRootact渲染到真实 DOM并封装了renderIntoContainer辅助函数管理挂载/卸载生命周期Tooltip stubTooltip 定位属于原语层关注点测试里直接 stub 成渲染 children让组件测试聚焦在自身逻辑上。7 个测试覆盖4 种 tab 的标签渲染worksheet.self/common.schema/common.history/sql-editor.jit、匹配时应用激活类、不匹配时不应用、点击触发onClick。4.2 实现细节TabItem.tsx 的实现把配置即数据发挥到极致用两个as const映射表把 tab 类型与图标、i18n key 一一对应const iconByTab { WORKSHEET: FileCode, SCHEMA: Database, HISTORY: History, ACCESS: ShieldCheck, } as const; const i18nKeyByTab { WORKSHEET: worksheet.self, SCHEMA: common.schema, HISTORY: common.history, ACCESS: sql-editor.jit, } as const; export function TabItem({ tab, onClick }: TabItemProps) { const { t } useTranslation(); const uiStore useSQLEditorUIStore(); const isActive useVueState(() uiStore.asidePanelTab tab); const Icon iconByTab[tab]; const label t(i18nKeyByTab[tab]); return ( Tooltip content{label} sideright delayDuration{300} Button variantghost className{cn( size-10 p-0, isActive bg-accent/10 text-accent hover:bg-accent/10 hover:text-accent )} onClick{onClick} aria-label{label} Icon classNamesize-5 / /Button /Tooltip ); }对照设计规格可逐条印证激活态bg-accent/10 text-accent10% 透明度的主色背景 主色图标悬停保持同色调非激活态走 ghost 默认样式hover:bg-control-bg text-control尺寸size-10 p-0对应 Vue composable 的 40×40 px 意图图标size-520 pxTooltipVue 侧是sideright delay{300}React 侧等价传参为sideright delayDuration{300}可访问性aria-label与 Tooltip 内容一致图标按钮对读屏器友好。从当前仓库实际落地的代码看GutterBar.tsx、TabItem.tsx迁移后的组件已进一步演进激活态改为bg-accent/80 text-accent-text实心主色填充 on-accent 文字色并加了sr-only标签与appearancesecondary——说明该方案在后续阶段经历了主题对比度相关的迭代但映射表 条件类名 从 store 订阅激活态的核心骨架与本文档一致。五、GutterBar 容器JIT 门控与路由解析Task 35.1 职责拆分GutterBar是零 props 容器职责有三渲染 Logo 链接顶部 Bytebase Logo/assets/logo-icon.svg 细分隔线渲染 4 个 TabItemWORKSHEET / SCHEMA / HISTORY 恒渲染ACCESS 仅在project.allowJustInTimeAccess为真时渲染JIT 门控写入 store点击时执行uiStore.asidePanelTab target。5.2 Logo 链接router.resolve模式Vue 侧用的是router-link :tolinkTarget target_blankReact 侧改为计算href后渲染普通a——这与仓库中 DashboardSidebar.tsx 既有模式一致const routeProjectParam useVueState( () router.currentRoute.value.params.project as string | undefined ); const logoHref routeProjectParam ? router.resolve({ name: PROJECT_V1_ROUTE_DETAIL, params: { projectId: routeProjectParam }, }).href : router.resolve({ name: WORKSPACE_ROUTE_LANDING }).href; // 渲染 a href{logoHref} target_blank relnoopener noreferrer img classNamew-9 h-auto src{logoIcon} altBytebase / /a逻辑是当前路由带project参数 → Logo 指向项目详情页否则指向工作区落地页。relnoopener noreferrer保证新标签页打开的安全性。5.3 GutterBar 的 4 个测试GutterBar.test.tsx 的 4 个用例分别验证非 JIT 项目渲染 3 个 tab、JIT 项目渲染 4 个 tab、点击写入 store断言store.asidePanelTab SCHEMA、Logo 链接在带 project 参数时的href与target_blank正确性。Mock 策略中有一个值得学习的技巧useVueState在 GutterBar 里会被调用多次project 读取、路由参数读取、每个 TabItem 的激活态判断测试用mockImplementation((getter) getter())让 getter 直接求值从而绕开响应式订阅层、把断言聚焦在纯逻辑上。5.4 尺寸 prop 的删减决策Vue 侧GutterBar.vue的根节点是h-full flex flex-col ... p-1父容器AsidePanel.vue已提供确定尺寸因此ReactPageMount默认的div classh-full可以直接撑满父容器无需额外的 flex wrapper div。设计文档留了一条退路若手动 UX 验证时高度塌陷则回退到 Stage 1 的div classflex-1 flex flex-col min-h-0包裹层。这种默认最简、预案兜底的写法值得迁移类任务借鉴。六、Vue 调用方替换与孤儿代码清理Task 4 56.1 单点替换全仓库只有一处调用frontend/src/views/sql-editor/AsidePanel/AsidePanel.vue。两处改动!-- 第 8 行模板 -- GutterBar sizemedium / → ReactPageMount pageGutterBar / !-- 第 81 行导入 -- import GutterBar from ./GutterBar; → import ReactPageMount from /react/ReactPageMount.vue;导入语句要放进/...绝对路径分组按 Vue 文件约定绝对路径导入放在相对路径之前靠近PROJECT_V1_ROUTE_DASHBOARD、useActuatorV1Store等既有导入。6.2 删除前的调用方清查删除 Vue 目录前必须先做 4 个 grep 模式确认零残留调用唯一的例外是 React 版GutterBar.tsx本身——它是替代品而非调用方grep -rn from.*AsidePanel/GutterBar frontend/src/ grep -rn import.*GutterBar.*from.*AsidePanel frontend/src/ grep -rn GutterBar frontend/src/views/ grep -rn from.*GutterBar/common frontend/src/任何模式在待删文件之外命中立即 STOP 并报告 BLOCKED 及调用方详情——这是防止删了还在被引用的代码导致构建炸掉的安全闸门。清查通过后删除整个目录GutterBar.vue、TabItem.vue、common.ts、index.ts共 4 个文件。6.3 基线错误的概念Task 4/5 的 type-check 预期都写着只允许 6 个预先存在的SchemaEditorLite错误零新增。这是迁移类任务的常见验收标准仓库可能存在历史遗留的类型错误基线验收的关键不是零错误而是相对基线零增量。若删除目录后出现Cannot find module ./GutterBar之类的新错误说明 Task 4 的导入替换漏了需要回溯。七、全量验证与人工 UX 检查Task 67.1 自动化验证链Stage 3 的最终验证串联了 4 条命令形成格式化 → 静态检查 → 类型检查 → 测试的完整流水线命令预期pnpm --dir frontend fix无改动或仅新文件的格式微调pnpm --dir frontend check通过ESLint Biome React i18n i18n sortpnpm --dir frontend type-check恰好 6 个既有SchemaEditorLite错误零新增pnpm --dir frontend test --run既有 1160 个测试 新增 11 个TabItem 7 GutterBar 4→ 1171 全部通过另用git status核对变更清单是否严格收敛4 个新文件2 组件 2 测试、6 个修改文件5 个 locale AsidePanel.vue、4 个删除文件。任何额外变更都视为范围蔓延scope creep需要标记上报。7.2 视觉一致性检查清单人工在 dev serverpnpm --dir frontend dev下逐项核对SQL Editor 左侧 gutter 从上到下依次是Bytebase Logo → 细分隔线 → 3 个 40×40 px 的 tab 按钮WORKSHEET / SCHEMA / HISTORY图标与 Vue 一致FileCode/Database/History切换到allowJustInTimeAccesstrue的项目 → 出现第 4 个 tabACCESSShieldCheck图标tooltip 文案 Just-In-Time Access激活 tab 有主色淡背景bg-accent/10和主色图标非激活 tab 用默认控件色 悬停淡色悬停每个 tab → 约 300ms 后右侧弹出本地化 tooltip点击顶部 Logo → 新标签页打开项目详情页无项目上下文时打开工作区落地页中英文切换 → tooltip 文案即时更新。7.3 桥接响应性检查关键这是 Stage 3 最核心的人工验证直接检验useVueState的双向订阅能力外部写入 → React 响应从 Welcome 屏Stage 1 迁移的叶子点 Connect to database 按钮——该按钮会设置asidePanelTab SCHEMA。预期连接面板打开且gutter 中的 SCHEMA tab 立即显示激活态无需任何手动重渲染。这证明 React 组件订阅了 React 子树外部的 store 写入React 写入 → Vue 响应点击 gutter 任意 tab → aside panel 右侧内容切换到对应面板WorksheetPane、SchemaPane等。这证明 store 写入反向传播给了仍为 Vue 的消费方。两个方向都验证通过才意味着READ via useVueState WRITE via 直接赋值的桥接闭环完整成立。八、迁移路线图中的位置与后续展望设计文档 §8 给出了一张清晰的阶段路线图帮助理解 Stage 3 的上下文阶段范围1 ✅Welcome叶子React 孤岛挂载模式2 ✅ConnectionHolderuseSQLEditorUIStorePinia 桥3本文GutterBarTabItem——React 对 store 的第一次响应式 READ4剩余小型EditorCommon/*叶子DatabaseChooser、DisconnectedIcon、ReadonlyDatasourceHint、OpenAIButton5第一个EditorPanel/Panels/*面板——将迫使Vue-in-React vs 级联迁移决策落地6批量面板、AsidePanel 子树、ConnectionPanel、TabList、ResultView7Monaco 封装、外壳翻转、清理Stage 3 的完成实际上为 Stage 5 的路线决策提供了关键输入既然 store 的响应式 READ 已被验证可行后续面板迁移可以选择React 面板挂到 Vue 壳内的渐进式路线而不必一次性翻转整个外壳。这一验证价值远超 136 行组件本身。九、可复用的迁移方法论沉淀从 Stage 3 的实施计划中可以提炼出一套可复用的 Vue→React 组件迁移模板划定 Non-goals明确不迁移父壳、不移植无关自定义件、砍掉无消费者变体YAGNIi18n 先行先补齐 React locale 缺失 key字节级一致接受暂时的 unused 警告中间态用脚本保证 1:1 parityTDD 双组件测试先行 → 确认 FAIL → 实现 → 确认 PASS用映射表tab → icon / i18n key替代命令式分支store 实例提升到组件顶层避免在useVueState回调里调用 hooks单点替换 调用方清查改完调用方后用 grep 模式确认零残留引用再删除孤儿代码基线化验收type-check 以既有错误基线 零新增为通过标准双向桥接人工验证外部 store 写入 → React 立即响应React 写入 → Vue 立即响应两个方向缺一不可。这套方法论不仅适用于 Bytebase 的 SQL Editor 迁移对于任何正在做Vue 组件渐进式迁移到 React的前端工程尤其是带 Pinia/Zustand 等全局 store 的项目都具有直接参考价值。参考文档与源码索引实施计划docs/superpowers/plans/2026-04-20-sql-editor-react-migration-stage-3.md设计规格docs/superpowers/specs/2026-04-20-sql-editor-react-migration-stage-3-design.mdReact 实现GutterBar.tsx、TabItem.tsxReact 测试GutterBar.test.tsx、TabItem.test.tsxPinia store 切片uiState.tsasidePanelTab状态与setAsidePanelTabaction 的定义处i18n 校验脚本check-react-i18n.mjs、sort_i18n_keys.mjs【免费下载链接】bytebaseDatabase governance built for humans and agents — controlling changes and access across every major database.项目地址: https://gitcode.com/GitHub_Trending/by/bytebase创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价