去年三月底,我把 Iris 仓库里的最后一个 issue 关掉,在 README 顶部加了一行“本项目已停止维护”,然后点了归档。那一刻说不上失落,更像是一种终于卸下担子的轻松。做这个项目的两年里,我无数次跟朋友解释 Iris 是什么,也无数次推翻自己的设计,最后决定让它停下。这篇东西,就是完整讲一遍:我为什么做了它,以及在什么节点、因为什么,我选择了停下来。
1. 先说说 Iris 是什么
1.1 起念:被阅读碎片化逼出来的需求
Iris 本质上是一个自托管的阅读标注工具,你可以把它理解成一个更私密、更可控的“稍后读 + 高亮管理”系统。当时我面临一个非常具体的问题:每天会花大量时间阅读技术文章、社区讨论和长文报告,但读完之后知识并没有沉淀下来。
我在浏览器里存了三百多个书签,分布在三个设备上,有的一直没读过,有的读完了没有标注,标注了又没有地方统一回看。Chrome 的书签文件夹乱七八糟,Pocket 之类的服务能收藏但高亮体验一般,Notion 用来记笔记又太重,记录一条灵感要建页面、填属性、分类别,等折腾完了写作冲动也过去了。
我需要的其实很朴素:看到一篇值得细读的文章,一键存进来,划几段高亮,顺手写两句想法,之后能按来源、按主题翻回去。这个需求市面上不是没有产品,但都让我在某一点上不舒服。要么数据在别人服务器上,要么订阅制年年涨价,要么导入导出格式不开放。于是“干脆自己写一个”的念头就冒出来了,这个念头最后变成了 Iris。
1.2 为什么非要自己造轮子
很多人听到“自建工具”的第一反应是:这不就是折腾吗?但我的判断逻辑是另一套。
我用过一个付费稍后读服务的三年订阅,期间它改版两次,改得越来越重,启动越来越慢,网页剪藏插件行为也越来越奇怪。每次它一变,我就得重新适应,数据躺在它那边,想迁出来却发现导出格式是一堆残缺的 HTML。这个体验让我彻底建立了对“托管类工具”的警惕:只要数据主权不在自己手上,你的工作流就永远建立在别人的商业决策之上。
我当时的想法很朴素:我需要的是一个能导出成纯文本和 JSON、能用 SQLite 文件自由备份、能在自己的服务器上跑、规则和存储都透明的工具。这些东西加在一起,市面上没有完全符合的现成方案,所以我就开始动手了。这个决定在今天看来依然是对的,哪怕 Iris 本身不再维护,它留下的那套数据模型和备份习惯,我到现在还在用。
1.3 功能定位:克制,是 Iris 的第一个设计原则
做 Iris 之前我给自己画了一条边界:它不做知识图谱,不做全文检索的 AI 问答,不做团队协作,不做浏览器内阅读。这些都是很吸引人的方向,但我很清楚,一个人维护的项目如果一开始就铺开这些,九成会死在半路上。
Iris 最终的核心功能只有四个:
- 通过浏览器插件或 bookmarlet 收藏网页文章;
- 提取正文,剔除导航、广告、页脚等噪音;
- 在正文上划词高亮,并添加随记;
- 按文章、标签、时间三种维度回看所有高亮。
这套功能合起来大概就是一张 A4 纸能写完的需求文档。克制看起来容易,但真正做的时候,你会碰到无数个“再加一个功能也不难”的诱惑。我后来发现,一个项目从失控到死亡,往往就是从某一次“顺手加个小功能”开始的。Iris 能活两年,很大程度上是因为我在功能范围上守住了底线。
2. 技术选型,每一个决定都是取舍
2.1 Go + SQLite + HTMX,为什么放弃大前端架构
Iris 的技术栈说出来可能有点“反潮流”:后端用 Go,数据库用 SQLite,前端主要靠 HTMX 加少量原生 JavaScript。没有 React,没有独立前端工程,也没有微服务。
选择 Go 的理由很实际。我需要一个编译成单个二进制就能部署的东西,不想在服务器上折腾 Node 版本、依赖安装和进程守护。Go 的部署产物就是一个文件,配合 SQLite 另一个文件,整个服务就是两个文件,备份时把目录打包就行。这种“一穷二白”的部署方式,对个人项目来说是最大的省心。
SQLite 则是刻意的反主流选择。很多人一听到数据库就想上 PostgreSQL,但 Iris 是纯单人、低并发的应用,数据量撑死几万条记录。SQLite 在这种场景下性能绰绰有余,还能获得零配置文件、单文件备份的巨大便利。我后来把整个 Iris 数据目录直接扔进 Synology 的同步文件夹里,每周自动备份,这个体验是任何客户端-服务器数据库都比不了的。
HTMX 的选择同样是从维护成本出发。Iris 的交互模式就是“点一下刷新局部区域”,HTMX 用服务端渲染加几个属性就能搞定,不需要写 API 层、状态管理、构建流程。省掉的这部分复杂度,对一个晚上十点以后才有空写代码的人来说,就是实打实的项目寿命。
2.2 数据模型里藏着的设计重心
Iris 的核心数据表只有四张:articles、highlights、notes、tags。真正的关键在于 high lights 表的设计,它不存偏移量,存的是选择文本的起止位置的锚点序列。
这里有一个我在开发中反复踩过的坑:网页文章的正文结构会变。文章改了排版、插了广告模块、换了 CDN,都会导致纯字符偏移量的失效。我最终采用的方案是存储“锚点片段 + 字符偏移”的组合,即记录高亮文本前后的短文本片段,加上片段内的相对偏移。做高亮回显时先找锚点片段,再定位精确字符位置,找不到锚点就退化到文本模糊匹配。
这个方案不算完美,但它在实用性和复杂度之间找到了平衡。相比“高亮即保存一张截图”这种取巧方案,它能保留选中文本本身,便于导出和检索;相比使用文档位置等高级标注协议,它又简单到我能独立维护得住。今天回看,这个数据模型是 Iris 里我最满意的一块设计——它解决的是一个真实且高频的问题。
2.3 被打回去重做的两次架构推翻
开发过程中我有两次比较大的推翻重做。
第一次是刚开始时我用了 Python 的 FastAPI 做后端。写了两三个星期后我发现,这个项目里异步任务几乎没有,Python 给我带来的便利远小于部署时的心智负担。在个人服务器上,Python 环境、虚拟环境、依赖版本,每一项都是摩擦。后来在某个周末,我把它整体用 Go 重写了一版,代码量甚至更少,因为去掉了很多为异步而异步的抽象。
第二次推翻是在存储层。早期我把高亮直接存在 JSON 文件里,觉得“反正自己用,怎么都行”。但写到后期,跨文件的关联查询变得越来越别扭,比如“找某篇文章的所有高亮”“找同一本书的所有随记”,JSON 手动遍历的代码写得又丑又脆。换 SQLite 花了我大概两个晚上迁移数据,换完以后整个代码库清爽了不止一个量级。
这两次推翻让我明白一个道理:个人项目不要一开始就追求“优雅”,但一旦发现某个技术选择成为持续的摩擦源,越早推倒重来越好。沉没成本会骗你继续走,但摩擦会真实地消耗你的维护意愿。
3. 从“能用”到“好用”,最花时间的三个环节
3.1 正文提取:永远有一万个意外
如果让我说 Iris 开发中最难的部分,正文提取绝对排第一。
现在的网页,有的正文在<article>标签里,有的要在一个嵌套十层的<div>里找,有的正文里有lazy load,图片和内容分段加载,有的本来就是单页应用,正文靠 JavaScript 渲染,服务端拿到的 HTML 里什么都没有。更麻烦的是各类“内容农场”站点,满屏的推荐链接、流量词、相关阅读,正文可能只有一千字,周围噪音有三万字。
我尝试过直接调用现成的解析库,比如 Python 生态里的 readability 算法。它对付老式网页还行,碰上现代前端站点就常常把侧边栏当正文。后来我采取了一个混合策略:优先找语义化标签,找不到就用内容密度算法评分,最后加一个手动规则覆盖特殊站点。这套逻辑最终把大多数技术网站的正文提取成功率拉到了九成以上,但代价是代码里有一堆针对个别站点的特判。
这部分的经验是:做内容相关工具,不要把“提取”想成一个通用算法问题,它在本质上是一个无限长尾的工程问题。认清这一点,就不会因为“为什么还有站点解析不了”而自我怀疑。
3.2 高亮同步:浏览器端的坑比想象中深
高亮功能本身不复杂,复杂的在于和页面生命周期共存。
第一个坑是动态内容。很多页面会在滚动到底部时继续加载新内容,如果你在页面上做的标记没有绑定到真实 DOM 节点上,内容一刷新标记就没了。第二个坑是 DOM 结构变动,评论区加载、广告位替换、图片懒加载完成,都可能把标记节点挤掉。第三个坑是 SPA 路由切换,在 Next.js 这类框架里,点击一个内链页面并不刷新,但 DOM 树会被整体替换。
我给 Iris 的浏览器插件做过一版 MutationObserver 的监控,在页面变化时重新挂载高亮节点。它能在多数场景下保住高亮不丢,但每次 DOM 大改的时候还是会撕裂半个页的样式。后来我换了个思路:高亮节点不直接用<mark>标签,而是用带自定义属性的<span>,配合 CSS 恢复选中文本的底色,这样做即使节点被脚本重排,也还有一层兜底能恢复样式。
3.3 部署和运维:长期项目的隐形杀手
Iris 跑在我一台树莓派 4B 上,一年多的运行时间里,我学到最重要的一件事是:个人项目的可靠性,由运维复杂度反着决定。
我最初把它包装成 Docker 容器,觉得“容器化部署更干净”。后来发现每次系统更新、Docker 版本升级、镜像重建都可能引入新问题,而我并没有精力时时维护。有一次树莓派断电重启后,容器里 SQLite 文件权限变了,服务起不来,我花了一个晚上排查才明白是挂载目录的所有者变了。那次之后我把部署方式改成了裸二进制加 systemd 服务,反而再也没出过问题。
数据库备份策略我一开始也很幼稚,直接定时复制.db文件。后来了解到 SQLite 的 WAL 模式下直接复制文件可能丢失最近事务,才改用.backup命令做一致性备份。我见过很多人自建服务最后死掉不是因为功能没做好,而是因为某次事故让数据丢了,信任瞬间归零。定期备份这件事,真得在设计的第一天就放进架构里,而不是运行三个月后再补。
4. 停下来:不是失败,是判断错了问题
4.1 时间账算不过来的那一刻
转折点出现在去年年初。我在某个周日做了一次完整的开发排期,列出的待办有十几个,包括插件重构、全文搜索、标签批量管理、移动端样式适配,等等。我估算了一下,把这些全部做完大概需要四到五个月的业余时间。而实际上,过去半年里我平均每周能投入 Iris 的时间,大概只有四到五个小时。
这意味着 backlog 会越积越多,而每个新点子进来,都会把它顶上来的情绪压下去一点。我开始感觉到一种以前没体验过的状态:打开项目不是兴奋,而是愧疚感。明明知道该修一个 bug,却有大量别的事情等着处理。当“写代码”变成“还债”,这个项目在心里的权重就开始翻转了。
我不认为维护慢就是失败,但“长期只能维护、无法向前演进”对个人项目来说是一种非常危险的状态。它会慢慢偷走你最初做项目的热情,最后把作品变成负担。
4.2 更扎心的发现:工具没解决真问题
真正让我下决心停下的,是一次冷静的使用数据分析。我导出了 Iris 里两年的全部数据,统计发现:我存了 860 多篇文章,真正读过一半以上的不到四成,写过随记的只有 70 篇左右,而回头翻看过旧高亮的次数,一年不超过五次。
这个数据非常残酷。它说明我的核心痛点不是“缺少一个好的高亮管理工具”,而是“看得太多、记得太少、回顾更少”——这本质上是一个注意力和使用习惯问题,不是一个软件设计问题。我可以把 Iris 打磨得再好、再顺滑,也改变不了这个根本行为模式。
意识到这一点后,我反而释然了。继续完善 Iris 就像给一个不看书的用户做更漂亮的阅读软件,是在错误层面上解决问题。工具能降低行动的摩擦,但它不能替代行动的意愿。这个洞察至今还在影响我怎么使用其他知识管理产品。
4.3 决定停更那天,我做了什么
决定停下之后,我没有直接删库,而是做了一整套收尾动作,在这里也分享给大家参考。
先把代码仓库设置为只读归档,README 顶部明确标注“不再维护”。再把所有数据导出成三种格式:原始 SQLite 文件、JSON 全量导出、以及一份 Markdown 目录结构。我篹建了一个简单的脚本,把每一篇文章的高亮和随记渲染成独立的 Markdown 文件,这样即使未来没有任何代码,我用文件管理器也能翻回过去的内容。最后写了一篇简短的项目结项说明,把架构文档、数据模型和已知问题梳理好放进仓库。
这些收尾动作花了我大概一个周末的时间,但价值很大。归档后的 Iris 不是一个“烂尾工程”,它是一个完整交付过的、有始有终的作品。我可以大方地在简历和作品集里提到它,介绍它的架构决策,解释我从里面学到的东西,这比项目遥遥无期地挂着要有尊严得多。
5. 复盘:如果再让我做一次,会有哪些不同
5.1 用“最笨的闭环”验证需求,而不是先造完美工具
如果重来,我会先用最笨的方式验证需求:手工复制粘贴文本到本地文件,用最原始的目录和标签体系管理一个月。确认自己在这个流程下真的有稳定的阅读和回顾习惯之后,再动手写代码。
我当时是反过来的,在习惯尚未形成时,先花大量时间造工具,这本质上是拿工程上的勤奋掩盖行为设计上的懒惰。工具能给行为提供脚手架,但真正决定价值的永远是行为本身。这个顺序一旦搞反,项目再精致也逃不掉空转的结局。
5.2 面向标准的开放性,优于自创一切格式
Iris 在高亮数据格式上自创了一套锚点方案,这在当时解决了问题,但今天看,更聪明的做法是直接用生态里已有的标准协议来做标注底层,把自有层只建立在标准之上。
标准的好处在于生态和工具链。用它,意味着我能直接用别人做好的解析器、前端渲染组件、移动端落库逻辑,而不需要自己维护一个只有自己用、文档只有自己能看懂的封闭格式。个人项目本来就缺人维护,能借用生态的力量就一定要借,所有“自己发明一套更好”的冲动,最终都会变成维护成本回到你头上。
5.3 维护策略比功能清单重要得多
我过去维护 Iris 的方式是“想起来了就做一点,有灵感就加一块”,这种节奏看似自由,实则让项目永远处于一种没有稳定边界的熵增状态。如果当初从第一天就定下一个更现实、更有持续性的维护策略,比如“每季度最多完成两个功能,其余时间只修影响使用的 bug”,项目可能会活得长很多。
对业余项目来说,维护计划的意义不在于强制执行,而在于给自己设一个边界,让项目不会无限膨胀。边界没有设好,项目会从热爱变成消耗,而消耗到最后往往就是一键归档。我很后悔没有更早给 Iris 设这个边界,这个教训后来用在了别的项目上,效果立竿见影。
现在偶尔还有人通过 GitHub 邮件询问 Iris 是否还有计划更新,我会如实说已经停了,并附上数据导出的方案。我仍然觉得做过它是一件特别好的事,它让我真正理解了项目停掉的那一刻,不是失败的时刻,而是你终于诚实地评估了它的价值。如果你也有一个正在犹豫要不要停下来的项目,我的建议是,把“维护意愿”当作一个和代码质量同等重要的指标去认真对待,适时停下,同样是项目生命周期里一个漂亮的里程碑。