项目进展2026-09-21

志睿源官网|静态资源「缓存指纹」补齐 上线报告

官网共享内核文件补齐内容指纹:内容变则 URL 变,「改完即生效」不再依赖用户手动刷新或清缓存。

  • 日期:2026-09-21
  • 起因:老板问「Ctrl+F5 都没刷新,是我在菜单里清理缓存后才看到」
  • 结论:服务端缓存策略正确、Ctrl+F5 机制上有效;真正的缺口是 /shared/ 下两个共享文件没有内容指纹,已于本轮补齐

一、老板的疑问:为什么 Ctrl+F5 不管用,清缓存才行

先给实测答案,再给机制。

1.1 服务端是清白的(实测)

对 /shared/md/md-enhance.js 做三种请求:

请求方式响应判断
无条件请求(模拟清缓存后)200 + 新内容✅ 正确
带错误 ETag(模拟旧副本)200 + 新内容✅ 正确
带当前 ETag304 Not Modified✅ 正确

nginx 侧也没有 open_file_cache(不会用旧 stat 算 ETag)。服务端没有任何"发新不发旧"的问题。

1.2 Ctrl+F5 机制上确实有效(实测)

用一个可识别内容的探针文件,模拟真实场景「浏览器已缓存旧副本 → 服务器已更新」,在真实 Chrome 里逐条测:

步骤探针值网络行为
① 首访(服务器=A)A200,缓存 A
② Ctrl+F5B ✅200,一次 hard reload 就切到新版
③ 普通 F5B ✅304(②已把本地副本更新)
④ 重新导航B ✅304
⑤ 清缓存 + F5B ✅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 的动态 importimport('/shared/md/md-enhance.js')(裸路径)拿不到指纹
md/index.htmlmd-enhance.css?20260918c(手工号)本次没更新 → 等于没变
md/js/app.jsimport '.../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 里的共享 cssmd-enhance.css?v=0f6b9bac
zry/*.html 里的 site.jssite.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


五、三个坑(已同步进技能)

  1. S 的 IIFE 作用域 —— site.js 是两个独立 IIFE,第一个里的局部 S

在第二个里不可见。这个错误被 .catch 吞成 console.warn,页面静默不渲染, console error 里看不到。

教训:动态 import 的 .catch 里必须把原始错误打出来,否则"没报错"不等于"没失败"。

  1. scp 的端口参数是大写 -P —— 小写 -p 是"保留文件时间戳",

于是 -p ***** 会把 ***** 当成源文件报 stat local "*****": No such file or directory。ssh 才是小写 -p。

  1. 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 侧加载内核,与浏览器缓存无关
← 返回文章列表