为什么我们放弃了 Electron
GitSquid v1 基于 Electron 构建。它能正常工作。用户可以浏览仓库、暂存更改、管理分支、处理远程仓库以及使用集成终端——所有功能都在一个桌面应用中完成。底层我们使用 simple-git 处理 Git 操作,node-pty 和 xterm.js 处理终端,以及常规的 React 技术栈构建界面。
但随着应用的成熟,Electron 的权衡变得越来越难以忽视。我们已经交付了一个功能完整的产品,但运行时本身却在拖我们的后腿。因此,我们使用 Tauri 2.x 将整个后端用 Rust 重写——同时保留了 95% 的 React 前端代码。
这是关于我们为什么这样做、如何做的,以及在这个过程中遇到了什么问题的故事。
Electron 的问题
Electron 是一个经过验证的框架。VS Code、Slack、Discord——主流产品都运行在它之上。但对于 Git 客户端来说,其开销与任务相比是不成比例的。
- 应用体积:Electron 捆绑了完整的 Chromium 浏览器和 Node.js 运行时。我们打包后的应用超过 150 MB。对于一个 Git 客户端来说,这很难说得过去。
- 内存占用:每个 Electron 窗口会生成多个 Chromium 进程。空闲时的内存消耗远超原生应用完成相同工作所需的水平。
- 启动时间:启动 Chromium 并非即时完成。用户会注意到应用变得可交互之前有明显的延迟,尤其是在较旧的硬件上。
- 原生体验:尽管 Electron 在原生标题栏和菜单方面做了努力,应用始终无法给人一种属于操作系统的感觉。窗口管理、焦点行为、键盘快捷键等细节之处,总带着一丝 Web 应用的味道。
这些问题中没有任何一个单独来看是致命的。但组合在一起,它们产生的摩擦与开发者工具应有的体验不符:快速、轻量、无感。
为什么选择 Tauri 2.x
在最终选定 Tauri 之前,我们评估了多个替代方案。决定性因素很明确:
- 使用系统 WebView 而非捆绑 Chromium。Tauri 使用操作系统提供的 WebView(macOS 上的 WebKit、Windows 上的 WebView2、Linux 上的 WebKitGTK)。仅此一项就消除了 Electron 二进制文件的大部分体积。
- Rust 后端。后端作为编译后的 Rust 二进制文件运行。内存安全、无垃圾回收器、可预测的性能。对于每次交互执行数十个 Git 操作的工具来说,这很重要。
- 显著更小的二进制文件。Tauri 应用可以在 10 MB 以下分发。与 150 MB 以上相比。
- 具备原生集成的跨平台能力。Tauri 2.x 为原生菜单、系统托盘、通知和文件对话框提供了可靠的 API。应用给人一种属于操作系统的感觉。
- 活跃的生态系统和良好的开发者体验。Tauri 的插件系统、IPC 模型和文档在 2.x 版本中已显著成熟。它已经准备好用于生产环境。
迁移策略
完全重写是有风险的。我们通过清晰的关注点分离来降低这一风险。
保留前端,重写后端
我们的 React 前端已经围绕 IPC 边界进行了结构化。组件调用与 Electron 主进程通信的函数;我们只需将这些调用重定向到 Tauri 的 invoke API。大约 95% 的 React 代码无需更改即可迁移。
后端则是另一回事。每个 Node.js 服务——Git 操作、文件监控、终端管理、认证、设置——都必须用 Rust 重新实现。总计,我们编写了约 7,200 行 Rust 代码,迁移了 120 多个 IPC 命令。
使用 Git CLI 而非 libgit2
一个常见的问题:为什么不使用 git2-rs(libgit2 的 Rust 绑定)?
我们选择 std::process::Command 直接调用 Git CLI。原因如下:
libgit2并不支持所有 Git 功能。交互式 rebase、某些 merge 策略和特定配置选项等操作要么缺失,要么行为不同。- 用户已经安装了 Git。通过调用他们本地的 Git 二进制文件,我们保证了与命令行的行为一致性。如果在终端中能工作,在 GitSquid 中就能工作。
- 调试更简单。我们运行的命令与用户输入的完全相同。出问题时,错误信息是熟悉的。
替换 Node.js 生态系统
每个 Node.js 依赖都被替换为 Rust 等价物:
simple-git变为通过std::process::Command直接调用 Git CLInode-pty变为用于终端模拟的portable-pty- 令牌存储迁移到
keyringcrate,使用操作系统密钥链(macOS Keychain、Windows Credential Manager、Linux Secret Service) - 文件监控切换到
notifycrate - HTTP 请求迁移到
reqwest
无缝设置迁移
我们保留了相同的配置目录结构。从 v1 升级到 v2 的用户不会丢失他们的设置、已保存的仓库或偏好。Rust 后端读取的是 Node.js 后端写入的相同配置文件。
我们遇到的挑战
迁移并非一帆风顺。几个问题花费了我们大量的调试时间。
Git 日志解析中的空字节
我们使用 git log --format 的自定义格式字符串来解析提交数据。在 Electron 版本中,我们在字段之间使用特定的分隔符。移植到 Rust 时,我们最初尝试使用空字节(\x00)作为字段分隔符——它们看起来是安全的选择,因为永远不会出现在提交消息中。
错了。Rust 字符串可以正常处理空字节,但技术栈的多个层会将其视为 C 风格的字符串终止符。日志输出在第一个空字节处被静默截断。我们改用了一个永远不会在 Git 输出中自然出现的多字符分隔符。
密钥链令牌存储
keyring crate 将凭据存储在操作系统密钥链中,这是正确的做法。但我们遇到了类似的空字节问题:在存储之前,我们用空字节分隔符拼接令牌数据。检索时,某些操作系统密钥链实现会在第一个空字节处截断值,返回不完整的凭据。解决方案相同——使用可见的、明确的分隔符。
BitBucket 认证的复杂性
GitHub 和 GitLab 有相对简单的基于令牌的认证流程。BitBucket 更加复杂。根据操作和账户类型,你可能需要用户名、邮箱、应用密码或 OAuth 令牌——而在什么地方使用哪个的规则并不总是显而易见的。让 BitBucket 认证在 clone、fetch、push 和 API 调用中可靠地工作需要多次迭代。
逐行暂存
GitSquid 支持从 diff 中暂存单独的行,而不仅仅是整个 hunk。在 Electron 版本中,补丁生成在 JavaScript 中完成。对于 Tauri 版本,我们将此逻辑移到了 Rust 后端。从任意行选择生成有效的 Git 补丁格式、处理上下文行的边缘情况,以及确保 git apply 接受结果——这是重写中最复杂的部分之一。Rust 实现最终比原始 JavaScript 版本更加健壮。
法语撇号导致构建失败
这个问题令人印象深刻。我们的一些面向用户的字符串包含法语排版撇号(弯曲样式,U+2019)而非直线 ASCII 撇号。这些在特定 CI 配置的构建过程中导致了编码问题。问题被识别后修复很简单,但错误信息完全没有指向实际问题。
结果
完成迁移并实现完全的功能对等后,以下是我们的成果:
- 应用体积:与 Electron 构建相比显著减小。Tauri 包只是之前 150 MB 以上的一小部分。
- 内存占用:基线更低,负载下更可预测。没有 Chromium 进程树在后台消耗内存。
- 启动时间:明显更快。应用几乎立即变得可交互。
- 功能对等:100%。v1 的每个功能都存在于 v2 中。集成终端、逐行暂存、多远程支持、分支管理——全部都有。
- 前端复用:95% 的 React 代码库得以迁移,验证了我们保持清晰 IPC 边界的架构决策。
值得吗?
值得。
重写花费了大量精力。约 7,200 行 Rust 代码、120 多个 IPC 命令需要重新实现,以及几个只因 Rust 和 Node.js 处理边缘情况方式不同才暴露出来的非显而易见的 bug。这不是一个周末项目。
但结果是一个根本性更好的应用。GitSquid v2 启动更快、内存占用更少、以更小的二进制文件分发,并且在每个平台上都更具原生感。Rust 后端比 Node.js 等价物更可预测、更易于理解。而且由于我们保留了相同的 React 前端,用户看到的是他们已经熟悉的相同界面。
如果你正在维护一个 Electron 应用且运行时开销困扰着你,Tauri 是一个值得认真考虑的选择。其生态系统已足够成熟可用于生产环境,迁移路径——尤其是如果你已经有清晰的前后端分离——比完全重写可能暗示的要更加可行。
关键洞察:将你的 IPC 边界视为 API 契约。如果你的前端只知道如何调用 invoke('get_commits', { path, branch }),并不关心响应来自 Node.js 还是 Rust,那么更换后端就会从幻想变成一个现实可行的项目。