2026 年 9 月 29 日。这几天没写多少「新功能」,大半时间在修自找的麻烦。把坑摆开来看,会发现它们不是孤立的雷,而是同一条纪律在不同层次上的缺席。这篇把它们归成几类,顺便记下我们正逐渐形成的「锁法」。
一、资产指纹:一处不同步,整站裸奔
最疼的一类,全栽在「同一份知识有两处定义」上。站点的每个静态资源都有指纹(哈希或版本号),但这个指纹在 asset-manifest.json、sw.js、SSR 首页、CSR 薄壳 index.html 四处各自存在。一旦其中一处没跟上,就是灾难:
- SW 换版裸站事故:老访客导航 SWR 首绘旧缓存文档(引旧指纹),新 SW 已 activate 删了旧缓存,旧指纹 CSS 冷缓存回源拿到当前字节 → 完整性校验
asset-claimed-mismatch硬拒 → 浏览器拒用样式表,整站丢样式。修复是在 A 类校验加了一条「claimed 不符但实算字节 == 表内当前指纹」的宣泄通路,外加 install 原子性(addAll 全成功才 skipWaiting)。 - CSR 薄壳指纹漂移:改完 site.css 只跑了 gen-manifest(更新 manifest+sw)却跳过 build-web,而 index.html 的指纹注入在 build-web 里做 → index.html 还引旧指纹。根治是把
render_shell()的指纹注入收口进 gen-manifest.py:manifest 变则 index.html 必同步。
教训:指纹必须在一个源头派生,靠流水线收口,而不是靠人记得多跑一个命令。这也催生了 scripts/verify.sh——部署后一键校验「路由降级头 + 指纹三处同源 + stale inode」,今早刚写完的。
二、前端 wasm:两个「看不见的坑」
1. Dioxus 0.8 的 #[layout] 是「布局栈」,不是「分段切换」。 多个平级 #[layout] 之间漏了 #[end_layout],内页会同时渲染两套导航(header 数量翻倍),跨 layout 切换状态错乱——点「首页」导航栏就没了。症状判据很直接:document.querySelectorAll('header.site-header').length 在 /posts 冷启动 == 2 即中招。
2. mem::forget 挂全局 click 闭包 + 信号值 drop。 DeployDropdown 用 mem::forget 让 document 级闭包在组件卸载后还活着,点「首页」触发路由切换 → 组件卸载 → 信号 Dropped → 闭包里 *oc.read()(即 try_read().unwrap())抛 ValueDroppedError panic → wasm panic=abort 崩掉整站。修复是换成 try_peek(),优雅放行。
教训:这两个坑都「看着像导航栏 bug,其实是 wasm 生命周期 bug」。这次我们把根因做成 native 行为测试锁死(zjchina-ui/src/signal_drop.rs),让它以后永远进不了生产。
三、部署与数据:小内存 VPS 上的地雷
1. 小机全量编译,饿死整机。 症状很迷惑:ping 通、TCP 22/80 能握手,但 banner/443 超时——不是进程死了,是用户态 CPU 被 cc1plus/rustc 抢光。止损是 pkill cc1plus/rustc/cargo/cmake,load 回落即恢复,不用重启服务。这也是 zjchina-render 要被隔离出根 workspace 的直接原因。
2. TrailBase 迁移的桩文件陷阱。 trail migration <name> main 会生成只含一行注释的桩文件,还把桩版本记进 _schema_history。正确的做法是直接手写 U<epoch>__<name>.sql 落真实 DDL;桩已入库的,补 DDL 再 DELETE FROM _schema_history WHERE version=<v>。
3. 名字不像它自己。 单元名是 zjchina-trailbase.service(不是 zjchina-trail),depot 在 ~/traildepot(不是仓库里 data-prod——后者是旧逻辑误建的空壳)。改名、探测、健康探针,一整套都不是「猜」,得照着真环境探。
四、架构兜底:多段未知路径返回 200
一个隐蔽的叠加 bug:Drogon 后端对非 admin 路径 serveShell,SSR 侧车对多段路径误传 fetch_post 解 JSON 失败返 500,最终 Drogon 降级回壳——多段未知路径错误返回 200,还带完整页面。修复是把「只有单段 /{slug} 或 /{posts}/{slug}」的判定做对,多段直接 404,并在 E2E 里补公开入口断言。
教训:两层兜底各自「仁慈」,叠起来就是「错误地成功」。兜底必须每层自报层级,不能静默。
五、今天刚踩的一个:bash 把全角标点吃进变量名
写 verify.sh 时,报 行 89: MVER: 未绑定的变量,但 bash -x 明明显示 MVER 已赋值。根因细到可笑:UTF-8 locale 下,bash 会把手写注释里紧跟 $MVER) 的全角右括号「)」的字节(0xEF 0xBC 0x89)并进变量名。一行 export LC_ALL=C LANG=C 根除。
这个坑提醒我们:问题最小的那一步,也可能来自最没想到的角落。
六、正在形成的「锁法」
坑踩多了,就有了对冲它们的套路。这几天逐渐沉淀下来的,是三条:
- 行为测试锁死根因。 不再靠「远程部署→冷缓存→重测」的循环复现,而是把根因翻译成 native 测试:信号 drop、layout 栈、指纹漂移,各有各的断言。
- 漂移锁自动化拦截。 契约 DTO 变了必须重跑
export-schemas提交 schema diff;指纹由键位派生、由 verify.sh 一键核对;「同一知识只有一处定义」从口号变成编译期/测试期强制。 - verify.sh 收口验收。 改一行 CSS 到全栈上线之间,不再手工 ssh tail+grep+pgrep 轮询,一条脚本跑完「路由 + 指纹 + 服务器侧」,把「改一行 → 遍地轮询」的积分消耗压掉。
坑不会消失,只会换地方出现。能做的,是让下一个坑在进生产之前就被一条测试、一把锁、或一条脚本拦住。
踩坑不是耻辱,重复踩同一个坑才是。我们把这几次的坑写下来,就是不想再来第二遍。