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"
}
三、 工作原理
- 构建阶段 :执行
generate-version.js脚本,收集版本、构建ID、提交哈希等信息,生成version.json文件并打包至输出目录。 - 运行时 :前端将当前版本信息存储于
localStorage。页面初始化时及后续每隔5分钟,会请求远端的version.json文件,比对buildId。 - 更新策略 :
- 若首次加载即发现版本不一致(例如从旧会话恢复),则静默刷新页面,对用户无感。
- 若在用户停留页面期间检测到新版本,则弹窗提示用户手动刷新。
四、 关键配置
为保证版本检测的实时性,必须确保 version.json 文件不被浏览器或代理缓存。我们通过以下两种方式实现:
- 前端请求追加时间戳 :每次请求
version.json时,URL 后附加?t=${Date.now()}参数,避免命中缓存。 - 服务端配置禁用缓存 :在 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;
}