在一个 Rust 核心上搭载各平台外壳的理由
如果要在三个操作系统上推出同一款编辑器,有一种方法只需编写一次 UI。我们曾经走过这条路,后来又退了回来,改为在一个核心之上叠加一组针对各平台的轻量外壳。

如果要在 Linux、macOS 和 Windows 上推出同一款编辑器,就得制作三次 UI。
一开始,我们想避开这项成本。我们使用一个跨平台工具包,在三个操作系统上绘制相同的界面。这个决定让我们通过了第一项性能门槛,也达成了韩文输入的第一个里程碑。
后来,优先级发生了变化。
性能比设计重要,设计比成本重要。时间和成本不再作为约束条件。
按照这个标准重新审视后,答案也变了。
我们放弃了统一的 UI
跨平台工具包有明确的优点。制作一次的功能可以在多个操作系统上使用。
但也有必须付出的代价。它会多占用 40~60MB 内存,启动速度会慢 150~200ms。文字呈现不如操作系统的默认文本引擎,系统原生的视觉效果也无法完整使用。
在开发成本很重要的时候,这是一个合理的选择。但当成本不再是约束条件后,就没有理由继续承受这些代价了。
因此,我们决定用 Rust 构建处理文档的核心,并为每个操作系统分别构建负责界面的外壳。
这并不是因为原有技术不好。优先级发生变化后,即使面对相同的选项,也会得出不同的答案。我们决定付出将功能制作三次的成本,以换回性能、文字质量和符合系统习惯的体验。
不过,我们并没有舍弃一切。
我们保留了整套测量规范。把指针移到窗口外、通过名称准确找到窗口、同步计算帧数。韩文输入的设计原则也一并迁移了过来。只向操作系统展示光标所在的行,不修饰正在组合的文字。如果状态出现偏差,就不进行猜测,而是重新同步。
代码可以丢弃,但经过验证的原则会留下来。
我们把外壳的职责限定得很小
如果各平台的外壳开始承担大量工作,三个产品很快就会变得不同。因此,我们明确规定了外壳应该做什么,以及不应该做什么。
外壳负责创建窗口和绘制文字。它把键盘和指针输入传递给核心,并将韩文输入转换为编辑命令。它也会应用操作系统的视觉效果。
另一方面,它不解析 Markdown。除换行之外,它也不决定布局,不保存文件,也不判断许可证。
这项原则并不只是写在文档里。如果外壳中加入了 Markdown 解析代码,自动检查就会让构建停止。
如果只有某个外壳开始处理语法,各操作系统上的结果就可能逐渐产生差异。这类问题也很难发现。因为即使表格只在某个外壳中显示得不一样,真正的原因也未必出在绘制表格的代码上。
我们让核心成为唯一长期保留的部分
核心不依赖任何 UI 技术。即使没有窗口,也可以在终端中单独测试。
文档编辑、Markdown 解析、撤销、搜索、大纲、文件保存、导出和许可证验证,全都由核心负责。

用户按下一个按键后,外壳会将该输入传递给核心。核心修改文档,并且只重新解析变更部分的周边区域。它只计算屏幕上可见的内容,然后把发生变化的结果返回给外壳。外壳只重新绘制受影响的行,并完成一帧。
如果在这个过程中重新读取整个文档,那就不只是一个单纯的性能问题。那意味着违反了最初设定的预算。
正确性也只在核心中验证一次。它必须通过全部 652 个 CommonMark 官方示例,只要通过数量减少,构建就会失败。我们还会进行模糊测试,不断输入预料之外的内容。
我们也会确认保存后的文件是否与原文件逐字节相同。如果往返一致率低于 1.0,那就不是速度慢,而是文件内容丢失了。
即使外壳发生变化,核心也会保持不变。在 Linux 和 Windows 上直接连接核心,在 macOS 上则使用 Swift 连接代码。macOS 外壳使用 AppKit 和 TextKit 2 构建,Windows 外壳则使用 DirectWrite 构建。
我们的目标是让文档保持一致,同时让操作手感符合各个操作系统的特点。
文档相同,使用感受不同
确定边界之后,需要统一的内容也变得清晰了。
需要统一的是文档。无论在哪个操作系统上保存,文件内容都必须逐字节一致。表格对齐、行尾和最后的换行也不能发生变化。
Markdown 的正确性已经在核心中完成验证。每次制作新的外壳时都不必重新证明。在新平台上需要确认的是使用感受和性能。
相反,我们不会统一直接触达用户的体验。
文字使用各操作系统的文本引擎绘制。韩文输入法也直接采用操作系统的方式。半透明效果同样在 macOS 和 Windows 上使用系统功能,而在没有相同功能的 Linux 上,只自行绘制必要的部分。
我们也不会强行统一窗口按钮的位置、菜单的形式和快捷键的辅助键。我们的目标并不是在不同的操作系统上显示相同的像素。在每个操作系统上都让人感觉自然更加重要。
发布条件也针对各操作系统分别制定。只有通过性能标准,并使用默认韩文输入法完成实际测试后,才能发布。
基准测试表中对应平台的行,也只有在构建产物真正生成之后才会出现。因为如果提前写入尚未测量的行,表格中的其他行也会变得难以信任。
这个选择有着明确的代价
相同的功能必须制作三次。韩文输入也要实现三次,构建环境也要维护三套。
目前使用的机器无法构建或调试 Windows 和 macOS 版本。我们需要额外的硬件和自动化环境。因此,我们不会为这两个平台标注日期。
我们也保留了重新审视这项决定的条件。将成本排除在约束条件之外,是做出这项决定的前提。如果无法获得所需的硬件,就只推迟对应平台的日程。Linux 版本仍会照常发布。
一项决定不仅需要理由,也需要明确重新考虑它的条件。这样在情况发生变化时,就不必从头重复争论。
需要长期保留的是核心。外壳必须随时都可以被丢弃。
我们只把可以丢弃的部分交给平台。