一天撸一个 Android SSH 客户端:WebSSH 开发全记录
这篇文章记录了一个很典型、也很有参考价值的个人项目实践:在已有 WebSSH 后端(Node.js + Express + ssh2) 的基础上,用一天时间快速做出一个 Android 客户端 MVP。原文把技术选型、开发阶段、踩坑过程和最终经验都交代得比较完整,尤其适合想做“已有 Web 系统移动端适配”的开发者参考。
起因
作者手上已经有一个可用的 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 | |
作者最终把 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 项目,正在考虑是否值得再做一个移动端客户端,这篇文章给出的答案很明确:只要边界定义清楚、复用策略得当,完全值得试,而且很可能比你想象得更快。