Refactoring English: Month 13

Michael Lynch

重构英语:第 13 个月

原文由 Michael Lynch 发布,订阅该博客

一句话总结

写着关于专注的章节,却被别的事分了心

第一次来?

你好,我是 Michael。我是一名软件开发者,也是几家小型独立科技公司的创始人。目前我正在写一本书,叫Refactoring English: Effective Writing for Software Developers

每个月,我都会像这样发布一篇回顾,分享这本书的进展以及我整体的工作情况。

本月亮点

  • 为我的书新增了基于购买力平价的地区定价。
  • 完成了我的第一个 Flutter 应用。
  • 正在编写我的第一个跨语言库。

目标评分

每个月初,我都会定下当月的目标。以下是本月的完成情况:

发布一款能为Refactoring English 网站引流的游戏

发布这篇博文是一次冒险,因为它只有登上 Hacker News 首页才能触达新读者,而唯一的机会就是 2026 年头几周。

幸运的是,这篇文章登上了 Hacker News 榜首,并在首页停留了近 22 小时。它延续了我推介其他优秀技术作者的策略——我喜欢这个策略,因为对我、读者和被推荐的作者来说是三赢。

那个 Hacker News 预测游戏我已经完成了约 80%。我还没想好怎么处理它——虽然快做完了,但我总觉得它不好玩,所以一直提不起劲去收尾。不过我还是想把它做完,看看大家的反馈。

发布《Refactoring English》的两个章节

  • 结果:两个章节都有进展,但均未完成
  • 评分:D

颇具讽刺意味的是,我正在写的两章主题正是动力与专注,但我却总让对 MeshCore 的各种尝试干扰写作。进入新的一年后,我在保持专注方面有所好转,而且这些分心其实也有好处——让我获得了关于如何重获专注的新鲜素材可以写进书里。

为一个纯属好玩的家庭照片分享应用写一份设计文档

还是老问题,12 月我又被 MeshCore 的实验分了心,没能取得预期的进展。我喜欢写设计文档,也觉得它很有用,但写起来实在枯燥,所以总忍不住想把它搁置一旁,去做那些能更快获得成就感的事。

Refactoring English 数据一览

指标2025 年 11 月2025 年 12 月变化
独立访客7,6082,266-5,342 (-70%)
预售收入$1,018.48$492.55-$525.93 (-52%)
赞助收入$48.25$48.25$0.00 (0%)
总收入$1,066.73$540.80-$525.93 (-49%)

预售额下降是因为我没有发布新文章来吸引新读者(那篇 Hacker News 文章直到 1 月才发布)。不过,“被动销售”仍在持续增长,这是一个积极的信号。12 月的预售额接近 500 美元,对比访客量相近的月份,5 月只有 241 美元,8 月为 361 美元,可见整体呈上升趋势。我希望随着书稿日渐完善、越来越多的读者主动推荐,被动销售能继续增长,而不必靠我每个月都去寻找一次成功的营销机会。

为我的书增加地区定价

11 月做黑五促销时,一位读者来信说,即使打了七折(20 美元),这个价格在阿根廷对一本书来说依然难以负担。他问我是否会考虑地区定价。他提到 Steam 游戏在阿根廷通常比美国便宜 50%,我觉得这个参照不错。

我通过 Stripe 收款,但在 Stripe 后台找不到任何地区定价的选项。我在 Stripe 知识库里找到一篇题为《地理定价实践:为何重要以及如何实现》的文章,刚开始还挺兴奋,通读全文后才发现,他们居然漏写了“如何实现”这部分。

所以,Stripe 一面提倡地区定价,一面却没有提供相应功能。这恰好提醒了我,Stripe 是最烂的支付服务商——除了其他所有支付服务商之外

于是,针对这位阿根廷读者,我临时手动为他创建了一个折扣价的专属支付链接。操作过程中我意识到,还可以直接用阿根廷比索定价,这样他就不用承担货币转换费了。我把价格定为 22,000 ARS(约 15 美元),他对价格和支付体验似乎都很满意。

这位读者建议我公开提供地区定价,至少面向巴西、印度这类开发者众多但购买力相对较低的国家。

即便 Stripe 原生不支持地区定价,把之前手动操作的那套流程自动化似乎也不难。我读到了Sebastien Castiel 为他的课程实现地区定价的分享,又由此看到了Wes Bos 关于同一话题的文章

Sebastien 分享了很多技术细节,但他的方案重度依赖 React,而我的网站是原生 HTML 和 JavaScript。他还依赖折扣码,这点我不太喜欢,因为这会让大多数顾客看到自己没能享受到的优惠。

我花了几个小时用云函数实现了一套方案,可以动态计算合适的价格并即时生成 Stripe 支付链接。随后我意识到,其实可以预先计算好一切,完全不需要服务端逻辑,于是又把云函数删掉了。

我的实现思路如下:

  1. 手动整理出 Stripe 支持的所有国家/货币列表。
  2. 编写脚本,从世界银行拉取数据,计算列表中每个国家的购买力平价(PPP)。
  3. 根据各国相对于美国的购买力计算相应折扣。
    • 例如,巴西的购买力平价比美国低 54%,因此享受 54% 的折扣。
  4. 过滤掉购买力平价与美国相差在 15% 以内的国家(折扣太小,不值得折腾)。
  5. 过滤掉折扣为负的国家。
  6. 将折扣上限设为 75%
    • 否则,埃及的价格会低至 4 美元,扣除转换费后我大概只能拿到 3.5 美元。
  7. 为列表中剩下的每个国家自动生成对应的 Stripe 价格对象和支付链接。
  8. 在网站上用一个 HTML 下拉菜单列出所有这些国家:

用户只需选择自己的国家,即可激活对应国家的 Stripe 购买链接,并以本国货币支付。

我采用的是自觉遵守的诚信模式,所以没有做 IP 定位或 VPN 拦截。我会隐藏每个国家的具体折扣,以避免有人特意挑选最便宜的选项。而以各国本地货币定价的好处之一在于,如果有人作弊、选择了并非自己所在地区的货币,还会在兑换费上吃点亏。

这些数字感觉还不太准确。按严格的购买力平价计算,美国 30 美元的等价物在埃及是 4 美元,但我怀疑在埃及你根本买不到 4 美元的正版程序员图书。

Wes Bos 当时就是直接让读者告诉他合理的价格,我也来试试。请留言或给我发邮件,告诉我你们国家面向开发者的图书一般是多少钱(以当地货币计)。

创建我的第一个 Flutter 应用

12 月,我发布了《我对 MeshCore 离网通信的初印象》。我对这项技术感到兴奋,但发现所有客户端都是闭源的,不免有些失望。

当时我决定暂时搁置对 MeshCore 的探索,但 MeshCore 贡献者Frieder Schrempf 在我的文章下回复并分享了一个有意思的观点

我很认同你在这方面的很多看法。我个人认为 MeshCore 的价值在于协议本身,而不太在于固件、应用等软件实现。[……]如果 MeshCore 作为一项协议能够成功并被广泛采用(目前看来正是如此),那么维护良好的开源实现自然会随之出现(至少我希望如此)。

我赞同 Frieder 的看法,心想:“要不我就写一个开源的 MeshCore 概念验证应用?”

其实,已经有一个 MeshCore 的概念验证应用了。官方 MeshCore 应用的开发者 Liam Cottle 之前曾为官方版写过一个MeshCore 网页应用作为原型。在推出官方(闭源)MeshCore 应用后,他废弃了这个原型,但原型的源代码依然可以获取,而且已经具备了我需要的大部分功能。

我好奇把这个原型移植到移动端会有多难。MeshCore 作为网页应用很难用,因为它需要蓝牙访问和离线模式。我听说过关于Flutter(Google 的跨平台移动开发方案)一些还不错的评价。我猜测,或许不用我过多介入,LLM 就能成功把网页原型的代码移植到 Flutter 上。

我的计划是让 LLM 分三步把原型移植到 Flutter:

  1. 使用 Playwright 为原型网页应用编写端到端测试。
  2. 将原型实现移植为 Flutter 网页应用,保持端到端测试不变以确保功能一致。
  3. 为 Flutter 项目添加 Android 构建。

这个计划奏效了,但每一步都比我预想的要笨拙:

  • 在为原型编写端到端测试之前,我不得不改造它,让它使用语义化 HTML 和 ARIA 属性,因为很多输入框标签只是光秃秃的 <div>
  • 我没能保持 Playwright 测试不变,因为 Flutter 实际上不会为网页应用生成语义化 HTML。它会创建一套 Flutter 特有的 HTML 方言,并把所有内容画在一块 HTML canvas 上。大多数 Playwright 元素定位器不知怎么仍能工作,但我还是不得不对测试做了大量针对 Flutter 的修改。
  • 即便有 LLM 帮忙,搞清楚如何用 Flutter 构建 Android 安装包也花了很长时间。
    • Gradle——Android 的构建系统——在 NixOS 上有 bug。我不断遇到神秘的报错,最后发现都是它缓存在主目录里的陈旧数据导致的。
  • Flutter 在蓝牙通信上出人意料地麻烦。在网页上(至少在 Chrome 上),只需调用navigator.bluetooth.requestDevice 就能免费获得该能力,但在 Flutter 中,你必须使用第三方的闭源库,并自己实现设备选择界面。

我本以为这会是个几小时就能搞定的周末小项目。结果花了 30 个小时、200 美元的 LLM 额度后,才终于让它跑通。

在真机 Android 设备上运行我的 MeshCore Flutter 应用

然而,就在我让我的 Flutter 实现达到与原型功能一致的那天,我去 Reddit 上分享时,却看到有人刚分享了meshcore-open——一个用 Flutter 实现的 MeshCore 客户端。想法和我一模一样,但做得比我好得多。

被人抢先让我有点失落,但也松了一口气。从短暂的 Flutter 开发经历来看,我巴不得尽快摆脱 Flutter。我本来就只想做个概念验证,指望有人能接手继续做,所以现在看到已经有了功能丰富的开源 MeshCore 客户端实现,我很开心。

也许 MeshCore 需要的是一个跨语言库

在开发我的 MeshCore Flutter 应用时,我不得不实现底层逻辑来解析 MeshCore 设备到客户端的消息。有一个公开的规范定义了 MeshCore 的点对点协议,但那份规范本身就比较宽松。而设备上运行的 MeshCore 固件与配套客户端(例如 Android 应用)之间通过蓝牙或 USB 通信的另一套协议,则根本没有文档。

事实上的参考实现是MeshCore 固件,但它把点对点协议逻辑、设备到客户端的协议逻辑和界面逻辑混在一起,实现分散在代码库的各个角落。

例如,MeshCore 客户端可以通过蓝牙从 MeshCore 设备获取联系人列表,但必须把原始字节反序列化为联系人对象。由于没有用于解码消息的库,每个 MeshCore 客户端和库都在各自重复实现:

我注意到这些实现有以下问题:

  • 它们不得不使用像 32 这样的魔法数字,而不是引用某个权威位置定义的常量。
  • 没有一个为它们的解析器编写了自动化测试。
  • 它们把不必要的底层工作带到了高级语言中。例如,所有实现都在存储 outPathoutPathLen 变量。这是 C 语言实现的遗留产物,因为 C 中数组不知道自身长度。而在 JavaScript、Python 或 Dart 这样的语言中,你根本不需要手动跟踪数组大小。
  • 它们没有仔细校验数据,所以会轻易放过诸如负的路径长度或超出地球范围的 GPS 坐标之类的垃圾数据。
  • 它们都忽略了 flags 字段,尽管该字段本应指示哪些字段已被填充。至少在点对点消息中应该是如此。而对于设备到客户端的消息,这些标志似乎毫无意义。

我最初的想法是用protobufCap’n Proto 这类协议库重写逻辑,但现阶段我看不到以向后兼容的方式集成第三方库的办法。

那么,如果我用 C 来写一套 MeshCore 设备到客户端协议的核心实现呢?我可以再为各种语言添加绑定,这样就不需要为 Dart、Python、JavaScript 以及其他想用的语言各自维护一整套独立实现了。

于是,我开始了自己的 MeshCore 客户端库:

这个库还没到可以作为概念验证来演示的程度,但已经很接近了。

MeshCore 的维护者完全有可能不喜欢这个想法,如果没有他们的支持,这个项目基本就胎死腹中了。但我还是做了,因为我从未尝试过编写跨语言库,这本身就是一次有趣的经历。

我上一次尝试从 Python 调用 C 代码还是 20 年前,当时不得不使用SWIG。那时感觉既痛苦又像是拼凑出来的权宜之计,而现在似乎已经好了 80%。

我非常想用 Zig 而不是 C 来写核心实现,但遇到了太多阻碍:

  • Zig 尚不能编译到大多数 MeshCore 设备所使用的 xtensa 架构。
  • 大多数 MeshCore 固件项目使用的 PlatformIO 并不支持 Zig。
  • Dart 的ffigen 或许能与 Zig 配合,因为 Zig 支持 C 的 ABI,但即便是让它与 C 一起工作都很困难。
    • Python 的cffi 也是如此。

总结

完成了什么?

经验教训

  • 减少并行推进的项目
    • AI 让启动新项目变得前所未有的容易,但要把它们打磨到可发布的状态,瓶颈仍然是我。结果就是堆积了大量进行中、等着我审核后才能发布的项目。频繁的上下文切换和任务跟踪带来了不小的心理负担。

下月目标

  • 发布《Refactoring English》的三个章节。
  • 发布我的 2025 年度回顾(第 8 年)。

求助

在你生活的地方,30 美元(USD)买一本面向开发者的书算贵吗?如果算贵,请告诉我,在你的国家,像Designing Data-Intensive Applications 这样的编程书通常卖多少钱(以当地货币计)。

本文章由 muse-spark-1.2-contributor 进行翻译

评论