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;
}

相关推荐
码云之上12 小时前
万星入坞·其二:子应用如何优雅地"入坞"
性能优化·架构·前端框架
真夜17 小时前
开发正常但生产异常的 Bug:Vite manualChunks 循环依赖导致 ReferenceError
前端·前端框架·vite
光影少年1 天前
大前端框架生态
前端·javascript·flutter·react.js·前端框架·鸿蒙·angular.js
码云之上1 天前
万星入坞:我们如何用三层插件体系干掉巨石应用
前端·架构·前端框架
GISer_Jing2 天前
BOSS上AIAgent|前端AI所需要技能
前端·人工智能·ai·前端框架
光影少年3 天前
useMemo 与 useCallback 区别、各自解决什么性能问题、依赖陷阱
react.js·前端框架·掘金·金石计划
Highcharts.js3 天前
无需搭建数据管道,如何快速上线投资基金筛选器?
开发语言·javascript·react.js·前端框架·highcharts
Codebee3 天前
Ooder UI LLM Deep 匹配模式深度解析
前端框架
声声codeGrandMaster3 天前
React框架的基础代码使用
前端·react.js·前端框架
Highcharts.js3 天前
Highcharts React 5.0 正式版:支持 ES 模块化、组件更精简、开发体验全面升级
前端·javascript·react.js·elasticsearch·前端框架·highcharts