志睿源官网|静态资源「缓存指纹」补齐 上线报告
官网共享内核文件补齐内容指纹:内容变则 URL 变,「改完即生效」不再依赖用户手动刷新或清缓存。
- 日期:2026-09-21
- 起因:老板问「Ctrl+F5 都没刷新,是我在菜单里清理缓存后才看到」
- 结论:服务端缓存策略正确、Ctrl+F5 机制上有效;真正的缺口是
/shared/下两个共享文件没有内容指纹,已于本轮补齐
一、老板的疑问:为什么 Ctrl+F5 不管用,清缓存才行
先给实测答案,再给机制。
1.1 服务端是清白的(实测)
对 /shared/md/md-enhance.js 做三种请求:
| 请求方式 | 响应 | 判断 |
|---|---|---|
| 无条件请求(模拟清缓存后) | 200 + 新内容 | ✅ 正确 |
| 带错误 ETag(模拟旧副本) | 200 + 新内容 | ✅ 正确 |
| 带当前 ETag | 304 Not Modified | ✅ 正确 |
nginx 侧也没有 open_file_cache(不会用旧 stat 算 ETag)。服务端没有任何"发新不发旧"的问题。
1.2 Ctrl+F5 机制上确实有效(实测)
用一个可识别内容的探针文件,模拟真实场景「浏览器已缓存旧副本 → 服务器已更新」,在真实 Chrome 里逐条测:
| 步骤 | 探针值 | 网络行为 |
|---|---|---|
| ① 首访(服务器=A) | A | 200,缓存 A |
| ② Ctrl+F5 | B ✅ | 200,一次 hard reload 就切到新版 |
| ③ 普通 F5 | B ✅ | 304(②已把本地副本更新) |
| ④ 重新导航 | B ✅ | 304 |
| ⑤ 清缓存 + F5 | B ✅ | 200 |
所以:在"标准导航式刷新"下,Ctrl+F5 是能拿到新版的。
1.3 那为什么老板会遇到「不生效」
真正的机制在缓存条目的有效期由"缓存当时"服务器给的头决定:
浏览器缓存一个资源时,会把响应里的 max-age / Cache-Control 一起存下来,
之后在"期限内"直接使用本地副本 —— 期间根本不会联网,
也就无从发现服务器后来把策略改成了 no-cache。
而本站在 2026-09-18 之前,/shared/ 并没有单独的 location,
.css / .js 命中的是通用规则 expires 30d(30 天强缓存)。
那次改动之前访问过站点的浏览器,手里就留着一条 30 天的条目 ——
在它自己到期之前,怎么刷都可能被它挡住。
清理浏览器缓存能解决,正是因为它直接删掉了这条条目,下次必然联网。
补充一句诚实的说明:hard reload 本身是会绕过缓存的(§1.2 已实测), 所以老板那次 Ctrl+F5 为什么没绕过,可能还叠加了操作场景因素 (例如不是通过"重新加载"进入页面、在多个标签间看的不是同一个、或页面从 往返缓存 bfcache 恢复)。但这些都只是猜测 —— 根因是"生效与否依赖用户手动操作"这件事本身,下节的修复就是让它不再依赖。
二、真正的架构缺口:指纹漏接
官网本来就有完善的版本号机制(_bust_cache.py):所有自有资源都带内容指纹。
/zry/css/base.css?v=a1b133f5
/zry/js/site.js?v=655dff5c ← 内容变了,URL 就变,用户零操作即可生效
/zry/js/page-article.js?v=483d8b3f
内容变 → URL 变 → 任何缓存层都无从复用旧副本。 这是"改完立刻生效"的根基。
而共享内核漏了:
| 引用位置 | 改前 | 问题 |
|---|---|---|
zry/{about,now,product,article}.html | /shared/md/md-enhance.css(无参数) | 拿不到指纹 |
zry/js/site.js 的动态 import | import('/shared/md/md-enhance.js')(裸路径) | 拿不到指纹 |
md/index.html | md-enhance.css?20260918c(手工号) | 本次没更新 → 等于没变 |
md/js/app.js | import '.../md-enhance.js'(裸路径) | 拿不到指纹 |
_bust_cache.py 的处理范围只覆盖 /zry/ 开头的 URL,/shared/ 的引用它完全看不见。
所以本次改的 md-enhance.js / .css 是全站唯一两个"改了但 URL 不变"的资源 —— 撞上历史 30 天缓存残留,就表现为「必须清缓存」。
三、修复(三项)
① site.js 的动态 import 接上现成的补号函数
文件里早就有一个 BUILD 号机制(S.assetUrl,注释写明"给动态载入资源补版本号"),
只是这行新加的 import() 没接上:
/* 改前 */
import('/shared/md/md-enhance.js').then(...)
/* 改后 */
var SS = window.Site;
var entry = (SS && SS.assetUrl)
? SS.assetUrl('/shared/md/md-enhance.js')
: '/shared/md/md-enhance.js'; /* Site 未就绪时兜底 */
import(entry).then(...)
⚠️ 这里踩了一个坑:该段代码在独立 IIFE 内,取不到上面那个局部的
S。 必须走全局window.Site(第 1 个 IIFE 结束时w.Site = S,先于本段执行)。
② _bust_cache.py 覆盖 /shared/ 并纳入全局构建号
RX_HTML从/zry/...放宽到/(?:zry|shared)/...→ HTML 里的共享 css 自动获得指纹- 新增
shared_deps():把shared/md/*.{js,css}纳入全局构建号计算
为什么必须要这一步:
site.js的版本号是全局构建号(任一前端文件变化即变)。 若shared/md/*不参与计算,就会出现"只改了 md-enhance.js、没动 zry 任何文件" 时 BUILD 不变 → 懒加载 URL 不变 → 仍旧命中旧缓存 —— 正是要消灭的"依赖记性"。
③ md 分享端补指纹
<!-- md/index.html -->
<link rel="stylesheet" href="styles.css?v=7e6203ec" />
<link rel="stylesheet" href="../shared/md/md-enhance.css?v=0f6b9bac" />
<script type="module" src="js/app.js?v=d5b80a82"></script>
// md/js/app.js —— 静态 import 的 URL 必须是字面量,故写死内容哈希
import { renderHtml } from '../../shared/md/md-core.js';
import { enhance } from '../../shared/md/md-enhance.js?v=4cf61fa2';
⚠️ 第二个坑:
md-core.js刻意不带版本号。 因为md-enhance.js内部是以裸路径引用它的(./md-core.js)。 若外部带?v=,二者就是两个不同的模块 URL → 加载两份、两个实例、状态不共享。 它的更新交给/shared/的no-cache保证即可。
四、验证
4.1 最终版本号(线上实测)
| 位置 | 值 |
|---|---|
zry/*.html 里的共享 css | md-enhance.css?v=0f6b9bac |
zry/*.html 里的 site.js | site.js?v=ab425d38(新全局构建号) |
| 动态 import 实际请求 | /shared/md/md-enhance.js?v=ab425d38 |
| md 端 | app.js?v=d5b80a82、md-enhance.js?v=4cf61fa2 |
4.2 端到端实测(真实 Chrome)
① 官网文章页 /zry/article.html?slug=mdreader-v110
200 /shared/md/md-enhance.css?v=0f6b9bac
200 /zry/js/site.js?v=ab425d38
200 /shared/md/md-enhance.js?v=ab425d38 ← 动态 import 已带上构建号
未渲染占位块: 0
图形 8 张:全部 FIT_OK(289 / 616 / 613 / 457 / 398 / 681 / 543 / 681 px)
console error: 无
② md 分享端 /md/
200 /md/js/app.js?v=d5b80a82
200 /shared/md/md-enhance.js?v=4cf61fa2
未渲染占位块: 0 console error: 无
4.3 部署一致性
17 个文件(12 个 html + site.js + md 的 4 个)本地与线上聚合 md5 完全一致:
85f7a01ef186f988ff3bd1256e96c241
回滚点:/data/_bak/zry_deploy_20260921_200359.tgz
五、三个坑(已同步进技能)
S的 IIFE 作用域 ——site.js是两个独立 IIFE,第一个里的局部S
在第二个里不可见。这个错误被 .catch 吞成 console.warn,页面静默不渲染,
console error 里看不到。
教训:动态 import 的
.catch里必须把原始错误打出来,否则"没报错"不等于"没失败"。
scp的端口参数是大写-P—— 小写-p是"保留文件时间戳",
于是 -p ***** 会把 ***** 当成源文件报
stat local "*****": No such file or directory。ssh 才是小写 -p。
md-core.js不能加版本号 —— 见 §3③,会与其他裸引用形成双实例。
六、以后怎么办(规范)
改 /shared/md/ 下任何文件后,必须做这两步:
cd C:/bzm/page/online
# 1) 重建版本号(共享文件变化会推动全局构建号,12 个页面同步)
python _bust_cache.py --apply
# 2) 部署(html 版本号变了,必须连带上传)
# 另需手工同步 md 端:md/index.html 的 app.js?v= 与 md/js/app.js 里的 md-enhance.js?v=
验收口径(一句话):
在 Chrome DevTools 的 Network 里看 md-enhance.js 的请求 URL ——
只要带了 ?v=,就说明指纹链路是通的;不带就是漏了引用点。
判据(可复用):
Ctrl+F5 能被绕过 ≠ 用户不会被缓存坑。凡是"改了要用户手动刷新才算数"的地方,都是指纹没接上。
七、影响面
| 端 | 是否受影响 | 说明 |
|---|---|---|
| 官网 zry | ✅ 已修 | 4 个页面 + site.js 动态 import |
| md 分享服务 | ✅ 已修 | index.html 3 处 + app.js/graph.js/parser.js |
| 鸿蒙 App | ❌ 不受影响 | 本地资源,不走 HTTP 缓存 |
| 服务端直出 | ❌ 不受影响 | Node 侧加载内核,与浏览器缓存无关 |