今天突然又想记录点东西了
记录了把 Hexo 发布链路做成 Rust + Tauri 桌面端的过程:为什么坚持薄壳架构、front-matter 为什么不能做 YAML 往返、WebP 有损编码踩到的库边界,以及顺手挖出来的三个真 bug。
这个博客前段时间被我从备份里捞了回来,之后一路加东西:封面换掉失效图床、文章补 AI 摘要、关于页加贡献热力图、友链改成 Issue 半自动审批,构建和发布全部搬到了 GitHub Actions。前两天又把手机端接上了——Markor 写作、GitSync 推仓库。
剩下最后一块:桌面端。现在写文章得在编辑器里手改 Markdown、自己记住 front-matter 的格式和文件名规则,再切回终端跑命令。于是决定用 Rust + Tauri 做一个写作端。这篇笔记记录一下这次的设计取舍,顺便当作发布链路的测试样本。
一、先定架构:这是薄壳,不是重写
动手前最想清楚的一件事是:App 不要重新实现一遍发布规则。
目前仓库里已经有一套约定:正文不足 60 字算草稿、要拦住;AI 摘要只给没有 description 的文章补;手机拍的图片要先归位到文章目录;提交顺序必须是 add → commit → pull --rebase → push;推完还要盯 CI。这些规则分布在 tools/ 下的几个 Node 脚本里,CI 那边还会再拦一次。
如果 Rust 里再实现一遍,就会出现两份规则,改了一边忘了另一边,最后以哪份为准都说不清。所以 App 只做三件事:读写文件、启动子进程、展示日志;发布一律交给现成的 node tools/publish.mjs。
1 | ┌────────────────────────┐ ┌──────────────────────────┐ |
界面这一层用 Svelte 5 + Vditor。Vditor 是我特意选的:中文文档齐全、所见即所得/分屏/即时渲染三种模式都有,而且它的静态资源可以整个复制到本地,不依赖 CDN——断网也能写。
二、三个不敢拍脑袋的技术点
第一个是 front-matter 怎么写回去。 最容易踩的坑是用 YAML 序列化库解析完再整体写回,那样 layout: post 会丢、空 tags: 会变成 tags: []、双引号会变单引号,每篇文章都会产生一堆无意义的 diff。现在的做法是按行定位那四个键就地替换,其余字节原样保留,连 CRLF 都不动。测试里锁了一条:用空补丁保存,必须逐字节不变。
第二个是 WebP 的有损编码。 Rust 里最常用的 image 库能解 WebP,但只能无损编码,做不了质量参数。有损得靠 webp crate(底层是 libwebp),而且要绕开它对 DynamicImage 的依赖,直接用 RGBA 原始字节喂进去。参数定的是长边 1600、Lanczos3、质量 82;GIF 动图不转码,原样复制,免得丢帧。
第三个是编辑器里的图片预览。 WebView 里一张 <img src="photo.webp"> 是解析不到 source/_posts/<文章名>/ 的,得走 Tauri 的 asset 协议。更麻烦的是”即时渲染”模式保存时是从 DOM 反序列化回 Markdown 的,如果我们把图片地址改写成了绝对路径,存盘就会把绝对路径写进正文。所以最终用了分屏模式(源文本才是真源),牺牲一点手感换正确性。
三、顺手挖出来的三个真 bug
做这个 App 的过程中,发现发布脚本本身有几处会咬人的地方:
- 删除文章会让发布崩掉。 脚本第一步会遍历
git status里列出的每篇文章去数字数,文件已经被删了还去读,直接抛未捕获异常,整条流程中断。也就是说,之前想在网页上删一篇文章,光靠改仓库是发不出去的。 git status --porcelain -z的重命名记录是两段路径(R 新路径\0旧路径\0),按普通记录解析会把第二段切坏。改个文件名就可能让路径解析错位。- 用内置
GITHUB_TOKEN发起的 push 不会触发下游 workflow。 这条最隐蔽:友链机器人批准一条友链后会把数据提交回仓库,我一直以为提交信息里的[skip ci]是元凶,其实就算去掉也没用——GitHub 有防递归规则,用仓库自带 token 推的提交压根不会触发新的 workflow。真正的修法是显式调用workflow_dispatch。
顺带还发现,”比对构建产物有没有回归”这件事本身就不好做:随机封面每次构建都换 seed、页面里有个 data-lastPushDate 时间戳、marked 还会随机把邮箱字符转成十进制或十六进制实体。不先把这三样归一化,任何 diff 都会被误判成回归。
四、怎么验证的
发布相关的规则没法靠肉眼确认,所以主要靠两层:
一层是单元测试,覆盖 front-matter 的恒等保存、CRLF 保留、缺失键追加、mtime 冲突拒绝覆盖、路径穿越拦截、删除后回收站里有备份、PNG 转 WebP 后的尺寸与体积。
另一层更实一些:把仓库里真实的十几篇文章复制到临时目录,逐篇”读出来再原样存回去”,断言字节级完全一致。这条测试不碰真实仓库,但用的全是真的文章,能抓住那些”看起来没变、其实把引号风格改了”的问题。
五、还没做完的
- 发布过程中不能取消,界面没有取消按钮;
- 只能通过文件对话框插图,剪贴板粘贴和拖拽还没接;
- 删除后文件进了回收站,但 App 里没有恢复入口,得手工复制回去;
tauri build还没跑过,安装包和图标都没验证。
测试时往编辑器里粘了一张图,正好验证了保存前的兜底:粘贴的图会被自动转成 WebP 文件,正文里只留文件名。

同一张图通过「插入图片」写入资源目录的 16:9 版本(长边缩到 1600):

先把能用的部分用起来,剩下的边用边补。毕竟这个博客最初的目的就是记录,工具本身不该成为目的。
