原型即规格:这个项目怎么保证"做出来的就是设计出来的"

ssh-gui 技术系列 · 四 · 2026-10-23

软件项目里最常见的失真,发生在设计和实现之间。设计稿上是精致的,实现出来是"差不多"的,中间没有任何东西负责拦住那一点一点的走样。ssh-gui 用了一套有点笨但很有效的方法来对付这件事,这篇讲讲它怎么运转,也讲讲它出过什么岔子。

一份 3300 行的 HTML

项目的起点不是代码仓库,而是一个单文件 HTML:约 3300 行,CSS 和 React 全部内联,双击就能在浏览器里打开。它包含 11 个界面和 3 个弹窗——主机、终端、文件、转发、片段、密钥、录制、分组、设置、新建连接、批量执行——每个界面都是可交互的,不是静态图。

为什么坚持单文件?因为浏览器在 file:// 协议下会把外部 JS 当跨域脚本拦截,拆成多个文件就必须起 HTTP 服务器,"双击即开"就没了。这个原型的受众包括非程序员的协作者,打开成本必须是零。

重要的是,这份原型不是丢弃式的草图,而是规格本身。它内部按固定顺序分节:令牌、基础组件、视图、对话框、应用外壳。后来生产实现拆到 frontend/src/ 时,就是沿着这些分节边界拆的,几乎不用重新设计。原型阶段还固化了三条工程约束——状态集中在应用根节点、动作函数经单一对象下发、会话模型用面板数组而不是标签数组(分屏能力的根)——这三条原样搬进了生产代码,一条没改。

23 张截图和 11 项断言

原型之上跑着一个 Playwright 验证脚本:渲染全部界面,截 23 张图,执行 11 项交互断言——垂直分屏真的能分、分隔条拖动真的改比例、瞬时故障重试后真的成功、确定性失败重试后依然失败、Ctrl+K 真的能唤出命令面板——外加"0 控制台错误"这条底线。任何一项不过,脚本以非零码退出。

这套东西的价值在改样式的时候才显现。设计令牌换一个色值,跑一遍脚本,23 张图里哪一张变了颜色一目了然。人工点界面永远点不了这么全。

一次订正,催生了截图门禁

这套体系也不是一开始就严密。项目记录里留着一条很难看的订正:某轮界面收尾时,进度文档写下"批次 1–5 已逐屏对齐原型",事后发现不成立——新建连接弹窗和设置页从头到尾一行都没改过,"对齐"是从视图数量推断出来的,不是逐屏看出来的。

这件事的教训直接变成了机制:"对齐"必须以逐屏截图为证据,不接受推断。 现在生产前端有自己的截图脚本(compare/shots-*.mjs),对每一屏出暗色、亮色两套截图并断言真实数据,失败项 必须为 0;另有一个 stitch.mjs 把原型截图和生产截图拼成左右成对图,人眼最后过一遍——自动化管数据正确性,人眼管"像不像"。

生产侧的门禁是一整套:Go 的 vet 和测试、前端的 typecheck、lint、format、测试、构建、绑定漂移校验,最后才是截图。目前后端 78 项测试、前端 141 项,全绿才算动过。数字本身不重要,重要的是它们挡下过真问题——有一次改了个类名,截图断言立刻红了,因为验证脚本依赖那个类名定位元素。

真机:CDP 驱动真实窗口

截图脚本跑在浏览器里,但产品是 Wails 窗口。中间隔着 WebView2、系统凭据框、真实的 SSH 握手——这些只有真机能暴露。最后的办法是给应用加了一个 -cdp-port 启动参数,让 WebView2 开出调试端口,Playwright 用 CDP 协议连进真实窗口里做自动化。

顺带踩过一个坑:一开始想用 WebView2 官方的环境变量开调试端口,怎么都不生效,后来发现 Wails 自己显式传了参数,运行时会忽略环境变量。这类发现只有真机跑起来才会有,全记进了开发文档。

真机回归抓出的 bug 都是"单测全绿也发现不了"的类型。举两个:SFTP 的客户端缓存会把一条已经死掉的 SFTP 通道交给调用方,服务端一重启,后续操作全部报 connection lost——修法是缓存监视上游连接,命中前先探测、死了就重建;另一个是堡垒机禁止端口转发时,错误信息落到了"请检查网络与服务状态"这种兜底文案上,用户根本不知道发生了什么——修法是定向识别服务端拒绝原因,报错写明"堡垒机禁用了转发"。

真机脚本断言的东西也很实:键盘敲进去,字符经 xterm、WebSocket、远端 shell 转一圈回显到屏幕;上传的文件逐字节比对;杀掉测试用的 SSH 服务器,看标签进入断线状态、退避计时、服务器恢复后自动重连、输入依然可用。28 项断言,全过。

文档和两个工程细节

这个仓库的文档体系有个明确规矩:每件事只有一个真相源——视觉看 DESIGN.md,架构看 architecture.md,进度看 TODO.md,其余文档只引用不复述。两份真相源打架时,不自行挑选,而是修掉矛盾、两边留痕。规矩执行起来很重,但换来的东西是:任何时刻打开文档,它和代码不会差出一个版本。

最后记两个不起眼但代价惨重的细节。

一是编码。Windows PowerShell 5.1 的 Get-Content/Set-Content 按 GBK 解码无 BOM 的 UTF-8 文件,有一次把仓库文档里的 225 个中文字符直接吃掉了。从那以后"禁用 PowerShell 读写仓库文件"写进了协作红线,脚本必须显式用 UTF-8 API。

二是"诚实空态"。界面上的列表没有数据就显示空态,不放假数据撑场面——看起来更满的界面会误导用户以为系统里真有那些数据。这条对 SSH 客户端尤其重要:主机列表里多出一台不存在的主机,比列表短一截危险得多。

代价

这套方法明显的代价是慢。改一个按钮颜色,要过令牌核对、全套门禁、暗亮两轮截图。但运维工具的可靠性不是迭代出来的,是约束出来的——这份原型继续充当视觉与交互的最终裁决,直到今天,生产界面的每一屏都能在 compare/pair/ 里找到它和原型的并排照片。"按原型来做"在这不只是一句口号,是一摞可以点开的图片证据。

posted @ 2026-10-23 23:36  cliffdev  阅读(3)  评论(0)    收藏  举报