问题:用户死活看不到新版本
SPA 项目部署完,产品经理说他改不出新的功能。F12 一看,加载的还是上次发布的 app.123abc.js,新 commit 完全没生效。让他强刷一下 (Ctrl+F5) 立刻就好,但你没法要求所有用户都来这一下。
本质是浏览器缓存没失效。Vue / Webpack 默认 hash 文件名(app.[hash].js)按理说能自动 cache-bust,但很多项目里:
- 构建 hash 策略没开,文件名一直是
app.js(覆盖式发布) - 有,但
index.html自身被 CDN / 浏览器缓存了,里头引用的还是老 hash - Service Worker 或 PWA 缓存层把旧文件锁住

方案一:文件名加时间戳查询串
最简单粗暴的方案 —— 每次构建给静态文件加 ?v=时间戳 查询串。哪怕文件名不变,query 变了浏览器就当成新资源重新拉:
// vue.config.js (Vue CLI / webpack 项目)
const path = require('path')
const timeStamp = new Date().getTime()
function resolve(dir) {
return path.join(__dirname, dir)
}
module.exports = {
configureWebpack: {
name: 'XiaoDong',
resolve: {
alias: {
'@': resolve('src')
}
},
output: {
filename: `js/[name].js?v=${timeStamp}`,
chunkFilename: `js/chunk.[id].js?v=${timeStamp}`
}
},
css: {
extract: {
filename: `css/[name].css?v=${timeStamp}`,
chunkFilename: `css/chunk.[id].css?v=${timeStamp}`
}
}
}
构建后 index.html 里引用变成:
<script src="js/app.js?v=1700000000000"></script>
<link rel="stylesheet" href="css/app.css?v=1700000000000">
每次构建 timeStamp 变,等于每次发布都换 URL。
原理:为什么 query 变就重拉
浏览器决定"用不用缓存"的核心 key 是 URL(包括 query)。app.js?v=1 和 app.js?v=2 在浏览器看来是两个不同的资源,即使服务器返回的内容一样,浏览器也会重新发请求。
CDN 也一样,URL 不同就当作不同资源,缓存独立。
方案二:Vite 项目里的做法
Vite 默认就有 hash,但同样可以加 timestamp:
// vite.config.ts
import { defineConfig } from 'vite'
const timestamp = Date.now()
export default defineConfig({
build: {
rollupOptions: {
output: {
entryFileNames: `js/[name]-${timestamp}.js`,
chunkFileNames: `js/[name]-${timestamp}.js`,
assetFileNames: `assets/[name]-${timestamp}.[ext]`
}
}
}
})
这种把 timestamp 嵌进文件名(不是 query),效果一样,部分 CDN 对 query 的处理不一致,文件名更稳。
方案三:开 hash 但配置 index.html 不缓存
更标准的方案,既保留 hash 又防 index.html 缓存:
Vue / Webpack hash 配置(默认就开了,确认下):
// vue.config.js
module.exports = {
filenameHashing: true, // 默认 true,JS/CSS/字体都加 contenthash
productionSourceMap: false
}
Nginx 给 index.html 设置 no-cache:
server {
listen 80;
root /var/www/dist;
# 带 hash 的静态文件,强缓存 1 年
location ~* .(js|css|woff2?|png|jpe?g|gif|svg|ico)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# index.html 永不缓存
location = /index.html {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
}
# SPA fallback
location / {
try_files $uri $uri/ /index.html;
}
}
这套是最佳实践:
- JS / CSS 带 hash,内容变 hash 变,filename 变就重拉,内容不变就走强缓存(1 年)
index.html不缓存,每次访问都拉新的,新 hash 立即生效
方案四:Service Worker / PWA 场景
如果项目接了 PWA / Service Worker,情况复杂一点。Service Worker 会主动缓存所有资源,普通的 hash 也救不了 —— 它直接拦截请求返回缓存内容。
解决:Service Worker 注册时加版本检测,新版本到达就清旧 cache:
// public/sw.js
const VERSION = 'v-' + new Date().toISOString() // 构建时替换
self.addEventListener('install', (e) => {
self.skipWaiting() // 新版立刻接管
})
self.addEventListener('activate', (e) => {
e.waitUntil(
caches.keys().then(keys =>
Promise.all(
keys.filter(k => k !== VERSION).map(k => caches.delete(k))
)
)
)
self.clients.claim()
})
更省心的方案是用 Workbox(vite-plugin-pwa / workbox-webpack-plugin),它自动处理版本控制和清理。
哪种方案选哪个
| 方案 | 优势 | 劣势 |
|---|---|---|
| 时间戳 query | 配置最简,一行加完 | 没变的文件也重拉,浪费带宽 |
| hash + index.html no-cache | 只重拉变了的文件,最优 | 要改 Nginx,稍麻烦 |
| Service Worker 版本控制 | PWA 场景必备 | 复杂,debug 难 |
验证缓存策略生效
F12 打开 Network,刷新页面,看每个资源的"Status"和"Size"列:
200+ 实际 size → 真实下载200 (from disk cache)→ 浏览器走强缓存,没发请求304 Not Modified→ 发了请求,服务器告诉浏览器内容没变
理想状态:第一次访问全 200 真实下载,刷新后所有带 hash 的资源是 disk cache,index.html 是 200 或 304。
一句话总结
能 hash + nginx 配置就走最佳实践,赶时间或者控制不了服务器配置时,时间戳 query 一行救命。两个本质都是"让浏览器认为这是个新资源",方法不同效果一样。
—— 别看了 · 2026