1.0.9 静态资源传输层优化 + 页脚版本号 + logo 回概述

## 性能:瓶颈在传输,不在代码

先量后改。helper 冷启动 0.44s(编译 22173 行)看着像瓶颈,但
drouter-helpd 是常驻守护,请求走 Unix socket,正常路径根本不 fork;
实测各 API 已是毫秒级。app.js 语法编译 18ms、顶层执行 18ms
(393 个顶层函数 / 13401 行)—— JS 执行不是瓶颈。

真瓶颈是 app.js 690,482 字节每次重传。**决定不拆包**:JS 执行只 18ms,
拆包引入首屏 waterfall 的风险大于收益。改走三条:

1. 缓存策略按「有没有指纹」区分(不是按扩展名)
   - 带 ?v=<mtime> → public, max-age=31536000, immutable
   - 没指纹 → 仍 no-cache(否则部署后用户拿到旧文件)
   - index.html → 永远 no-store(每次都要重新注入)
2. 编码协商 br > gzip > 原文,**逐级降级**
3. 压缩结果按 (path, mtime, size, enc) 缓存

真机实测:首次 690,482 → 200,137 字节,第二次打开 0 字节(304)。

## 逐级降级不是可选项

目标机**没装 python3-brotli**(只有 C 库 libbrotli1)。最初写的
_brotli_cached 在模块缺失时 `return data, None`,而 None 是 encoding,
调用方理解为「压不动,原样输出」—— 于是 583KB 裸传,**比优化前更慢**。
改成 (None, None) 明确表示「这条路不通」,由 _encode_body 继续试 gzip。

同时修:Accept-Encoding 探测必须认 q=0(br;q=0 是「明确不要」,
子串匹配会正好发给解不开的客户端 → 满屏乱码);编码必须进缓存键
(共用键会互相覆盖);brotli 固定 q5(q11 只省 10KB 却要 1.5-3s)。

## 宽 except 把 bug 装成了特性

兜底只接 ImportError 这类**真·预期**的缺失,不接 TypeError/ValueError
(「str 传给要 bytes 的接口」也是 TypeError,接了等于没接)。
早先的 `except Exception` 吞掉了一个 NameError(漏注入 _comp_lock),
表现是压缩功能完好但从不生效,页面照常能开、只看响应头的判据完全发现不了。

## 版本号:建立单一真源 packaging/VERSION

原来版本号在 4 个构建脚本各写死一个默认值 + openapi 里硬编码。
漏改一处就产出「文件名 1.0.9、control 里 1.0.8」的包 —— apt 按
control 认版本,dpkg -l 永远显示旧版且升不上去。

index.html 的 ETag 并入版本号:它发出去的是注入后的内容,
ETag 却按原文件算,只改 VERSION 文件时 ETag 不变,客户端换到 304
继续用旧副本,页面版本号永远停在旧版。

## logo 回概述

用 <a> 而不是 <div onclick>:Tab 能聚焦、Enter 能触发、读屏会念「链接」。
已在 dash 页时不重复 go()(go 会 stopPageTimers + 重拉数据)。
CSS 显式去 text-decoration/color(UA 样式表不声明仍生效),
:focus-visible 焦点环保留(纯键盘用户唯一的路标)。

## 顺带修掉一个已发布版本的真 bug

_vpn_env 里 os.path.join('/lib/modules', ...) 被留成了 '/nonexistent'
(反向验证脚本被 SIGTERM 打断,finally 没执行,注入留在源码里),
于是 mod_file 永远为空、**need_module 整个状态从四态里消失** ——
这正是 v1.0.8 release notes 声称修好的那个 bug(PVE 的 KVM 虚机内核
明明有 wireguard.ko.xz 却报 unsupported,前端配一句写死的
「通常说明跑在精简容器里」把排查方向带偏)。

新增 _dev/t-worktree-clean.py 扫注入哨兵,放在 sync.sh 所有判据之前 ——
基线被污染时,后面每一条红都不可信。

## 判据

新增 t-static-perf.py(42 项静态)、t-encoding-runtime.py(26 项**真跑**,
覆盖 9 种 Accept-Encoding 组合、brotli 缺失降级、两种编码不互相覆盖、
压不动直出、内部错误不被吞)、t-worktree-clean.py。
三个都做了反向验证,注入后确实变红。

反向验证本身也踩了坑:判据里 len(body) 在 body 为 None 时先崩 ——
退出码非 0 但没有 FAIL 行,看着像「跑了」,实际没人知道它验出了什么。
取长度一律过 _sz() 包一层。

全量回归 54 个判据 0 失败;真机验收 22 项全通过;
前端三查 47 视图 0 异常、26 项移动端适配全过。
This commit is contained in:
liuzhuohua committed 2026-10-04 10:27:02 +08:00
1 parent 2c92cc9b28
commit ed5eb4428b
22 files changed
+1036 -55

No files matched your search

+1 -1
View File
@@ -65,7 +65,7 @@ if (!fs.existsSync(STRIPPED)) {
// ---------- 3) 剥后关键 token 数量不该变多 ----------
// ⚠️ 判据是「只多不少」,**不是**「完全相等」。
// 写死相等会红:注释里经常提到这些标识符(比如「这里之前用
// innerHTML 于是…���这种讲 bug 来历的话),剥注释会连带剥掉它们。
// innerHTML 于是…这种讲 bug 来历的话),剥注释会连带剥掉它们。
// 而「变多」才是误吃的信号 —— 原文被切开重组时 token 数会膨胀。
const TOKENS = ['function', 'const', 'innerHTML',
'addEventListener', 'querySelector'];
+17 -2
View File
@@ -43,6 +43,11 @@ PY
"$PYBIN" _dev/t-env-path.py || { echo "❌ 环境契约预检失败"; exit 1; }
# 打包产物结构自检:deb 元数据 / 维护脚本 / Dockerfile(挡住「打出来的包不能用」)
"$PYBIN" _dev/t-packaging.py || { echo "❌ 打包自检失败"; exit 1; }
# 工作区残留预检:反向验证被 SIGTERM 打断时 finally 不执行,注入会留在源码里。
# 这不是保险 —— 2026-10-04 就因为它,helper 的 _vpn_env 把 /lib/modules 写成
# /nonexistent,need_module 整个状态从四态里消失,而这份缺陷**已经发到 v1.0.8**。
# 放在所有判据之前:基线被污染时,后面每一条红都不可信。
"$PYBIN" _dev/t-worktree-clean.py || { echo "❌ 工作区有注入残留,先还原再继续"; exit 1; }
# 防火墙日志「为什么一条都没有」的状态判定:规则未载入 / 开关未开必须分开提示
"$PYBIN" _dev/t-fwlog-state.py || { echo "❌ 防火墙日志状态预检失败"; exit 1; }
# 外置存储(USB/Type-C/雷电):格式化的三重保护是最容易造成不可逆损失的地方
@@ -73,8 +78,15 @@ PY
"$PYBIN" _dev/t-helpd.py || { echo "❌ 常驻执行守护预检失败"; exit 1; }
# 统一日志:conntrack -E 是阻塞事件流,曾经让每次查询固定白等 2 秒
"$PYBIN" _dev/t-ulog-perf.py || { echo "❌ 统一日志性能预检失败"; exit 1; }
# 响应压缩与 ETag 协商缓存:少了 Vary 会让代理把 gzip 喂给不支持的客户端
# 响应压缩与 ETag 协商缓存:少了 Vary 会让代理把 gzip 喂给不支持的客户端。
# 压缩策略本身(长缓存 / brotli 协商 / 逐级降级)由 t-static-perf.py 静态查、
# t-encoding-runtime.py 真跑一遍验证。
"$PYBIN" _dev/t-gzip.py || { echo "❌ 响应压缩预检失败"; exit 1; }
# 静态资源传输层:指纹长缓存、brotli 协商、版本号单一真源、logo 回概述。
# 这几条都是「不报错但功能悄悄失效」的类型 —— 压缩不生效、版本号停在旧版,
# 面板照常能开,只有看传输量才发现得了。
"$PYBIN" _dev/t-static-perf.py || { echo "❌ 静态资源传输层预检失败"; exit 1; }
"$PYBIN" _dev/t-encoding-runtime.py || { echo "❌ 编码协商行为预检失败"; exit 1; }
# 模块联动:前端「保存/应用」要处理的模块,后端必须接得住。
# 曾经 system / pppoe / portfwd 三个页面点了应用直接弹「未知的模块」。
"$PYBIN" _dev/t-linkage.py || { echo "❌ 模块联动预检失败"; exit 1; }
@@ -157,7 +169,10 @@ PY
echo "=== 打包源码 ==="
# docs 也要带上:deploy.sh 会装到 /opt/drouter/docs(systemd 的 Documentation 指向它)
tar czf /tmp/drouter-src.tgz backend web scripts docs 2>/dev/null
# packaging/VERSION 也必须带上:它是版本号唯一真源,deploy.sh 装到
# /opt/drouter/VERSION,页面底部的版本号与 openapi 的 version 字段都读它。
# 漏了它不会报错,只会让真机一直显示兜底硬编码的旧版本号。
tar czf /tmp/drouter-src.tgz backend web scripts docs packaging/VERSION 2>/dev/null
ls -la /tmp/drouter-src.tgz
echo "=== 上传 ==="