Vue3 缓存旧文件解决方案

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,如果用户一直不刷新页面(比如几天没关浏览器标签页),他们用的依然是旧版。
为了解决这个问题,可以通过轮询检测服务器上的资源是否有更新。

实现思路:

  1. 打包时生成版本文件: 在 public 目录下放一个 version.json,每次打包时写入当前时间戳或 Git commit hash。或者更简单粗暴地,直接请求服务器端最新的 index.html,对比其中引用的 script 标签的 hash 值。
  2. 前端轮询或路由切换时检测:

示例代码(通过请求 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。

总结排查顺序:

  1. 检查 Nginx 的 index.html 缓存头是否配置正确(最关键)。
  2. 添加 router.onError 处理旧 chunk 丢失问题。
  3. (可选)加入新版本主动提示弹窗。
相关推荐