坑很大。

很大。

坑的起源:我有一个 idea

今年 7 月,在几个产品 MVP 都无人问津后,我也开始思考下一个产品要做啥。正巧这时候在做一个项目的后端,需要自测,也就要用到 postman 一类的工具。

找了一圈没找到合适的,对我来讲,他们大都有这样的问题:

  • 启动时弹窗要登录,或者弹窗告诉你这个版本不被支持了,一定要你升级
  • 由于后端开发要对同一接口请求多次,导致历史记录基本没什么用
  • 保存请求后,读取这个请求,请求另外一个地址再保存,这个保存的请求会被覆盖掉…… 经常误操作,所以还不如历史记录有用
  • 客户端大都基于 Electron

那么,为啥不自己写一个,作为公司的下一个产品?越想越觉得这个点子靠谱。开干!

毕竟没做过桌面端应用,我预想到会有坑,给了自己一个宽松的时间:

两个月半之内(九月底),做一个 postman 替代品,实现环境,文档,同步,支付等功能并上线正式版开卖。

这时我还不知道,被坑支配的恐惧……

坑一:Rust

写 Rust 一时爽,一直写 Rust 一直爽。语言设计充满合理性。函数式编程,泛型,trait 等特性让它写起来完全不像是系统语言,非常像高级语言。亲切的编译器不但会告诉你有问题,还会手把手告诉你问题的原因可能是什么,顺便再教你点你不知道的 rust 规则细节。

那么为什么说是坑呢?

因为在生产项目中运用不熟悉的新技术都是给自己挖坑。而 Rust …… Rust 太难学了。

Rust 的书看得头疼,所有权的概念不好理解,语言设计复杂度高,但真正写起来时…… 会发现比文档里还复杂。

比如说,用一个数组(vec)保存实现一个 trait (类似 interface) 的多种闭包(匿名函数),并写一个函数接受闭包参数并存储在这个数组中,这时需要 1. 数组里,将闭包放在一个容器(box)中,2. 函数里,给闭包加上 + 'static 修饰。

为什么?

  1. Rust 中每个匿名函数都属于自己的匿名类型,将这些不同类型的函数放在一个数组中,也就是说必须是动态存储,编译时不知道到底是哪些类型,也就不知道它们的长度大小,必须要用 box 包起来才能保证长度一致。

  2. 通常情况 'static 是表示一个变量引用的是静态变量,但在这里这是表示这个闭包只会引用静态变量,不会出现这样的情况:一个函数返回一个闭包,闭包引用了函数中的本地变量,函数返回后这个本地变量失效被销毁,闭包调用时又去访问这个无效的变量。

这解释,我自己看着都头疼。

写 Rust 时,时常会遇上这种情况,文档看完了觉得自己懂了,但实际写起来时,会遇到怎么也想不通,怎么写也编译不过的情况,stackoverflow 查无数资料,并筛选掉已经过时的资料后…… 才终于能搞懂。

除了难学,复杂之外,还有两个问题:

问题一:语言虽然稳定了,但也还在积极开发中。也就导致:1. stackoverflow 上有不少过时的答案 2. 重要特性如异步(async)还没稳定 3. 有的库只支持 nightly rust。

问题二:生态还不成熟。如:异步还在完善中,GUI 库刚起步。

坑二:桌面 GUI 和 web 开发差别不小

不同的领域需要不同的技术。

写 web 时,不管前端后端,基本都不用进行手动线程管理,或者考虑各个操作系统的兼容。

写桌面端就不一样了,想异步发个网络请求?新建个线程呗。想异步写文件?新建个线程呗。

想要有回调,async(Promise,future)?嘿嘿嘿,GUI 程序有个消息循环跑在一个线程上,异步一般有个事件循环跑在一个线程上,你可以把异步操作整合到消息循环里去,也可以再开个线程跑事件循环,再写个东西让这它们两能互通。

朋友,你喜欢造轮子吗?

再比如说,web 前端早已从绑定事件 + dom 操作转向了 React 一类的 MVVM 模式,而我所选的 GUI 库的使用方法仍然是事件驱动,手动更新 UI。难道……要自己写一个 React ?

朋友,你喜欢造轮子吗?

坑三:GUI 引擎的选择

GUI 引擎跨平台的有如 wxWidget, GTK, Qt。喜欢自虐也可以用各个平台原生的。

Rust 原生的也有一些,但并不成熟,不用考虑。

和 js 前端框架不同,成熟的 GUI 引擎都有不小的复杂度,很难看一两个教程就了解全貌,也就导致…… 都没用过的话,真不知道要怎么选。

比如 GTK 的架构就包括 GTK+,Pango,GDK,ATK,GIO,Cairo,Glib。他们都是干啥的?比如 Qt 还有个 Qt Quick,有啥区别,有啥坑?

比如 wxWidget 官网里文档导航下第一项是一本 700 页的书。

写 js 时,不管前端后端,基本一个 readme.md 就能搞清楚这个库是干啥的,怎么用,撸起袖子就可以开干。但这些 GUI 库,光是找到文档页就得花一番功夫。

到底怎么选?

此外,C 和 Rust 的内存管理风格差别很大,就算有 binding,也可能会很蹩脚……怎么办?

本着对 Linux 和开源的好感,我一开始准备用 GTK,但 GTK 示例项目在 Windows 下崩溃了,此外,我发现 GTK 3 直到 3.4 才真正支持 Windows,让我怀疑它的跨平台性……

最终选择了 Sciter,一个 5M 大小的使用 HTML 的 GUI 引擎。体积小,内存占用低,还是熟悉的 web 技术,好几个杀毒软件用了它,所以也经过了市场验证。

那么,代价是什么呢?和标准不兼容,要重新学一遍 HTML / CSS,文档很差,坑不少,渲染性能也和 Chrome 有差距。当然也没有 React 给你用。

在被 Sciter 折磨的日子里,我无比怀念写浏览器应用的日子…… 作为用户,虽然我仍然不喜欢 Electron,但我开始理解为啥开发者们这么喜欢它…… GUI 应用开发太难了。

踩坑的代价

开始开发一个月半后,在八月底,开发陷入了泥沼,我的动力也跌到了谷底。

  1. 开发过程中经历了多次返工重构,新老代码夹杂,比如:代码中存在一种同步和两种异步操作数据库的方法。

  2. 已完成的部分不少需要修补,比如 UI 太丑需要重做,有些地方错误处理得并不好。

  3. 离完成遥遥无期。还有很多功能完全没有开始做。粗略估计一下,半年完成当初的预想可能是比较现实的估计……

  4. 每天开工都想摔键盘。八月有两个周末也接着码代码,不仅没有加快进度,反而透支了精力。

实在太过痛苦,让我已经想要放弃……

从快要烂尾到 0.1.0

如何从坑中爬起来?

  1. 拆分最初的目标,将“一下推出带文档,同步,导入导出,付费功能的 1.0 版”变为从 0.1 - 0.5 五个版本,先推出功能残废的 0.1 版,再慢慢完善。

  2. 进一步将 0.1 拆分为四个版本,0.0.1 重新设计界面,清理旧代码,0.0.2 关于窗口,错误提示等,0.0.3 增加 json 代码高亮,聚合开源库的协议等,0.1 自动检查更新,sqlite 数据库路径改为 home_dir,release 版去掉调试相关功能,添加示例项目。

  3. 每完成一个小版本都给自己放个假,身体是革命的本钱……

终于在十月底,完成了 0.1.0 版并发布,不管怎么样,没有烂尾,可以松一口气。

经验总结

  1. 超过一个月的项目都最好有里程碑,一步一步慢慢来,不管是个人开发还是团队。

  2. 编译类语言和解释类语言的差别不小,web 编程和 gui 编程的差别也不小。跨领域要做好心理准备。

  3. Electron 也挺好的,虽然我仍然不喜欢它。

  4. Rust 是真的香。