我 Vibe Coding 做了一个阅读 App
我用 Vibe Coding 做了一个阅读 App
为什么要做这个
最近半年我痴迷于学习英语。阅读英文原著是最好的一种输入方式,但读到不认识的词,要跳出去查;查了又不知道在这个句子里该怎么用;想记下来,但笔记和阅读是割裂的;想练发音,又要再跳出去,例如微信读书只能朗读全部不能选中单词朗读。
市面上的 EPUB 阅读器大多有翻译功能,但翻译不等于学习。你只能通过一门语言的解释去学会你想认识的那个单词,理解了意思却还是不会用。
没有找到合适的工具,所以决定自己做了一个叫做Immersed阅读软件。
什么是 Immersed
Immersed 是一个 EPUB 阅读器,支持全平台,Flutter 写的,一套代码跑多个移动端。
它不只是能打开书。点一个单词,弹出音标、AI 解释、标注、TTS;选一段话,可以让 AI 按你设定的词汇难度解释整个句子的结构;读完一章,高亮的内容可以直接生成书摘卡片导出分享。整个阅读到学习的闭环,都在里面。
技术栈怎么选的
做阅读器的第一个问题是:用什么技术路线?
Vibe coding 的默认路线是 WebView也就是使用Readium(支持三端ios,安卓和WEB)。
把 EPUB 里的 HTML/CSS 塞进一个 WebView 里渲染,省事,够用,AI 也最熟悉这条路。我最开始也沿着这条路走,但很快发现问题:WebView 太卡了。翻页有延迟,打开大书会掉帧,根本做不到秒开。
并且没有精确的分页,分页只能做到章节里面的分页,而且这个章也的分页也不准确。甚至说我的设备旋转了,那它的分页就变了,然后也不是精确的。
我当时在用微信读书,发现它打开一本书的速度快得出奇,翻页也很丝滑。第一反应是:这肯定不是 WebView。打开手机上的 WebView 检测工具扫了一下,确认不是。它用的是 Canvas 自绘。
这给了我一个方向。
为什么选 Flutter
然后关于Immersed这个App,它其实不是一个特别靠原生能力的,或者靠原生平台某个能力的App。所以说选择Flutter来做全平台的开发,将安卓和iOS都能覆盖。
手机是单页竖向阅读,平板是横屏双页展开,两种布局的分页逻辑、翻页动画、进度管理都不一样,但底层的阅读状态、标注、词库应该共用。Flutter 的多平台特性让这件事成立:一套 Dart 代码覆盖两端,只是渲染策略不同。
更重要的是 Canvas。
Immersed 需要精确控制选区、高亮、标注恢复、排版算法,这些在 WebView也能通过一些邪修的办法做到,但是比直接使用Canvas要复杂得多,或者做到了也完全控制不了细节。
Flutter 的 Canvas 自绘让我可以自己决定每一行字怎么排、每一个高亮块的坐标是什么、用户选了文字之后工具条出现在哪里。
Canvas还有一个重要的一点就是它的速度非常快。
大家可能想起最近非常火的pretext,性能之快就是因为使用canvas,和我的这个项目日历如出一辙,都刚好想要解决排版问题。
为什么用 Rust 解析 EPUB
Canvas 解决了渲染速度的问题,但还有另一个问题:排版还原。
EPUB 本质上是一个 zip 压缩包,打开之后是一堆 HTML 文件加上 CSS 样式表。一本书的每个章节,就是一个 HTML 文件。它的排版信息,段落间距、字体、缩进、图片位置,全都藏在 HTML 和 CSS 里。
用 WebView 渲染时,浏览器引擎帮你把这些 HTML/CSS 解析成页面,排版还原是”免费”的,代价是你失去了控制权。但现在我用的是 Canvas 自绘,Canvas 不认识 HTML,它只知道”在第 X 行第 Y 列画一个字”。所以我需要自己把 HTML 翻译成 Canvas 能理解的结构,这个翻译的过程就是解析层。
还剩一个问题:EPUB 解析层怎么写。EPUB 本质是打包的 HTML,但结构参差不齐。不同书的排版、链接、空行、图片各不相同,有的用 <br/> 做段间距,有的用 margin,日语书有自己的一套空行惯例。这一层如果不稳,上面所有的排版都会出问题。
正好想起一件事:小米做手机相册时,UI 层用什么技术栈都行,但图片的解码和处理层单独改用了 Rust,为的是极致的性能和稳定性。解析层跑得快,出了问题可以精确定位,不会混入上层的噪音。这个思路和我的问题完全对得上。
于是我用 Rust 写了一个 EPUB parser,把原始 HTML 解析成内部的 RenderNode 结构,再交给 Flutter Canvas 排版和渲染。
整条管道是:Rust 解析 EPUB → 转成 RenderNode → Flutter Canvas 排版绘制。这条路线比 WebView 累的多,但是AI刚好适合做这种脏活,但它让后续所有的阅读体验都是可控的。
模块文档系统
技术路线定了之后,下一步是解决一个很多人 vibe coding 时都会忽视的问题:AI 没有记忆。
每个对话框都是一张白纸。你在 A 对话框里做的决定,B 对话框根本不知道。多开几个窗口并发开发,每次都是一个新的session,这样会导致一个问题?
AI每次都要重新理解上下文,所以我们agent都有Claude.md或者agents.md来解决。
我选择了一套模块文档系统来解决这个问题,而不是把所有规则都堆进一个巨大的 CLAUDE.md。
具体做法是:Immersed 的每个核心模块都有一份独立的文档,放在 docs/modules/ 目录下。Reader、AI Explain、Mark/Note、Words、TTS、Quote Card,每个模块一个文件。每份文档写清楚六件事:模块目的、In/Out 边界、核心流程、关键状态与数据、交互与异常、验收标准,以及明确的非目标。
开发某个模块之前,先把对应文档喂给 AI,让它知道”这个功能的边界在哪里”、“不该动哪里”。
这套系统的核心价值有几个:
上下文更小,AI 更聚焦。 AI 不需要每次加载整个项目说明。改 Reader 选区,就读 reader.md 和 highlight.md,不会因为一个小需求把 Library、Settings、Parser 都顺手乱改。
决策沉淀下来,不只活在聊天记录里。 聊天里做出的产品和技术决策,开发完后同步进对应模块文档。后面的 AI 不需要重新理解历史聊天,也能知道当前设计为什么是这样——比如 Mark 为什么不用 pageIndex 定位、Explain cache 为什么不因 Vocabulary Level 自动失效,都记在文档里。
遇到 bug 别慌,先打 log
真正开始开发以后,但有几个问题反复出现,改了又坏,坏了又改。
最典型的是分页和翻页。手机翻页正常了,平板双页就乱;平板双页修好了,手机切章节又出现闪烁。我一直在往 AI 喂 bug 描述,让它修,然后看到”修手机坏平板”的下一轮开始。
后来我意识到,每次喂给 AI 的只是”现象”,而不是”数据”。AI 在没有任何运行时信息的情况下猜原因、猜位置,当然容易猜偏,改了东墙塌西墙。
Vibe coding 有一个不如 Web 开发的地方:Web 里浏览器 DevTools 可以让AI通过mcp直接阅读;但 Flutter app 跑在手机上,你看不到任何运行时状态,只能看到 UI 上的表现。这让 AI 的”自纠性”大幅下降。
但这不是无解的,只是需要换一套工作节奏。
遇到问题,先不要让 AI 改代码。
第一步:让 AI 把关键路径的 log 先打好。分页乱了,就在分页计算的入口和出口各打一条,把输入参数、中间状态、最终结果都打出来。缓存没失效,就在缓存读取和写入的地方打版本号和时间戳。
第二步:带着这些 log 重新跑一遍出问题的流程,把完整的日志输出复制出来。
第三步:把日志直接贴给 AI,让它看着真实数据分析,而不是靠想象猜。
这一套流程下来,AI 的修复准确率有质的提升。因为它不再是在猜,而是在读证据。
分页和翻页那个循环,最终定位到问题的那一次,不是靠我更准确地描述”手机翻页正常平板就乱”,而是靠把分页计算的输入输出完整打出来之后,AI 一眼看到:两端的 viewport 宽度在某个时机传错了。没有 log,这个问题可以再转三个星期。
翻完那段时间的对话记录,那几百条 prompt,有多少是真正的决策?有多少只是”好,继续”、“再试一次”、“还是不对”?
那些必须停下来想清楚的决策
到这里,技术栈选定了,模块文档系统搭好了,架构设计也定了。这些事情做完,感觉很好,像是真正在设计一个产品。
但接下来有一段时间,我其实只是在按回车。功能一个接一个开,遇到问题让 AI 改,改完继续下一个。这个阶段的对话记录看起来很热闹,实际上是大量的”好,继续”、“不对,再改一遍”、“还是不行”。我完全沉浸在执行的节奏里,以为推进就等于决策。
直到被几个反复出现的问题卡住,我才停下来。
停下来之后做的第一件事,不是让 AI 直接修,而是切换到一种节奏:先让 AI 给出一个方案,我来审。
这件事有个名字,在 Claude Code 里叫 Plan Mode。你告诉 AI 目前的问题,让它分析、给出解法思路,但先不动代码。然后你读这个方案,找它有没有漏洞,看它的修改方向对不对,确认之后再让它跑。
关键在”确认”这两个字。AI 给的方案不是圣旨。它可能拿到了正确的问题,但往错误的方向解。它可能修了表面,没碰根因。这个时候你如果只是看了一眼,觉得听起来不错就让它继续,结果不会比直接让它改好多少,你还是在按回车。
真正的 Plan Mode 是:你读方案,你判断,你有异议就推回去,你没异议再放行。这个过程里,你的专业本能才有机会介入。
举两个我实际碰到的例子。
阅读进度定位。进度保存用什么来定位当前位置?最直觉的答案是 pageIndex,AI 给的第一个方案也是这个。看起来合理,翻到第几页就存第几页。我把这个方案放在脑子里转了一圈,发现了问题:Canvas 排版是动态计算的,手机和 iPad 的 viewport 不一样宽,同一段文字在手机上是第 47 页,在 iPad 双页展开时可能是第 23 页。pageIndex 在设备切换之后就失效了,进度会乱。
真正稳定的定位应该是基于内容本身,而不是排版结果。我让 AI 换了方向,用 block anchor,也就是把进度绑定到 EPUB 内部的某个内容节点上,这个节点不会因为屏幕宽度变化而改变。这个方案 AI 第一轮给不出来,因为它不知道”排版会变”这件事在实际使用中意味着什么。
依然需要改进的事情
首先,我感觉50% 的时间我都只是在推进AI做这件事情,没有真正的深度思考。
当然我自己也做了一些贡献,比如说我规定了技术栈,规定了架构,然后思考了一些困难需求怎么解决。但是大部分都是我去问AI,让AI帮我推荐,然后我自己做甄别。
我感觉我失去了一部分思考的能力。我认为这是不太好的一件事情,毕竟人是需要大量思考的,否则就像和刷短视频一样,没什么区别,是沉浸在一个快速实现一个功能的美梦中而已,并没有真正地去解决问题。
还有就是写这个只是满足了我的一个需求,但其实按道理来说,我应该是以市场需求为主,而不是以自己的需求为主。
写这篇文章也只是为了停下来思考这件事情。要让自己多思考,而不是只是单纯地被AI推着走。
更形象一点的描述就是如这张图一样,我经常会开多个tab去,或者开多个terminal一起去并发需求。但是感觉这样其实并没有提升太多的效率,反而让我变得更疲惫,并且让我没有办法更集中地思考,解决更难的问题。
下面这张截图也是推特上一个年轻人所和我一样有相同的感受。他认为Cloak Code之类的Agent让人们更少的思考,损害我们的大脑。毕竟我们大脑只有思考才会进化,如果不思考的话,就会变得愚笨。