给静态资源加版本号,修掉「服务端更新了但界面没变」

现象:CSS 更新并上线后,用户打开站点看不到任何变化,怀疑没部署。
实测服务器上的 style.css 已是新版(与工作树逐字节一致),真实浏览器
直连线上也拿得到新文件,所以问题在客户端缓存:

  · 页面 HTML 带 no-store,每次都重新渲染 → HTML 一直是新的;
  · 但 /assets/*.css|js 的响应没有 Cache-Control / Expires,浏览器只好
    退回自己的「启发式缓存」——在它自行估算的新鲜期内根本不发请求;
  · 而 <link href="/assets/style.css"> 的地址从不改变,文件换了浏览器
    也无从得知。于是出现「HTML 新的、CSS 旧的」,界面看起来毫无变化。

修法:新增 asset() 辅助函数,拼上 mtime + 文件大小做 cache busting,
文件名不变而内容一变、地址就变,浏览器只能重新拉取,普通刷新即生效。
版本号同时带上文件大小,挡住「mtime 没变但内容变了」这种漏网情况;
取不到文件信息时不加版本号,退化成原行为,不影响可用性。

已验证:线上 HTML 现在引用 /assets/style.css?v=…;真实浏览器直连线上
三个视口(1920/1280/390)均无横向溢出,资源全部 200;1920px 下顶栏品牌
与正文左边缘同为 420px,头像 26×26(源图 400×400)。
This commit is contained in:
2026-09-23 07:06:36 +08:00
parent a43dd06d1f
commit 43171597e2
2 changed files with 27 additions and 3 deletions
+23
View File
@@ -25,6 +25,29 @@ function url(string $route, array $params = []): string
return '/index.php?' . http_build_query($params); return '/index.php?' . http_build_query($params);
} }
/**
* 静态资源地址 + 内容版本号(cache busting)。
*
* 为什么必须有:CSS/JS 的响应没有 Cache-Control/Expires,浏览器会退回到自己的
* 「启发式缓存」——在它自行估算的新鲜期内**根本不发请求**。文件换了它也不知道,
* 因为地址一直没变。表现就是「服务端明明更新了,用户看到的还是旧样式」。
* 拼上随文件变化的版本号后,文件一改地址就变,浏览器只能重新拉。
*
* 版本号用 mtime + 文件大小:只用 mtime 时,同一秒内改两次会漏;
* 补上大小能挡住绝大多数「内容变了但 mtime 没变」的情况。
* 取不到文件信息就不加版本号,退化成原来的行为 —— 不影响可用性。
*/
function asset(string $path): string
{
$path = '/' . ltrim($path, '/');
$file = dirname(__DIR__) . $path;
$mtime = @filemtime($file);
if ($mtime === false) {
return $path;
}
return $path . '?v=' . $mtime . '-' . (int)@filesize($file);
}
function redirect(string $route, array $params = [], string $anchor = ''): void function redirect(string $route, array $params = [], string $anchor = ''): void
{ {
$location = url($route, $params); $location = url($route, $params);
+4 -3
View File
@@ -15,9 +15,10 @@ $__initial = $__name !== '' ? mb_substr($__name, 0, 1) : '·';
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"> <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title><?= e($pageTitle ?? '家庭出行记录') ?></title> <title><?= e($pageTitle ?? '家庭出行记录') ?></title>
<link rel="stylesheet" href="/assets/style.css"> <?php /* 静态资源一律走 asset():不带版本号的话,浏览器会一直用缓存里的旧 CSS */ ?>
<link rel="stylesheet" href="/assets/vendor/cropper/cropper.min.css"> <link rel="stylesheet" href="<?= e(asset('assets/style.css')) ?>">
<script src="/assets/vendor/cropper/cropper.min.js"></script> <link rel="stylesheet" href="<?= e(asset('assets/vendor/cropper/cropper.min.css')) ?>">
<script src="<?= e(asset('assets/vendor/cropper/cropper.min.js')) ?>"></script>
<script> <script>
window._AMapSecurityConfig = { window._AMapSecurityConfig = {
securityJsCode: '<?= e(app_config()['amap']['security_js'] ?? '') ?>', securityJsCode: '<?= e(app_config()['amap']['security_js'] ?? '') ?>',