问题
主题(或单独的 CSS/JS 文件)更新后,用户刷新页面看到的还是旧样式。原因是浏览器缓存了旧的 CSS/JS。我们不可能要求每个用户手动清缓存,需要一套能让旧样式自动失效的机制。
核心原理
浏览器缓存是一张表:key 是完整的资源 URL(含 query string),value 是文件内容 + 过期时间。
页面引用 style.css 时,浏览器先查这张表:
- 表里有这个 key 且未过期 → 直接用本地旧文件,可能连请求都不发。
- 表里没有这个 key → 去服务器重新拉取。
关键点:style.css 和 style.css?ver=123 在浏览器眼里是两个完全不同的 key。query string 参与 key 的比对。
所以缓存破坏的本质不是去”清”缓存,而是换一个新 URL,让浏览器查表时”没见过”,从而主动重新请求。旧缓存查不到、用不上,自然被晾死。
解决方案:用 filemtime 自动生成版本号
思路是给资源 URL 拼上一个随文件修改时间变化的版本号。文件一改,修改时间变,版本号变,URL 变,浏览器重新拉取——全自动,不用手动维护版本号。
WordPress 的 wp_enqueue_style / wp_enqueue_script 第四个参数 $ver 会自动拼成 style.css?ver=xxx。但很多主题写死版本号,只改 CSS 不改版本号就失效。最稳的做法是用过滤器统一接管所有本站资源的版本号。
<?php/** * Cache busting: 用 filemtime 给本站 CSS/JS 自动加版本号, * 文件更新后用户自动拉取新版本。删除本文件即可完全回滚。 * 注意:依赖 HTML 本身不被页面缓存才能把新 URL 送到用户手里。 */add_filter('style_loader_src', 'dten_filemtime_ver', 9999);add_filter('script_loader_src', 'dten_filemtime_ver', 9999);function dten_filemtime_ver($src) { if (strpos($src, content_url()) === false) return $src; $path = str_replace(content_url(), WP_CONTENT_DIR, strtok($src, '?')); if (is_file($path)) { $src = add_query_arg('ver', filemtime($path), strtok($src, '?')); } return $src;}代码逐行说明
add_filter('style_loader_src', ...)/add_filter('script_loader_src', ...):WordPress 生成 HTML 时,每条<link>(CSS)和<script>(JS)的src都会经过这两个过滤器。优先级设为9999,是为了最后一个动手,盖过主题或其他插件加的版本号。if (strpos($src, content_url()) === false) return $src;:只处理本站wp-content下的资源。第三方外部资源(如外链字体库)原样放过,因为filemtime只能读本地文件。$path = str_replace(content_url(), WP_CONTENT_DIR, strtok($src, '?'));:从 URL 反推服务器上的物理文件路径。strtok($src, '?')砍掉可能已有的旧?ver=,str_replace把 URL 前缀换成磁盘路径前缀。if (is_file($path)):确认文件真实存在,防止路径推算失败时出错。filemtime($path):整个机制的心脏。 读取文件”最后修改时间”,返回一个 Unix 时间戳(如1718294400)。文件一被保存/覆盖,操作系统就更新它的修改时间,时间戳随之改变。add_query_arg('ver', ...):把时间戳拼成style.css?ver=1718294400。
完整因果链
- 覆盖/保存了
style.css→ 操作系统更新该文件的修改时间。 - 下一个用户访问,WordPress 生成 HTML,
filemtime读到新的修改时间 → HTML 里 CSS 的 URL 变成style.css?ver=新时间戳。 - 用户浏览器拿到新 HTML,看到没见过的 URL → 缓存未命中 → 拉取最新 CSS。
- 旧的
?ver=旧时间戳缓存还在,但再也不被任何页面引用,自然失效。
用户全程零操作,下次访问自动看到新样式。
代码该放哪里(重要)
这段代码的放置位置直接决定它会不会被覆盖、是否需要回滚:
| 放置位置 | 是否推荐 | 说明 |
|---|---|---|
第三方主题的 functions.php | ❌ 不推荐 | 主题一更新就被覆盖,改动全没。而这次的问题恰恰由主题更新引发,会反复踩坑。 |
子主题的 functions.php | ✅ 可以 | 不被主题更新覆盖,但前提是已经有子主题。 |
wp-content/mu-plugins/(must-use 插件) | ✅ 最推荐 | 一个 .php 文件丢进去即自动加载,无需启用、不受主题更新影响、不依赖子主题、出问题删文件就回滚。 |
只有在「这是自研主题」或「主题以后不再更新」时,直接改主题
functions.php才算合理。
SSH 一条命令部署到 mu-plugins(目录不存在先 mkdir wp-content/mu-plugins):
cat > wp-content/mu-plugins/cache-bust.php << 'EOF'<?phpadd_filter('style_loader_src', 'dten_filemtime_ver', 9999);add_filter('script_loader_src', 'dten_filemtime_ver', 9999);function dten_filemtime_ver($src) { if (strpos($src, content_url()) === false) return $src; $path = str_replace(content_url(), WP_CONTENT_DIR, strtok($src, '?')); if (is_file($path)) { $src = add_query_arg('ver', filemtime($path), strtok($src, '?')); } return $src;}EOF现成插件(可选)
如果想要后台开关、或交接给非技术同事维护,可用 Autover 插件:装上、启用即可,效果与上面这段代码几乎一致(同样基于 filemtime 给入队资源加版本号)。
对自己掌控服务器、看得懂 PHP 的情况,mu-plugins 那几行其实比装插件更轻、更可控——插件会多一层心智负担和兼容性维护。插件的价值主要是给”只会点后台”的人用的。
一个必须知道的前提
新的资源 URL 是写在 HTML 里的。所以整个机制的命门是:用户每次拿到的 HTML 必须是新生成的。
如果 HTML 本身被页面缓存了(例如 WP Rocket / W3 Total Cache / WP Super Cache 的 page cache),用户拿到的还是旧 HTML,里面写的还是旧 URL,新版本号传不过去——机制整个失效。
部署后务必验证:用隐身窗口打开页面,看源码里 CSS 的 ?ver= 是否已变成时间戳数字(如 ?ver=1718294400)。
- 变了 → 成功,用户陆续访问会自愈。
- 没变 → HTML 这一层被缓存了,需先 purge 页面缓存插件,再复查。
能力边界(不是 bug,是限制)
这套机制只覆盖用 wp_enqueue_style / wp_enqueue_script 入队的资源。以下情况它管不到,需要单独处理:
- 直接硬编码在
header.php等模板里的<link>/<script>; - CSS 里用
@import引入的子文件; - CSS 里
background-image等引用的图片。
长期影响
可以长期放在服务器上,它本就是为生产环境设计的方案:
- 性能开销可忽略:每个本地资源多一次
filemtime()(读元数据的 syscall,微秒级,且 inode 信息通常已在系统缓存里)。 - 纯函数、无副作用:只改写 HTML 里的 URL 字符串,不写数据库、不写文件、不留状态。删掉立即恢复原样。
- 唯一要留神:函数名
dten_filemtime_ver全站只能定义一处,别在别处又复制一份同名函数,否则 PHP 会因函数重定义直接 fatal error。
一句话总结
浏览器按 URL 缓存资源。用 filemtime 让文件内容一变、URL 就变,浏览器便自动拉取新文件——把代码放进 mu-plugins 以免被主题更新覆盖,部署后用隐身窗口确认 ?ver= 已更新即可。