一天撸一个 Android SSH 客户端:WebSSH 开发全记录

这篇文章记录了一个很典型、也很有参考价值的个人项目实践:在已有 WebSSH 后端(Node.js + Express + ssh2) 的基础上,用一天时间快速做出一个 Android 客户端 MVP。原文把技术选型、开发阶段、踩坑过程和最终经验都交代得比较完整,尤其适合想做“已有 Web 系统移动端适配”的开发者参考。

原文链接:https://bosh.886423.xyz/posts/1774945680000.html

起因

作者手上已经有一个可用的 WebSSH 后端,浏览器端功能并不少,包括:

  • SSH 终端
  • SFTP 文件管理
  • 服务器配置管理

但在手机浏览器上使用时,体验问题非常明显:

  • 键盘会遮挡终端
  • 缺少常用快捷键
  • 界面整体并不适合移动端操作

所以,作者给自己定了一个目标:一天内做出一个 Android 客户端

技术选型

整套方案非常务实,核心原则不是“最理想”,而是“最快跑通”。

组件选择

  • 语言:Kotlin
  • UI:Jetpack Compose + Material 3
  • 网络:Retrofit 2 + OkHttp
  • SSH 终端:xterm.js(运行在 WebView 中)
  • 持久化:DataStore

其中最值得注意的是终端方案:没有选择原生终端渲染,而是直接复用已有的 xterm.js 前端,通过 WebView 承载

这样做的优点很直接:

  • 能复用现有后端和前端逻辑
  • 开发速度快
  • 很适合快速验证产品方向

缺点也很明确:

  • 性能和交互上限不如原生实现
  • 键盘适配、视口适配等移动端问题会更棘手

但从“一天做出 MVP”的目标来看,这个选择非常合理。

三阶段开发过程

原文把整个开发过程拆成了三个阶段,节奏清楚,也很符合真实项目推进方式。

Phase 1:核心功能(约 2 小时)

第一阶段先把基础能力搭起来:

  • 服务器增删改
  • SSH 终端
  • SFTP 文件浏览
  • 文件上传 / 下载 / 预览

功能虽然很快搭起来了,但作者一开始就踩了一个非常经典的联调坑:

  • 后端接收参数是 ?server=123
  • Android 端发的是 ?serverId=123

只是一个参数名不一致,就导致 SSH 终端连不上,还花了 30 分钟排查。

这个案例很典型:前后端联调时,字段名、参数名、协议细节的偏差,往往比复杂逻辑本身更容易浪费时间。

Phase 2:体验增强(约 2 小时)

第二阶段开始补用户体验:

  • 批量 ZIP 下载
  • 设置页面
  • 备份 / 恢复
  • 修改密码
  • 标签筛选

这部分主要是 UI 和交互层面的完善,整体推进比较顺利。但也出现了一个隐藏很深的问题:

服务器更新接口在 Android 端没有传密码字段,结果后端把密码覆盖成了空字符串,导致一批服务器后续无法 SSH 登录。

这个问题说明:

  • 更新接口一定要区分“未修改”与“置空”
  • 服务端对敏感字段不能盲目覆盖
  • 客户端与服务端都要有防御性设计

Phase 3:锦上添花(约 1 小时)

第三阶段加入的是“让产品更完整”的能力:

  • SSH 密钥认证
  • 文件搜索
  • 权限显示
  • 暗色主题

这一阶段虽然耗时不长,但很能体现项目完成度。一个小工具是否“像样”,很多时候就取决于这些细节功能是否齐全。

几个非常有代表性的坑

原文最有价值的部分之一,就是把踩坑过程写得很具体。这里挑几个特别有参考意义的点。

1)HTTPS 页面发起 ws://,触发混合内容错误

问题的根源是:

  • WebView 中加载的 xterm.js 资源来自 HTTPS CDN
  • 终端连接使用的是 ws://

结果被浏览器安全策略拦截,报错:

1
An insecure WebSocket connection may not be initiated from a page loaded over HTTPS

作者最终把 xterm.js 下载到本地 assets/ 目录中,通过 file:///android_asset/ 加载,从而绕过混合内容限制。

这个处理很有工程味:不是追求最完美,而是优先解决当前 MVP 的可用性问题。

2)后端 WebSocket 错误处理不完善,导致 Node.js 进程崩溃

当用户 SSH 认证失败时,后端直接退出,原因是:

  • Client 实例抛出了 error
  • 某些 ws.send() 在连接已关闭时再次抛错
  • 异常没有被完整兜住

作者的修复方法包括:

  • 封装 safeSend()
  • 增加 ws.on('error')
  • ws.close()try/catch

这其实是后端稳定性里非常重要的一课:实时连接类服务里,错误处理不是附属逻辑,而是主流程的一部分。

3)手机键盘遮挡终端

这是全文里最“移动端开发真实现场”的部分。

作者尝试了多套方案:

  • adjustResize + visualViewport 动态调整
  • 65vh 固定高度
  • ResizeObserver + 动态高度
  • visualViewport.height - toolbar

结果都不稳定,最终采用了一个朴素但有效的方案:

  • 终端固定 45vh
  • 工具栏固定定位
  • 不再和键盘弹出行为做过度博弈

这个结论很实在:移动端 Web 终端的键盘适配,本身就是行业难题。 如果不是必须复用 Web 技术栈,专业场景还是更适合原生渲染。

4)Adaptive Icon 不生效

如果 Android 8+ 只提供普通启动图,而没有配置 mipmap-anydpi-v26/ic_launcher.xml,系统可能会直接显示默认绿色机器人图标。

这是一个看起来小、但非常影响成品感的细节。作者通过定义前景图层和背景色解决了问题,也说明:移动端产品的“完成度”经常取决于这些不起眼的配置。

5)Compose 图标缺失

作者本来想用 Icons.Default.Fingerprint 做指纹登录图标,结果编译时报 Unresolved reference: Fingerprint,最后改成了 emoji 🔐。

这个例子虽然轻松,但也说明一个现实:即使是常用框架,也不能默认认为所有组件都“理所当然可用”。

最终成果

从原文给出的结果看,这个 Android WebSSH 客户端已经具备相当完整的功能集:

  • SSH 终端(xterm.js + WebSocket + 虚拟快捷键工具栏)
  • SFTP 文件管理(上传、下载、批量 ZIP、预览、搜索、权限)
  • 服务器管理(增删改、密码与密钥认证、标签筛选)
  • 指纹生物识别登录
  • 数据备份 / 恢复
  • 暗色主题(支持 Android 12 动态取色)
  • 自定义应用图标

开发数据也很亮眼:

  • 时间:约 11 小时(09:00 - 20:30)
  • 代码量:约 2500 行 Kotlin + 200 行 HTML / JS
  • Git 提交:22 次
  • Bug 修复:10+
  • SSH 终端键盘适配版本:6 个

这篇文章最值得吸收的经验

如果把全文浓缩成几条最重要的经验,大概是这些:

1. 参数和协议细节必须严格对齐

前后端字段差一个字母,都可能让你多排查半小时甚至更久。联调时一定要核对:

  • URL 参数名
  • JSON 字段名
  • WebSocket 事件名
  • 空值与缺省值语义

2. 实时连接系统的错误处理要按“主功能”来设计

WebSocket、SSH、长连接这一类系统,一旦错误链条没兜住,就不是“偶发异常”,而是直接影响整个服务稳定性。

3. WebView 方案很适合 MVP,但要认清它的边界

复用 xterm.js 是一个很聪明的工程决策,能快速验证需求;但如果目标是长期打磨专业终端体验,原生渲染依然更有上限。

4. 移动端适配不一定越动态越好

很多时候,复杂的自适应方案反而不稳定。简单、固定、可预期的布局,在真实设备上更可靠。

5. 一天做出 MVP 完全可能,但前提是“复用已有能力”

这篇文章不是在讲“凭空一天做完所有东西”,而是在说明:

  • 后端已经存在
  • 前端终端方案可以复用
  • 技术选型务实
  • 目标是 MVP 而不是终极形态

在这样的条件下,一天做出可用产品是成立的。

相关仓库

结语

这篇记录最打动人的地方,不只是“11 小时做完一个 Android 客户端”,而是它非常真实地呈现了独立开发的节奏:快速选型、边做边改、持续踩坑、尽快交付。

如果你手上也有一个 Web 项目,正在考虑是否值得再做一个移动端客户端,这篇文章给出的答案很明确:只要边界定义清楚、复用策略得当,完全值得试,而且很可能比你想象得更快。


本文由「皮皮虾博客助理」整理发布。

热爱生活 学无止境
使用 Hugo 构建
主题 StackJimmy 设计