做手作的人多少都有个通病:创作的时候痛快,整理的时候遭罪。一年下来,手机相册里堆了几百张过程图,浏览器收藏夹里躺着几十个灵感网址,备忘录里还有一堆写着“上次那个深色木蜡油色号”的碎片。等到真要找一样东西时,翻来翻去半小时都未必找得到。后来我干脆自己动手,写了一个叫 ArCraft 的创作工作台。名字取了 art 和 craft 的组合,核心目标只有一个:让灵感素材、创作过程与成品记录不再散落在聊天记录、相册和备忘录里。这篇复盘会把项目的立项思考、数据模型、内容采集链路、部署运维和后续迭代完整过一遍,给同样想搭建自托管资料系统的创作者作参考。
1. 立项动机:创作碎片散落在四个工具里,我终于决定自己写一个
1.1 “artcraft”这个名字的含义,以及项目边界
ArCraft 这个名字没什么玄妙的典故。art 是艺术,craft 是手艺,拼起来就是“把艺术灵感落到具体工艺上”。它不是一个社交平台,不做电商导购,也不打算变成又一个云笔记。它就是一个面向手作人、插画师和独立创作者的轻量内容工作台,负责保存三类东西:灵感素材、创作草稿、成品档案。
这个边界是第一版就划死的。因为“创作工作台”一旦定义得太宽,很容易把用户管理、互动社区、拍摄剪辑等功能全部卷进来,项目会迅速失控。我给自己定的范围是:一个人或一个工作室小团队使用,登录后维护自己的条目库,能够打标签、传图、写正文、看历史版本、做检索。后来项目能顺利推进,很大程度得益于一开始就限制了这个边界。
1.2 我在实际记录创作时遇到的四类碎片
我做木艺器物,流程大致是:先刷到一张值得参考的国外作品图,截图存到手机;再在备忘录里记下木料种类和漆料色号;动手阶段每一步工序都要拍下来留档,方便之后复盘;最后成品图需要修图,准备发到社区。这四类信息分散在相册、备忘录、浏览器收藏夹和文件管理器四个地方。
真实的情况往往是,某天想做一件新的器物,想参考一下去年那件深色器物的上色效果,同时确认当时用的木蜡油品牌,却要在三四个地方来回找。相册里是图,备忘录里只有含糊的一两行字,浏览器收藏夹里躺着十几个未分类链接,根本对应不上。这个痛点足够频繁,频率高到我觉得必须用一套工具来收拢。
1.3 现成笔记、相册、文档工具差在哪一口气
我没有一上来就写代码,而是花了两周时间认真试用市面上的工具,想找一个“半成品”直接上手。结果没有一款产品能同时满足下面几个条件:图片可补充自定义字段,分类和多维标签灵活,正文支持轻量 Markdown,来源链接能保留,数据最好能导出。
对这轮试用过程,我整理成了一个小表格:
| 工具类型 | 适合干什么 | 实际缺什么 |
|---|---|---|
| 相册工具 | 按时间和地点管理大量图片 | 无法给图片补充“材料/技法/出处”等自定义信息 |
| 在线文档 | 成篇幅的长文与协作 | 打开慢、结构重,不适合高频碎片化记录 |
| 灵感收集工具 | 快速收藏网页素材 | 收藏之后没有创作侧流程,缺少过程跟踪 |
| 本地文件管理器 | 按目录归档 | 搜索和标签能力弱,多端同步麻烦 |
结论很清楚:我需要一个轻量的、以条目为核心的自建工具。数据模型和交互自己说了算,该存哪些元数据可以慢慢打磨。于是 ArCraft 正式立项。
2. 数据建模:一张条目表如何同时装下作品、素材和草稿
2.1 为什么不做作品表和素材表两张表
第一版我确实按习惯分了两张表,作品表存成品,素材表存灵感。写了两天就发现大量重复代码:列表页要渲染两种卡片、搜索要同时查两张表、回收站还得各写一套逻辑。于是第二版直接合并成一张 entries 表,用 type 字段区分类型,额外字段统一放进 meta 这个 JSON 列里。
CREATE TABLE entries ( id TEXT PRIMARY KEY, type TEXT NOT NULL, -- 'work' 成品 / 'material' 素材 / 'draft' 草稿 title TEXT NOT NULL, cover_url TEXT, content TEXT, tags TEXT, meta JSON, source_url TEXT, created_at INTEGER, updated_at INTEGER );合并表之后,列表、搜索、标签过滤、回收站全部只写一遍,作品与素材之间的互相引用直接用一个字段存目标条目 id,不用考虑跨表主键转换。实际用下来的感受是,这个合并远不止是少写代码,更重要的是心智负担降低:所有条目都用同一种方式看待,不用在“我这会儿处理的是作品还是素材”上多想一秒。
2.2 标签体系:词频统计做候选,人工规则兜底
标签是检索用的地基。第一版让用户手动打标签,结果是我自己都经常偷懒,大量素材堆在“未分类”里。后来改成半自动:新增条目时,后端在标题和正文里提取候选标签,按词频排序返回,用户勾选或补充后保存。
切词逻辑很基础,按空格以及常见的中英文标点分隔,再用一份停用词表过滤“关于、然后、这个、一个”这类噪声。中文没有用独立分词库,而是对标题做二元字符组切分,效果谈不上完美,但配合人工补充足够使用。真正有价值的往往不是自动切出来的“木蜡油”“榫卯”,而是“待制作”“已实现”“复购”这类状态词,所以我在保存流程里加了人工规则:含特定前缀的内容自动附加对应状态标签。
2.3 版本快照只存增量,不存全量
写作品条目时,描述和工序记录会被我反复改,于是必须要有历史版本回滚。最初想全量存版本,几十个版本下来体积膨胀得厉害,尤其正文还带着图片引用。后来改成增量的行级 diff:每条记录先存一个 baseline,之后每次编辑只记录与上一版的文本差异,配合一个递增 version 号。
读取历史版本时,从 baseline 开始按顺序把差异重放回去。这个方案对 Markdown 正文尤其合适,因为 Markdown 天然按段落组织,段落级 diff 语义清晰。为防止重放链过长,我每攒到 20 个增量就重新生成一次 baseline,并清理历史差异。实际使用中版本回滚非常顺滑,数据体积也比全量方案小得多。
3. 内容工作流:从浏览器剪藏到自动保存的完整链路
3.1 浏览器剪藏插件:正文提取和主图选择
ArCraft 使用频率最高的功能,是采集灵感。我正在浏览一个作品页面,点一下浏览器插件,页面标题、正文、主图、来源链接会在一两秒内变成一条 type=material 的素材条目,自动打上“待分类”标签,留到之后统一整理。
这里几个细节很关键。正文提取不能直接取整个页面的 innerHTML 存库,否则搜索时会被导航栏、页脚、推荐位噪声干扰,所以要优先解析到 article 标签或者主内容区块。主图选择优先取 og:image 标签,这是社交分享时专门声明给第三方用的主图,可靠性最高;没有时才退回到页面尺寸最大的 img,并把 alt 文本也记录进 meta,方便搜索。来源链接必须原样保存,这样之后从素材条目跳回原网页只要一次点击。
插件侧我只保留了收集功能,分类和打标签都放到 Web 端处理,这样插件的逻辑足够薄,不随功能迭代频繁发版。
3.2 图片压缩与三档缩略图,列表页加载终于不卡了
图片是素材和作品最主要的载体,原图直接传当然最省事,但列表页一旦有几十张原图,浏览器基本卡死。我在早期就做了规则:原图保留一份,同时生成 1280、640、320 三档缩略图。列表卡片用 320,详情页主图默认 640,点击放大才会加载原图。
| 缩略图宽度 | 使用场景 | 作用 |
|---|---|---|
| 320px | 列表卡片、搜索结果 | 大量图片同时加载不卡 |
| 640px | 详情页默认主图 | 清晰度和加载速度的平衡点 |
| 1280px | 大图预览前的用户主动操作 | 接近原图观感但不加载原图 |
| 原图 | 保存原始素材 | 后续修图、打印、出版可用 |
处理脚本的核心逻辑是按目标宽度等比缩放,文件名用时间戳加随机后缀生成,避免不同条目间冲突。JPG 固定 85% 质量输出;PNG 只有透明通道需要保留时才会输出为 PNG,否则一律转 JPG。这套规则跑完后,列表页加载速度提升了大约五倍,滚动浏览素材时再也没有卡顿感。
3.3 编辑器选型、自动保存防抖与本地草稿兜底
条目正文我用的是 Markdown 编辑器。选择它主要是因为创作者写记录时用到的就是标题、列表、引用、图片说明这几件事,Markdown 足够表达,而且段落式结构天然契合我前面做的增量版本快照。编辑器本身没有做深度定制,只保证三件事:粗体和标题快捷键、图片粘贴直传、自动保存。
自动保存的防抖时间我调过几次,最终稳定在两秒:编辑器内容一变化,延迟两秒请求保存接口;如果存接口失败,就把最新内容写入 localStorage,下次打开编辑器时优先从本地草稿恢复。这个双保险看起来简单,却在几次浏览器崩溃里真正救回了差点丢失的创作记录。具体实现是监听内容变化后启动一个 2000ms 的定时器,再次变化就重置,定时器到期才真正发起保存请求。
4. 上线后的运维补课:存储、搜索、备份三轮调整
4.1 本地目录迁移到对象存储,签名 URL 是怎么处理的
第一版所有图片都放在服务器本地目录,配合 Web 服务器直接托管,单机部署很顺。问题出现在换机器部署的时候:整个图片目录需要手动拷贝,一旦文件多了,迁移费时费力,而且本地目录方案将来也很难支持多端访问。所以后来把图片统一迁到了对象存储。
对象存储最容易踩的坑就是权限边界:桶不能完全公开读,否则图片链接等于裸奔;也不能完全私有,因为浏览器加载图片时需要带上鉴权信息。我的方案是预签名 URL——后端在返回条目列表时,为每张图和缩略图生成一小时有效期的签名链接;客户端直接使用签名链接访问对象存储,不需要接触任何密钥。上传也走后端换取上传凭证,文件流直接到对象存储,服务器不用转发大文件,既省带宽也降低负载。
升级时还踩过一个和 CDN 相关的小坑:签名链接和 CDN 缓存同时启用时,旧签名过期但 CDN 缓存还没过期,会出现加载混乱。单人自托管场景实际不需要 CDN,所以直接关掉,让签名 URL 直接访问存储源站。
4.2 搜索从 LIKE 到全文索引,中文标题如何切词
ArCraft 的条目到三千多条时,搜索框的响应明显变慢了。最初实现是 SQL 的 LIKE 模糊匹配,胜在简单,数据量小的时候毫无察觉,但数据量上来之后,每次搜索都相当于全表扫描,等待时间越来越难接受。于是把标题和正文字段换成了全文索引。
这里真正有坑的是中文标题。默认分词规则按空格和标点切分,英文内容没什么问题,中文标题则被当成一整段,检索效果很差。我的临时方案是,在写入索引前对标题做一次字符级二元切分,把“深色木蜡油”切成“深色 色木 木蜡 蜡油”这样的二元组再进索引;搜索时对查询词做同样的二元切分再查询。这种方法达不到专业分词库的语义效果,但对资料检索足够,而且不用引入额外依赖。还有一种容易忽略的情况:如果前端做了离线缓存,搜索请求和响应也要纳入缓存更新策略,否则离线状态下搜到的是旧结果。
4.3 备份策略与每周一次的恢复演练
自托管系统最怕的不是缺功能,而是数据全丢。ArCraft 里存的是几年的创作记录,一旦丢失很难重建。备份策略分了三层:数据库每天凌晨自动快照,保留三十天;文件对象存储开启版本管理,同时每天把增量部分同步到一台不常开机的设备;每周手动执行一次恢复演练,把最近一个快照恢复到测试环境,确认能登录、条目数对得上、图片能正常预览。
恢复演练是最容易被省略的环节。很多人做了备份但从不验证,真正需要恢复时才暴露问题。我演练时发现过一次恢复脚本里数据库路径写死成旧目录的错误,修复之后才真正放心。每次演练结束后,我会在日志里记录耗时和问题,下一次演练优先检查上一次出现过的问题项。
5. 真实使用反馈与下一轮迭代方向
5.1 用户反馈里最意外的三个需求
ArCraft 从个人使用扩展到几个朋友试用后,反馈方向和我预期差别还挺大。被问得最多的是导出功能,有人想把所有条目一键导成 PDF 或图片包,用来制作纸质归档;其次是批量导入旧笔记,大家手里都有几百条历史内容,不想一条条手写新建;还有人问能不能让工作室几个人在局域网内共同维护同一个素材库。
导出功能实现起来不算复杂:后端把条目按时间线渲染成 HTML,再通过无头浏览器打印成 PDF;图片包则遍历条目附件,按标签目录结构打包下载。批量导入则提供一个 CSV 模板,按标题、正文、标签、类型的格式解析,导入后先统一落为“待分类”标签,再交给用户批量修正。
5.2 从云服务优先转向本地优先的调整计划
这些反馈让我重新审视了 ArCraft 的架构方向。当前方案是后端跑在服务器上,浏览器通过公网访问,离线时完全不可用;而创作者恰恰经常在没网的环境里产生灵感,比如木工房、户外写生地。下一步我计划把数据层改成本地优先:本地数据库作为主存储,远端只做同步与备份;界面随时可以打开,网络恢复后自动把增量推送到服务器。
这个改动会牵动编辑器、图片存储和版本快照三条链路,短期成本不低,但更贴合项目的定位。毕竟这是给自己和身边少数创作者用的工具,数据先留在本地,比云端优先更让人安心。
最后再分享几个折腾完之后的实操体会。数据模型尽量早统一,我在作品表和素材表之间反复切换浪费了不少时间;图片缩略图一定要在第一版就做,等条目多起来再补,改动成本会翻倍;自动保存要做双层,服务器接口失败时本地草稿必须能兜底。如果你也在攒一个给创作者用的自托管工具,这三件事建议排进前三位。它们看起来不起眼,却是每天都会用到的基础能力。