SPA单页面应用静态资源缓存控制方案

SPA单页面应用静态资源缓存控制方案

    • [一、 背景](#一、 背景)
    • 二、优化步骤
      • [1. 调整HTML文件缓存策略](#1. 调整HTML文件缓存策略)
      • [2. 实现主动版本检测与更新提示](#2. 实现主动版本检测与更新提示)
        • [2.1 版本信息生成(构建时)](#2.1 版本信息生成(构建时))
        • [2.2 版本比对与更新提示(运行时)](#2.2 版本比对与更新提示(运行时))
        • [2.3 构建脚本集成](#2.3 构建脚本集成)
    • [三、 工作原理](#三、 工作原理)
    • [四、 关键配置](#四、 关键配置)

一、 背景

在H5应用开发中,前端发布新版本后,偶尔会出现用户点击功能菜单或按钮无响应的情况。经排查,该问题通常是由于网站发布后,浏览器仍缓存着旧版本的静态资源(如JS文件),导致无法加载更新后的代码。

二、优化步骤

1. 调整HTML文件缓存策略

通过配置Nginx,确保主HTML文件不被缓存,从而保证页面刷新时总能获取到最新的应用入口文件。这解决了用户必须手动清除浏览器缓存才能加载新版本的问题,提升了普通用户的使用体验。

nginx 复制代码
location ~* \.(html)$ {
  add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
  expires off;
}

location ~ .*\.(gif|jpg|jpeg|png|PNG|bmp|swf|asp|cfm|xml|py|pl|lasso|cfc|afp|txt|zip|log|ico|csv|xls|pdf|mp3|mp4|apk|svg)$ {
  expires      1y;
  access_log off;
}

location ~ .*\.(js|css)?$ {
  expires      1y;
  access_log off;
}

2. 实现主动版本检测与更新提示

解决用户长时间停留在浏览器标签页期间,系统部署新版本后导致的交互失效问题。通过主动检测版本并提示用户刷新,确保其使用最新代码。

2.1 版本信息生成(构建时)

在构建阶段生成包含版本标识的 version.json 文件。

js 复制代码
/**
 * 构建时生成 version.json,供前端版本检测使用
 */
const { writeFileSync, existsSync } = require('fs');
const { join } = require('path');
const { execSync } = require('child_process');

const projectRoot = join(__dirname, '..');

function safeExec(cmd, fallback) {
  try {
    return execSync(cmd, { encoding: 'utf-8', cwd: projectRoot }).trim();
  } catch {
    return fallback;
  }
}

const packageJson = require(join(projectRoot, 'package.json'));

const versionInfo = {
  version: process.env.npm_package_version || packageJson.version,
  buildTime: new Date().toISOString(),
  commitHash: safeExec('git rev-parse --short HEAD', process.env.GIT_COMMIT || 'unknown'),
  buildId: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`,
  environment: process.env.NODE_ENV || 'development',
  branch: process.env.GIT_BRANCH || safeExec('git rev-parse --abbrev-ref HEAD', 'unknown'),
};

const outPath = join(projectRoot, 'public', 'version.json');
writeFileSync(outPath, JSON.stringify(versionInfo, null, 2), 'utf-8');
console.log('✓ version.json generated at public/version.json');
2.2 版本比对与更新提示(运行时)

前端通过轮询或初始化时检测 version.json,比对版本标识,并决定是否静默刷新或弹窗提示。

js 复制代码
/**
 * 版本检测:通过请求 /version.json 判断是否有新版本,并提示用户刷新
 */
/// <reference types="vite/client" />

const VERSION_STORAGE_KEY = 'manage-web-version';
const CHECK_INTERVAL = 5 * 60 * 1000; // 5 分钟

export interface VersionInfo {
  version: string;
  buildTime: string;
  commitHash: string;
  buildId: string;
  environment: string;
  branch?: string;
}

function getLocalVersion(): VersionInfo | null {
  try {
    const raw = localStorage.getItem(VERSION_STORAGE_KEY);
    if (!raw) return null;
    return JSON.parse(raw) as VersionInfo;
  } catch {
    return null;
  }
}

function saveLocalVersion(info: VersionInfo) {
  localStorage.setItem(VERSION_STORAGE_KEY, JSON.stringify(info));
}

function showUpdateNotification(remoteVersion: VersionInfo) {
  import('element-plus').then(({ ElMessageBox }) => {
    const timeStr = remoteVersion.buildTime
      ? new Date(remoteVersion.buildTime).toLocaleString()
      : '-';
    ElMessageBox.confirm(
      `发现新版本(${remoteVersion.version}),构建时间:${timeStr}。请刷新页面以获取最新内容。`,
      '发现新版本',
      {
        confirmButtonText: '立即刷新',
        showCancelButton: false,
        showClose: false,
        type: 'info',
        closeOnClickModal: false,
        closeOnPressEscape: false,
      }
    ).then(() => {
      saveLocalVersion(remoteVersion);
      window.location.reload();
    });
  });
}

export interface CheckVersionOptions {
  /** 是否为首次进入页签时的检测:发现新版本时静默刷新,不弹窗;仅定时检测到新版本时才弹窗 */
  isInitialCheck?: boolean;
}

/**
 * 检测远程 version.json 与本地存储的版本是否一致
 * - 首次进入页签且发现新版本:静默刷新,对用户无感
 * - 定时检测到新版本(用户一直停留在页签期间部署):弹窗提示刷新
 */
export async function checkVersion(options?: CheckVersionOptions): Promise<void> {
  try {
    const base = import.meta.env.BASE_URL || './';
    const baseSlash = base.endsWith('/') ? base : base + '/';
    const url = `${baseSlash}version.json?t=${Date.now()}`;
    const response = await fetch(url);
    if (!response.ok) return;
    const remoteVersion = (await response.json()) as VersionInfo;
    const localVersion = getLocalVersion();

    // 首次访问:只保存,不提示、不刷新
    if (!localVersion?.buildId) {
      saveLocalVersion(remoteVersion);
      return;
    }

    if (remoteVersion.buildId !== localVersion.buildId) {
      if (options?.isInitialCheck) {
        // 首次进入页签即发现版本不一致(如从旧 session 的 localStorage):静默刷新,无感
        saveLocalVersion(remoteVersion);
        window.location.reload();
      } else {
        // 用户停留在页签期间部署了新版本:弹窗提示
        showUpdateNotification(remoteVersion);
      }
    }
  } catch {
    // 静默失败,如本地开发无 version.json
  }
}

/**
 * 启动版本检测:首次进入静默刷新(无感),之后定时检测到新版本时弹窗提示
 */
export function startVersionCheck(intervalMs: number = CHECK_INTERVAL): () => void {
  checkVersion({ isInitialCheck: true });
  const timer = setInterval(() => checkVersion(), intervalMs);
  return () => clearInterval(timer);
}
2.3 构建脚本集成

package.json 的构建脚本中集成版本生成步骤。

json 复制代码
"scripts": {
  "dev": "vite --host",
  "buildDev": "node scripts/generate-version.cjs && vite build --mode development",
  "build": "node scripts/generate-version.cjs && vite build --mode production",
  "preview": "vite preview --host"
}

三、 工作原理

  1. 构建阶段 :执行 generate-version.js 脚本,收集版本、构建ID、提交哈希等信息,生成 version.json 文件并打包至输出目录。
  2. 运行时 :前端将当前版本信息存储于 localStorage。页面初始化时及后续每隔5分钟,会请求远端的 version.json 文件,比对 buildId
  3. 更新策略
    • 若首次加载即发现版本不一致(例如从旧会话恢复),则静默刷新页面,对用户无感。
    • 若在用户停留页面期间检测到新版本,则弹窗提示用户手动刷新。

四、 关键配置

为保证版本检测的实时性,必须确保 version.json 文件不被浏览器或代理缓存。我们通过以下两种方式实现:

  1. 前端请求追加时间戳 :每次请求 version.json 时,URL 后附加 ?t=${Date.now()} 参数,避免命中缓存。
  2. 服务端配置禁用缓存 :在 Nginx 中针对 version.json 文件设置 Cache-Control 响应头,强制不缓存。
nginx 复制代码
location ~* version\.json$ {
  add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
  expires off;
}

相关推荐
LZQ <=小氣鬼=>6 小时前
React + Ant Design (antd) 国际化完整实战教程
前端·react.js·前端框架·antd·moment
红色的小鳄鱼6 小时前
Vue 监视属性 (watch) 超全解析:Vue2 Vue3
前端·javascript·css·vue.js·前端框架·html5
晚霞的不甘6 小时前
Flutter for OpenHarmony 流体气泡模拟器:用物理引擎与粒子系统打造沉浸式交互体验
前端·flutter·ui·前端框架·交互
红色的小鳄鱼7 小时前
Vue 教程 自定义指令 + 生命周期全解析
开发语言·前端·javascript·vue.js·前端框架·html
军军君0119 小时前
Three.js基础功能学习十三:太阳系实例上
前端·javascript·vue.js·学习·3d·前端框架·three
落霞的思绪1 天前
配置React和React-dom为CDN引入
前端·react.js·前端框架
Highcharts.js1 天前
使用Highcharts与React集成 官网文档使用说明
前端·react.js·前端框架·react·highcharts·官方文档
晚霞的不甘1 天前
Flutter for OpenHarmony从基础到专业:深度解析新版番茄钟的倒计时优化
android·flutter·ui·正则表达式·前端框架·鸿蒙
晚霞的不甘1 天前
Flutter for OpenHarmony专注与习惯的完美融合: 打造你的高效生活助手
前端·数据库·经验分享·flutter·前端框架·生活