Vue 3 项目打包上线后,用户端因为浏览器缓存加载了旧文件(甚至导致白屏或 ChunkLoadError 报错),是单页应用(SPA)非常常见且让人头疼的痛点。
要彻底解决这个问题,需要结合服务器配置(Nginx)、前端路由拦截以及版本检测机制等多管齐下。以下是标准且成熟的解决方案:
1. 核心基础:服务器(Nginx)缓存策略调整
Vue/Vite 打包后的文件结构通常是一个 index.html 和一堆带有 hash 值的 JS/CSS 文件(如 app.3f2a1b.js)。
核心原则是:index.html 绝对不缓存,静态资源(JS/CSS/图片)强缓存。
如果在 Nginx 中没有做区分,浏览器可能会缓存 index.html,导致用户永远请求不到包含最新 hash 值的 JS 文件。
Nginx 配置示例:
server {
listen 80;
server_name yourdomain.com;
root /path/to/your/dist;
# 1. 针对 index.html 禁止缓存
location = /index.html {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
}
# 2. 针对静态资源开启强缓存(Vite/Webpack 会自动生成带 hash 的文件名)
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y; # 缓存一年
add_header Cache-Control "public, max-age=31536000, immutable";
}
# 3. Vue Router history 模式必须的配置
location / {
try_files $uri $uri/ /index.html;
}
}2. 兜底方案:捕获路由加载失败并强制刷新
有时候用户正在使用你的网页,此时你发布了新版本,服务器上的旧 JS 文件(Chunk)被删除了。当用户点击某个菜单触发路由懒加载时,就会报错(找不到旧的 JS 文件)。
可以在 Vue Router 中全局拦截这个错误,并强制刷新页面以获取最新的 index.html。
在 router/index.js 或 router/index.ts 中添加:
router.onError((error) => {
const pattern = /Loading chunk (\d)+ failed/g;
const isChunkLoadFailed = error.message.match(pattern);
const targetPath = router.history.pending.fullPath;
// 也可以捕获动态导入模块失败的错误
const isImportFailed = error.message.includes('Failed to fetch dynamically imported module');
if (isChunkLoadFailed || isImportFailed) {
// 强制刷新页面并跳转到目标路由
window.location.href = window.location.origin + targetPath;
}
});3. 构建配置:确保 Vite/Webpack 正确开启 Hashing
Vue 3 默认使用 Vite 构建。Vite 默认就已经为生产环境的打包开启了文件 hash([name].[hash].js)。请确保你没有在 vite.config.js 中错误地关闭它。
正常的默认输出即可:
// vite.config.js 默认即可,无需特别配置 hash
export default defineConfig({
build: {
// 确保未将 rollupOptions.output.entryFileNames 等写死成固定名字
}
})4. 极致体验:前端主动轮询检测新版本并提示用户
即使 Nginx 不缓存 index.html,如果用户一直不刷新页面(比如几天没关浏览器标签页),他们用的依然是旧版。
为了解决这个问题,可以通过轮询检测服务器上的资源是否有更新。
实现思路:
- 打包时生成版本文件: 在 public 目录下放一个 version.json,每次打包时写入当前时间戳或 Git commit hash。或者更简单粗暴地,直接请求服务器端最新的 index.html,对比其中引用的 script 标签的 hash 值。
- 前端轮询或路由切换时检测:
示例代码(通过请求 index.html 里的 ETag 或 script hash 检测):
// versionCheck.js
let currentEtag = '';
async function checkUpdate() {
try {
// 加个时间戳防止请求被本地拦截
const res = await fetch(`/?time=${new Date().getTime()}`, { method: 'HEAD' });
const latestEtag = res.headers.get('etag'); // 或者 Last-Modified
if (currentEtag && latestEtag && currentEtag !== latestEtag) {
// 发现新版本!
return true;
}
currentEtag = latestEtag;
return false;
} catch (e) {
return false;
}
}
// 可以在路由切换前触发检测
router.beforeEach(async (to, from, next) => {
const hasUpdate = await checkUpdate();
if (hasUpdate) {
// 提示用户更新
if (confirm('系统已发布新版本,请刷新页面以获取最新内容!')) {
window.location.reload();
return;
}
}
next();
});(注:如果觉得每次路由切换都发请求太频繁,可以改为 setInterval 每隔半小时后台静默拉取一次。)
5. 排查 PWA / Service Worker
如果你的项目中使用了 PWA 插件(如 vite-plugin-pwa),Service Worker 会在本地代理并缓存所有请求,这是导致“死活更新不了”的重灾区。
- 解决: 确保 Service Worker 触发了更新机制(update() 和 skipWaiting())。如果不需要离线功能,强烈建议直接移除 Service Worker,并注销已注册的 SW。
总结排查顺序:
- 检查 Nginx 的 index.html 缓存头是否配置正确(最关键)。
- 添加 router.onError 处理旧 chunk 丢失问题。
- (可选)加入新版本主动提示弹窗。