1. 项目整体设计与命名哲学
1.1 项目代号从哪来:为什么叫"caveman"
做这个小项目的时候,我花在起名字上的时间比预想的多。最初想了十几个候选代号,什么"NoteHub""TinyMind"之类,看起来都很现代、很互联网。但产品形态越清晰,这些名字就越不对劲。最后动手做第一版原型那天,我在纸上画了一张使用流程图,整张图只有三个箭头:存进去、找出来、结束。看着这张图,脑子里突然蹦出一个词:caveman——穴居人,原始人。这名字一下就对了。
caveman这个词用在软件项目上,通常不是什么好话。它经常被人用来形容代码质量差、只会复制粘贴、不懂设计模式的开发者。但我觉得"原始人"背后其实藏着一个更值得琢磨的意象:原始人没有那么多工具,他手里只有一块石头、一根木棍,但他能靠这两样东西活下来。我的项目就是想做成这样的东西——只保留最必要的功能,每个功能都硬得能锤碎核桃,其他乱七八糟的全砍掉。
这个项目不是某个大厂出的产品,也不是要跟别人竞争的框架,它是我给自己写的极简命令行工具集,代号就叫caveman。核心价值一句话概括:用最少的外部依赖、最少的操作步骤,完成个人知识碎片的收集、定位与回顾,并且保证五年后这台机器上的文件还能打开、还能用、还能改。
适合谁参考?两类人。第一类是日常需要处理大量笔记、书签、待办信息,但已经厌倦了臃肿笔记软件的人;第二类是正在做自己的小工具、小脚本,但控制不好功能边界的开发者。这个项目里我走过的弯路、砍功能的过程,会比最终代码本身更有参考价值。
1.2 三个设计原则:用完即走、单文件交付、老机器友好
任何软件在动手写第一行代码之前,设计原则都是最重要的东西。没有原则,功能做三个月能发散出四十个,最后还是个四不像。caveman在设计之初就定了三条硬规矩,后续所有功能决策都用这三条来当尺子。
第一条,用完即走。工具不应该获得用户的注意力,它应该被使用然后被遗忘。搜索一个东西,敲完命令三秒钟出结果,看一眼,关掉,该干嘛干嘛。不做常驻后台,不做开机自启,不做消息推送。很多所谓"效率工具"本质上是在培养用户的依赖习惯,让你每天打开它、花时间在里面整理,这种工具做得再精致也是时间黑洞。
第二条,单文件交付。整个工具的代码打包成一个可执行文件,不依赖安装过程,不需要配置环境变量,不需要数据库服务。拷贝到任何一台电脑上,双击就能跑。为什么这么较真?因为我被各种软件的环境问题折磨过太多次。Python升级了某个库不兼容了,Node版本更新后依赖树崩了,CentOS和Ubuntu的包管理行为不一样了,这些破事在工具层面统统不应该出现。单文件交付意味着没有任何安装环节,也就没有环境问题可言。
第三条,老机器友好。工具本身要能在五年前的低配机器上流畅运行。这是一条自动筛选原则——它逼着开发者跟那些花里胡哨的功能说再见。富文本编辑器?砍掉。实时协作?砍掉。AI自动摘要?砍掉。这些功能不是不好,而是它们天生跟"轻量"打架。一条命令、一个文本检索、一个结果列表,这套东西用十年前的技术栈都能写得飞快。
三条原则定了之后,我意识到一件有意思的事:很多时候你以为你在做软件设计,其实你在做减法练习。caveman的整个开发过程,有一半时间花在说服自己"这个功能真的不能要"上面。
1.3 极简不等于简陋:两个概念的边界
这里必须说清楚一个误会:极简不代表简陋。没做过减法的人容易把"极简"当成"偷工减料",实际完全是两回事。
极简是功能经过筛选,每个被保留下来的功能都打磨到足够好;简陋是功能没想清楚,该做的事没做到位。我用一个对比表格说明区别:
| 评价维度 | 极简(caveman的目标) | 简陋(必须避免的陷阱) |
|---|---|---|
| 功能数量 | 少而精,每个功能解决一个明确问题 | 少但残缺,每个功能解决一半问题 |
| 交互成本 | 符合直觉,不需要读文档就能用 | 操作路径不连续,用到一半卡住 |
| 边界情况 | 报错信息清晰,知道怎么修复 | 静默失败,或抛出来自底层库的报错 |
| 扩展能力 | 留有合理的扩展接口 | 结构写死,想加功能只能推倒重来 |
| 维护成本 | 零依赖或极少依赖,升级风险低 | 依赖隐晦的全局状态,升级随时炸 |
caveman的功能数量控制在五个以内,但每个功能都覆盖完整的使用路径。比如"保存"这个功能,看起来只是往数据库里插一行记录,实际上包含了去重、标签解析、时间戳记录、格式校验四件事。功能可以少,但每个功能不能有短板。
这一点想明白了,后面所有的技术选型就都顺了。
2. 核心功能拆解与关键技术选型
2.1 功能范围定义:做什么与不做什么
动手之前,我先列了两张清单。第一张写"这个工具要做什么",第二张写"这个工具坚决不做什么"。两张清单一样重要,甚至后者更重要。
要做的有五件事:
- 记录:接受纯文本输入,自动附加时间戳和来源标签。
- 检索:按关键词、标签、日期范围三种方式定位内容。
- 回顾:随机或按标签抽取一条历史记录,用来做记忆强化。
- 导出:将全部记录导出为单个HTML文件,方便分享和长期保存。
- 自检:检查数据库完整性,修复异常索引。
坚决不做的有五件事:不做云同步、不做富文本格式、不做待办状态管理、不做移动端、不做多用户权限。每一项都有理由。比如不做待办状态管理,因为"收藏的内容"和"要做的事情"是两种完全不同的心智模型,混在一起会让工具的性质变得模糊。caveman的定位是"收件箱",不是"任务管理系统"。
这个功能范围花了很久才确定下来。一开始我也犯过老毛病,想着加上任务提醒该多好,加上阅读进度追踪该多好,加上浏览器插件又该多好。直到我承认一个事实:我需要的不是更多的入口,而是一个足够干净的出口——信息进来之后能快速被找到。
2.2 存储方案:为什么是SQLite而不是JSON或MySQL
存储层是整个工具的心脏,这个选择必须谨慎。我对比了三个方案。
方案A:JSON文件。最简单,人类可读,用一个列表存数据,追加就往数组里push一条。缺点是数据量上来之后每次都要全量读写,一百条记录无所谓,一万条记录就开始有明显的卡顿感。更麻烦的是并发写入会互相覆盖,虽然命令行工具基本不会并发,但这个风险始终让人不踏实。
方案B:MySQL/PostgreSQL。功能强大,支持完善,可变现的搜索索引方案一大堆。但问题是这个工具是单机命令行的,引入一个数据库服务意味着使用者必须先装MySQL,还要处理监听端口、账号权限、字符集配置这些问题。这跟"单文件交付"的原则正面冲突。
方案C:SQLite。完美匹配需求。它是一个嵌入式关系型数据库,数据存放在单个文件里,不需要任何服务端进程,Python标准库原生支持不需要安装第三方包,支持SQL查询所以检索能力远超JSON方案。
选SQLite还有一个隐性好处:它的数据文件本身就是跨平台的。Linux环境下生成的数据库文件拷到Windows上,工具一样能读。后期如果caveman要继续做导出、统计、批量操作,SQL能做的事比JSON多得多。SQLite官方文档第一句话说"大多数情况下,SQLite就是最好的方案"——在单机命令行工具这个领域,这句话不是广告,是事实。
实际测试了一组数据,一万条记录全量检索的时间在30毫秒级别,添加一条记录在5毫秒级别的响应,完全感知不到延迟。这个测试结果当时就坚定了用SQLite的信心。
2.3 交互方式设计:命令行的人机工程学
caveman是命令行工具,命令行的设计直接决定用户是用它还是放弃它。我参考了Git和Taskwarrior的设计经验,把交互规则定成三条:
第一,动词开头。所有操作都是"动作+对象"的结构:caveman add 这是一条记录、caveman find 关键词、caveman tags。从输入到执行不需要思考,动词即意图。
第二,输入最少。有默认值就不让用户填参,有自动检测就不让用户指定。比如添加记录时默认使用当前时间,用户不需要额外输入"现在"这个参数;搜索时默认搜索所有字段,用户不需要指定"搜标题还是搜正文"。
第三,输出可读。结果列表使用对齐的表格,高亮命中的关键词,末尾统计显示结果数量。错误信息必须说明"发生了什么+建议怎么做"。
这里有一个设计细节分享一下。find命令的模糊匹配逻辑,我一开始打算让用户手动指定匹配模式,比如"前缀匹配""后缀匹配""包含匹配"。但测试了两天后发现这种灵活性根本没人用,真实的使用场景里,用户想搜"react"就会输入"react",想搜"docker"就输入"docker",直接把输入当子串去匹配就够了。于是我把所有匹配统一成全字段子串匹配,用户侧少了一个选择题,代码侧也少了一个分支判断。好的交互设计,是把用户需要做的决定数量降下来。
2.4 技术栈选型:零第三方依赖的底气
技术栈的选择跟整个项目的调性高度相关。caveman最终用的是Python标准库加SQLite驱动,没有安装任何第三方依赖。Python的sqlite3模块是内置于标准库的,argparse管命令行参数解析,datetime管时间处理,这三个模块加在一起就覆盖了全部需求。
为什么不引入更现代的工具?比如用Click做命令行框架、用SQLAlchemy做ORM、用Rich做终端美化?这些库确实能让代码更好看,但代价是安装体积、升级风险、兼容性维护三座大山。一个只有几百行代码的小工具,引入SQLAlchemy之后光ORM的映射配置就要写几十行,属于典型的杀鸡用牛刀。
零依赖还有一个极端好处:这个工具永远不会遇到"依赖冲突"的问题。无论运行环境里的其他软件装了什么、卸载了什么、升级了什么,caveman的行为都不变。这种确定感在写工具类软件时非常重要——工具的本质是提高确定性,如果工具自身都不确定,那就没有存在的意义了。
3. 实操过程与核心环节实现
3.1 从仓库到首版:开发环境的搭建与目录结构
实际操作层面,我建议从一张干净的桌面开始。不要在已经跑了几十个服务的机器上开发这类工具,环境越干净,后期打包出来的产物越可靠。
我在开发机上做的事很简单:确认Python版本是3.8以上(因为我要用subprocess.run的新特性),确认文件系统支持SQLite的WAL模式(现在基本都支持),然后建一个目录开始写。
目录结构如下:
caveman/ ├── caveman.py # 主程序,单文件 ├── tests/ │ ├── test_add.py # 添加功能的单元测试 │ ├── test_find.py # 搜索功能的单元测试 │ └── test_export.py # 导出功能的单元测试 ├── docs/ │ └── usage.md # 使用说明 └── README.md我故意把tests目录放在项目根部而不是更深的层级,是为了让一个从未接触过这个项目的人在30秒内看到"这里有测试"这个事实。测试必须被看到,藏在src/tests/tests/里的测试等于不存在。
开发顺序上,我先把数据库层写出来——也就是连接、建表、插入、查询这四个基础操作。这四件事是地基,地基落定之后,上面的命令解析、输出格式化都只是拿来填墙的砖。想验证数据库层是否可靠,最简单的办法是跑一组边界测试:插入空字符串会怎样,插入超长文本会怎样,搜索一个不存在的词会怎样,删除空数据库会怎样。这组测试跑完,数据库层的稳定性心里就有底了。
3.2 数据库设计与建表语句
数据库设计是caveman里最需要认真对待的一环。表结构如果设计得不好,后面加功能必然要重构。caveman的库表很简单,总共两张表:记录表和标签表。
记录表,表名叫entries:
CREATE TABLE IF NOT EXISTS entries ( id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT NOT NULL, source TEXT DEFAULT 'manual', created_at TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_entries_created ON entries(created_at);标签表,表名叫tags:
CREATE TABLE IF NOT EXISTS tags ( id INTEGER PRIMARY KEY AUTOINCREMENT, entry_id INTEGER NOT NULL, tag TEXT NOT NULL, FOREIGN KEY (entry_id) REFERENCES entries(id) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS idx_tags_tag ON tags(tag);有两个设计考量值得说明。第一,标签单独建表,而不是在entries表里加一个tags字段存逗号分隔的字符串。这是为了支持按标签反查记录。如果是逗号分隔,查询"所有带python标签的记录"就得先把每行取出来再在应用层做字符串切割,效率低且代码丑。用关联表之后,一句SQL就能解决,索引还能加速。
第二,created_at存的是ISO 8601格式的字符串而不是Unix时间戳整数。ISO格式有个好处,它对人类可读,打开数据库文件直接用肉眼就能看到这条记录是什么时候存的。排序上ISO字符串按字典序就是时间序,不会出现大小比较的问题。这种字符串格式的缺点是需要转格式才能做复杂的日期运算,但caveman里日期运算很轻量,两条SQL就能覆盖,完全够用。
建表语句在程序启动时执行一次,IF NOT EXISTS保证幂等性,无论执行多少次都不会报错损坏数据。这个习惯在开发工具时值得保持:所有初始化操作都应该幂等。用户可能在任何奇怪的时机误点运行,程序不应该因为重复初始化而炸掉。
3.3 核心命令的实现与参数设计
接下来是三个核心命令的实现。第一个是add命令,它负责把一条记录写入数据库:
import argparse import sqlite3 import sys from datetime import datetime DB_PATH = "~/.caveman.db" def cmd_add(args): content = args.content if not content or len(content) < 2: print("错误:内容至少需要2个字符", file=sys.stderr) return 1 conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() now = datetime.now().isoformat(timespec="seconds") cursor.execute( "INSERT INTO entries (content, source, created_at) VALUES (?, ?, ?)", (content, args.source, now), ) entry_id = cursor.lastrowid for tag in args.tags: cursor.execute( "INSERT INTO tags (entry_id, tag) VALUES (?, ?)", (entry_id, tag), ) conn.commit() conn.close() print(f"已保存,ID={entry_id}") return 0这里用了?占位符的参数化查询而不是字符串拼接SQL。这个习惯必须养成,字符串拼接SQL不仅是安全漏洞,还会在内容里有引号时直接让程序崩溃。参数化查询同时解决了这两个问题。
add命令还设计了--source和--tag两个可选参数。--source用来标记来源,比如"memo"表示手机备忘录转存,"book"表示读书笔记,"idea"表示随手灵感。--tag可以重复传递,给一条记录加多个标签。这些元数据在后续的筛选检索中会发挥大作用。
第二个核心命令是find:
def cmd_find(args): conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() keyword = args.keyword like = f"%{keyword}%" query = """ SELECT e.id, e.content, e.created_at, GROUP_CONCAT(t.tag, ',') as tag_list FROM entries e LEFT JOIN tags t ON t.entry_id = e.id WHERE e.content LIKE ? OR e.source LIKE ? OR t.tag LIKE ? GROUP BY e.id ORDER BY e.created_at DESC LIMIT ? """ cursor.execute(query, (like, like, like, args.limit)) rows = cursor.fetchall() for row in rows: print(f"[{row[2]}] #{row[0]}") print(f" {row[1]}") if row[3]: print(f" 标签: {row[3]}") print() print(f"共 {len(rows)} 条结果") conn.close() return 0搜索逻辑的核心是LIKE模糊匹配,默认限制返回50条,避免刷屏。这里GROUP_CONCAT把多条标签合并到一行,避免一个条目出现多行重复。这个小技巧在处理一对多关系时非常好用,推荐记下来。
第三个核心命令是导出:
def cmd_export(args): conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute("SELECT id, content, source, created_at FROM entries ORDER BY created_at DESC") rows = cursor.fetchall() conn.close() html_parts = ["<!DOCTYPE html><html><head><meta charset='utf-8'><title>caveman 导出</title></head><body>"] for row in rows: html_parts.append(f"<h3>{row[3]}</h3><p>{escape(row[1])}</p><p><small>来源: {row[2]}</small></p>") html_parts.append("</body></html>") with open(args.output, "w", encoding="utf-8") as f: f.write("\n".join(html_parts)) print(f"已导出 {len(rows)} 条记录到 {args.output}") return 0导出的HTML用最简单的内联样式,不依赖外部CSS文件。为什么这样设计?因为导出的文件可能被保存到U盘、发到微信、用邮件传给朋友,如果CSS是外部引用的,接收方打开时样式就崩了。单文件、内联样式、UTF-8编码,这三个条件保证导出的文件在任何设备上打开都是可读的。导出功能的目的是数据逃离,而不是展示酷炫的界面。
主程序入口用argparse组织子命令。这个模块虽然用起来要写几行样板代码,但它的--help提示是自动生成的,用户输入caveman --help就能看到所有可用命令和参数说明。对命令行工具来说,帮助文档就是用户文档,这个能力不可或缺。
3.4 打包发布:让工具变成全局命令
写完成功能,还剩最后一步:让caveman在终端里可以用单命令调用,而不是每次都要写python3 /path/to/caveman.py。
Linux和macOS环境下,可以创建一个软链,或者在PATH目录里放一个启动脚本。我自己用的是在/usr/local/bin/caveman写一个三行脚本:
#!/usr/bin/env python3 import sys sys.path.insert(0, "/opt/caveman") from caveman import main sys.exit(main())然后给脚本执行权限,整个安装过程就完成了。没有复杂的安装向导,没有图形界面,没有注册系统服务,总共三步。
Windows用户的做法略有不同,会把caveman.py目录加入环境变量PATH,然后写一个caveman.bat批处理文件做转发。本质上思路一样,都是让系统能在任意目录找到执行入口。
发布时我还会做一件事:跑一遍完整的冒烟测试。从添加一条记录,到搜索它,到打标签,到导出HTML,再到用另一台没有Python环境的机器打开导出的文件,全流程走一遍。工具应该让用户稳定、预期一致地完成每个操作,发布前的冒烟测试就是保证"稳定"这个承诺的最后一道防线。
4. 常见问题排查与避坑实录
4.1 数据库文件损坏:意外断电与WAL模式
做caveman的过程中踩过的最大一个坑,是数据库文件的损坏问题。有一次在写入过程中,电脑突然断电,重启之后整个数据库打不开了。SQLite抛出来的错误是database disk image is malformed,这意味着数据库文件已经彻底损坏,连打开都做不到。
排查之后发现,问题的根源在于SQLite默认的日志模式是DELETE模式,它在每次事务提交时都要做一次文件的大面积写操作,断电时容易造成文件不一致。解决办法是开启WAL模式(Write-Ahead Logging)。WAL模式把写操作先放到一个独立的日志文件里,定期合并回主文件,这种机制对意外断电的容忍度要高得多。
开启方式很简单,只需要在创建连接后执行一行:
cursor.execute("PRAGMA journal_mode=WAL")这个PRAGMA语句是持久性的,设置一次之后,数据库文件会一直保持WAL模式,不需要每次启动都执行。
这个坑给我的教训是:工具类软件必须默认对意外场景有弹性,断电、杀进程、拔出U盘,这些都是真实世界中必然发生的事。设计阶段花一点时间考虑"如果这里失败了会怎样",比出了问题再修复的成本低得多。
4.2 中文搜索的编码与匹配问题
第二个典型的坑来自中文环境的编码。caveman的搜索依赖SQLite的LIKE操作符,而在默认配置下,SQLite的LIKE只对ASCII字符做大小写不敏感匹配,对中文则无所谓大小写问题,看起来没问题。但实际使用中发现一个奇怪的现象:有些中文内容明明包含关键词,LIKE却搜不出来。
深入排查后发现,问题出在Python字符串的编码处理上。当命令行参数从sys.argv进入时,如果终端环境的PYTHONIOENCODING没有设为UTF-8,某些特殊字符会被错误解码,导致传给SQL的查询条件跟实际输入不一致。症状表现为普通中文正常,但生僻字和特殊符号会搜索失败。
解决方法是程序启动时,在入口处显式设置标准输出和标准错误的编码:
import sys if hasattr(sys.stdout, "reconfigure"): sys.stdout.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")reconfigure是Python 3.7之后的标准库方法,能直接修改已打开流的默认编码。加上这行代码之后,中文搜索的异常就消失了。这类问题通常在开发机上永远不会暴露,因为开发者的终端环境早就配置好了UTF-8,但换到用户的机器上就会变成"怎么就坏了"。处理方式就是在开发时就假设目标环境是最麻烦的。
4.3 依赖膨胀的诱惑:从"顺手加一个"到"坚决不加"
这是我在整个项目里学到的最大一课,一定要单独拿出来讲。
开发caveman的过程中,我每隔几天就会冒出一个新念头:"如果能出一个漂亮的终端UI就好了""如果能自动从网页提取标题就好了""如果能生成统计图表就好了"。每个念头单独看都很有吸引力。但我给自己定了一条规矩:新功能提出后,先放进"待定清单"里,等两周再回头看。两周后如果仍然觉得非加不可,才允许考虑。
这条规矩救了我。事实是,清单里的点子两周后有90%会自动消失——因为它们并不是真正的痛点,只是开发者的创作冲动在作祟。剩下的10%里,又有大部分能找到更轻的解决方案,根本不需要新增功能来实现。
我亲眼见过很多开源项目是怎么变得臃肿的:作者说"我先加个小功能",小功能引进来一个小依赖,小依赖又要求某个版本的另一个依赖,半年之后,当初的轻量工具变成一个需要半小时安装配置的重型框架。项目死亡的方式不是被黑掉,而是被一点点喂胖到失去自理能力。caveman拒绝这种死法。
4.4 快速问题速查表
为了帮助其他想复刻这种极简工具风格的开发者,我把踩过的坑整理成下面的速查表,按优先级排列:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
数据库报malformed错误 | 意外断电、强制杀进程 | 开启WAL模式,定期备份数据库文件 |
| 中文搜索无结果显示 | 编码未显式设置 | 程序入口处设置stdout/stderr编码为UTF-8 |
| 运行时提示模块找不到 | 软链接或PATH配置不完整 | 检查启动脚本中的sys.path,使用绝对路径 |
| 命令运行非常缓慢 | 数据量大且缺少索引 | 按查询字段创建索引,添加LIMIT限制 |
| 导出HTML打开是乱码 | HTML缺少charset声明 | 确保<head>中包含UTF-8的meta标签 |
| 查询结果出现重复行 | JOIN导致一对多膨胀 | 使用GROUP_CONCAT或者SELECT DISTINCT |
| 插入内容时单引号报错 | 字符串拼接SQL导致 | 改用参数化查询,禁止任何形式的字符串拼接 |
这张表不覆盖所有情况,但覆盖了这个工具最可能遇到的七种问题。排查的顺序建议从前到后:先确认文件完整,再确认编码配置,然后看索引和查询逻辑。90%以上的问题都能在这一层解决。
5. 回顾机制:让工具从"仓库"变成"伙伴"
前面说了caveman的五个核心功能,其中有一个我觉得值得展开讲讲,就是"回顾"这个功能。它最初差点被我砍掉,后来成为整个项目里我最喜欢的功能。
回顾的机制不复杂,就是随机抽取一条历史记录。命令很简单:
caveman recall它从全部条目里随机选一条展示出来。也可以加一个--tag python参数,只在某个标签范围内抽取。用途是碎片时间的自我复盘——早上打开终端,敲一下caveman recall,随机弹出一条半年前记下的东西,可能是读书笔记的一句话,可能是一个没来得及做的想法,也可能就是当天的灵光一闪。
这个功能的原型版本只花了二十分钟就写完了,但它给我的使用感受完全超出了投入产出比。它的价值在于改变了工具的使用模式:CRUD类工具严格来说是让人存东西然后找东西的,但随机回顾把工具变成了一个"信息时间胶囊"。你不再只是存储和索取,你还会重新遇到你以为早就忘了的东西。
技术上,随机抽取的实现也很简单:
cursor.execute("SELECT content, created_at FROM entries ORDER BY RANDOM() LIMIT 1")一行SQL搞定。但因为数据量小,完全不担心性能。能用一行SQL解决的事情,就不用搭推荐系统。
如果你想做一个类似的工具,我强烈建议把"回顾"或"随机抽查"类功能纳入进来。它成本极低,但会以你意料之外的方式改变你记录的习惯——知道自己某天会随机翻到这些记录,你的写作方式都会变得更认真。
6. 最后几个值得带走的小技巧
这章是我做caveman整个过程中总结出来的一些散装经验,它们不构成体系,但每一条都在实际操作中帮过我的忙。
第一,给数据文件做自动备份。caveman的数据库文件就一个,我写了一个极简的备份函数,每次add成功提交事务后,顺带把当前文件复制一份带时间戳的副本到备份目录。一条记录、一个副本,多出来的成本微乎其微,但数据丢失这种灾难就再也不担心了。日常用笔记软件的人也应该养成类似习惯,定期导出备份比信任任何云服务都可靠。
第二,工具的配置量要趋近于零。caveman没有配置文件,DB_PATH这个常量写死在程序里。你可能想加个"-c config.yaml"来让用户配置路径,但仔细想想,哪个正常用户会需要同时使用两个笔记库?配置项存在的意义是解决真实的差异化需求,不是为了显得高级。
第三,代码行数不该成为改进目标。有些开发者喜欢炫耀"我只用300行代码就实现了XX功能"。行数可以作为参考,但不应成为目标。为了压缩行数把可读性牺牲掉,这种"极简"是走偏了。caveman的代码总量确实不大,但我写的时候优先保证的是每段代码读一遍就懂,行数是结果不是目的。
第四,把"删除"看得和"添加"一样重要。开发caveman的日子里,我做的最多的一件事是删除功能——删除没用的参数、删除不会走的代码分支、删除自我感动的过度设计。好的工具是雕出来的,先把石头削掉大部分,留下来的才是真正有用的躯干。对所有做工具、做产品的人,这句话都适用。
caveman这个项目现在还活着,我的日常笔记、书签、灵感,都流进那个小小的SQLite文件里。它不会自动帮我整理,不会主动推送提醒,不提供任何社交分享按钮。它只是在需要的时候被我敲一下,把我要的东西找出来,然后安静地等下次被使用。这恰恰是我当初给它起名叫caveman时想要的——像原始人手里的石头,粗粝、可靠,但足够迎战任何一场意外。