前端应用构建更新检测
你是否遇到过这样的场景:线上项目发布了新版本,但用户浏览器中仍然运行着旧代码——导致接口报错、页面白屏,甚至数据丢失?
这就是"前端应用构建更新检测"要解决的核心问题:让前端应用有能力感知到"版本已过期",并引导用户刷新到最新版本。
一、为什么需要构建更新检测?
1.1 SPA 的"长寿命"问题
传统的多页应用(MPA)每次跳转都会重新请求 HTML,天然能获取最新版本。但现代 SPA(单页应用)一旦加载完成,整个应用的生命周期可能持续数小时甚至数天——在此期间,用户不会再请求新的 HTML。
1.2 常见症状
| 症状 | 原因 | 影响 |
|---|---|---|
| Chunk 加载失败 | 旧版本引用的 JS chunk 文件在新部署后被删除 | 页面白屏、路由跳转失败 |
| 接口不兼容 | 新版本后端接口字段变更,旧前端未适配 | 数据显示异常、提交报错 |
| 功能缺失 | 新功能只在新版本存在,旧代码无法访问 | 用户困惑 |
| 样式错乱 | CSS hash 变化导致样式文件 404 | 页面布局混乱 |
1.3 核心思路
整个更新检测的核心思路可以用三步概括:
版本标记 → 变化检测 → 用户通知,所有方案都是围绕这三步展开的。
二、版本标识的生成方式
在检测版本变化之前,我们首先需要为每次构建生成一个唯一的版本标识。常见方式有以下几种:
2.1 构建 Hash
Webpack / Vite 等构建工具会为产物文件名添加内容 hash(如 app.3a7b2c.js),我们可以利用入口文件的 hash 作为版本标识。
// Vite 构建后的产物示例
// dist/assets/index-3a7b2c1d.js
// dist/assets/index-8e4f2b9a.css
// 核心思路:比较 index.html 中引用的脚本 hash
2.2 自定义版本文件
在构建时生成一个独立的版本文件(如 version.json),包含版本号、构建时间戳等信息。
{
"version": "1.2.3",
"buildTime": "2026-03-03T10:30:00Z",
"hash": "3a7b2c1d"
}
构建脚本示例(Node.js):
// scripts/generate-version.js
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const version = {
// 从 package.json 读取版本号
version: require('../package.json').version,
// 当前构建时间戳
buildTime: new Date().toISOString(),
// Git commit hash(短)
hash: execSync('git rev-parse --short HEAD').toString().trim(),
};
fs.writeFileSync(
path.resolve(__dirname, '../dist/version.json'),
JSON.stringify(version, null, 2)
);
console.log('✅ version.json generated:', version);
2.3 HTML Meta 标签
将版本信息直接注入到 index.html 的 <meta> 标签中:
<meta name="app-version" content="3a7b2c1d" />
<meta name="build-time" content="2026-03-03T10:30:00Z" />
2.4 ETag / Last-Modified
利用 HTTP 响应头本身携带的 ETag 或 Last-Modified 信息来判断 index.html 是否更新。
2.5 方式对比
| 方式 | 优点 | 缺点 | 推荐场景 |
|---|---|---|---|
| 构建 Hash | 零额外配置,直接从 HTML 解析 | 解析 HTML 有一定复杂度 | 通用方案 |
| 版本文件 | 简单直观,易于扩展 | 需额外构建步骤 | 中大型项目推荐 |
| Meta 标签 | 读取方便 | 需改造 HTML 模板 | 小型项目 |
| ETag | 利用浏览器原生机制 | 仅能判断"变了没有",无法携带语义信息 | 辅助手段 |
三、检测策略详解
有了版本标识之后,下一步就是何时检测和如何检测。
3.1 方案一:定时轮询版本文件
最简单直接的方案——定时请求服务端的版本文件,与当前版本对比。
完整实现:
class VersionChecker {
private currentVersion: string;
private timer: ReturnType<typeof setInterval> | null = null;
private readonly checkInterval: number;
private readonly versionUrl: string;
constructor(options?: { interval?: number; url?: string }) {
this.currentVersion = '';
this.checkInterval = options?.interval ?? 5 * 60 * 1000; // 默认 5 分钟
this.versionUrl = options?.url ?? '/version.json';
}
/** 启动版本检测 */
start() {
// 首次加载时记录当前版本
this.fetchVersion().then((version) => {
this.currentVersion = version;
console.log('[VersionChecker] 当前版本:', version);
});
// 启动定时轮询
this.timer = setInterval(() => {
this.check();
}, this.checkInterval);
}
/** 停止检测 */
stop() {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
}
/** 执行一次版本检查 */
private async check() {
try {
const latestVersion = await this.fetchVersion();
if (this.currentVersion && latestVersion !== this.currentVersion) {
this.notify();
}
} catch (error) {
console.warn('[VersionChecker] 检查失败:', error);
}
}
/** 请求版本文件 */
private async fetchVersion(): Promise<string> {
// 添加时间戳防止缓存
const url = `${this.versionUrl}?t=${Date.now()}`;
const response = await fetch(url, {
cache: 'no-cache',
headers: { 'Cache-Control': 'no-cache' },
});
const data = await response.json();
return data.version || data.hash;
}
/** 通知用户有新版本 */
private notify() {
// 可以替换为自定义 UI 组件
const shouldRefresh = window.confirm(
'检测到新版本,是否立即刷新页面?'
);
if (shouldRefresh) {
window.location.reload();
}
}
}
// 使用
const checker = new VersionChecker({ interval: 3 * 60 * 1000 });
checker.start();
请求版本文件时,一定要绕过浏览器缓存。常用手段:
- URL 添加时间戳参数
?t=Date.now() - 设置请求头
Cache-Control: no-cache - 服务端对
version.json设置Cache-Control: no-store
3.2 方案二:基于 HTML 解析的 Hash 检测
不依赖额外的版本文件,而是直接重新请求 index.html,解析其中引用的脚本 hash,与当前页面中实际加载的脚本做对比。
class HtmlHashChecker {
private currentScripts: string[] = [];
constructor() {
// 记录当前页面已加载的脚本
this.currentScripts = this.getPageScripts();
}
/** 获取当前页面中的脚本列表 */
private getPageScripts(): string[] {
return Array.from(document.querySelectorAll('script[src]'))
.map((script) => (script as HTMLScriptElement).src)
.filter((src) => src.includes('/assets/')); // 只关注构建产物
}
/** 获取远程 HTML 中的脚本列表 */
private async getRemoteScripts(): Promise<string[]> {
const response = await fetch(`/?t=${Date.now()}`, {
cache: 'no-cache',
});
const html = await response.text();
// 解析 HTML 中的 <script src="..."> 标签
const scriptRegex = /<script[^>]+src="([^"]+)"/g;
const scripts: string[] = [];
let match: RegExpExecArray | null;
while ((match = scriptRegex.exec(html)) !== null) {
scripts.push(match[1]);
}
return scripts.filter((src) => src.includes('/assets/'));
}
/** 检查是否有更新 */
async check(): Promise<boolean> {
try {
const remoteScripts = await this.getRemoteScripts();
// 比较脚本列表是否一致
const hasUpdate =
remoteScripts.length !== this.currentScripts.length ||
remoteScripts.some(
(script, i) => script !== this.currentScripts[i]
);
return hasUpdate;
} catch {
return false;
}
}
}
直接请求 index.html 的方式会增加服务端流量。如果你的应用访问量大,建议优先使用轻量的 version.json 方案。
3.3 方案三:Service Worker 方案
利用 Service Worker 的生命周期事件来检测更新。Service Worker 本身就有"安装新版本 → 等待激活 → 接管页面"的内置机制。
注册与更新检测:
// src/registerSW.ts
export function registerServiceWorker() {
if (!('serviceWorker' in navigator)) return;
window.addEventListener('load', async () => {
const registration = await navigator.serviceWorker.register('/sw.js');
// 监听新 Service Worker 安装完毕
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
if (!newWorker) return;
newWorker.addEventListener('statechange', () => {
// 新 SW 安装完成,进入等待状态
if (
newWorker.state === 'installed' &&
navigator.serviceWorker.controller
) {
// 当前有旧 SW 控制页面,说明这是一次"更新"
showUpdateNotification(registration);
}
});
});
});
}
function showUpdateNotification(
registration: ServiceWorkerRegistration
) {
// 显示更新提示 UI
const confirmed = window.confirm('发现新版本,是否立即更新?');
if (confirmed) {
// 通知新 SW 跳过等待,立即接管
registration.waiting?.postMessage({ type: 'SKIP_WAITING' });
// 刷新页面
window.location.reload();
}
}
Service Worker 文件:
// public/sw.js
self.addEventListener('install', (event) => {
// 预缓存资源(可选)
console.log('[SW] 安装新版本');
});
self.addEventListener('activate', (event) => {
// 清理旧缓存
console.log('[SW] 激活新版本');
event.waitUntil(clients.claim());
});
// 响应主线程的消息
self.addEventListener('message', (event) => {
if (event.data?.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
3.4 方案四:事件驱动的智能检测
与其盲目地定时轮询,不如在用户行为触发时再做检测——既省带宽,又能在关键时刻及时发现更新。
完整实现:
class SmartVersionChecker {
private currentVersion: string = '';
private checking: boolean = false;
private readonly versionUrl: string;
constructor(versionUrl = '/version.json') {
this.versionUrl = versionUrl;
}
async init() {
// 记录初始版本
this.currentVersion = await this.fetchVersion();
// 注册各种事件监听
this.listenVisibilityChange();
this.listenOnline();
this.listenRouteChange();
this.listenChunkError();
}
/** 页面从后台切回前台时检测 */
private listenVisibilityChange() {
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
this.check();
}
});
}
/** 网络从离线恢复到在线时检测 */
private listenOnline() {
window.addEventListener('online', () => {
this.check();
});
}
/** 路由变化时检测 */
private listenRouteChange() {
// 拦截 History API
const originalPushState = history.pushState.bind(history);
history.pushState = (...args) => {
originalPushState(...args);
this.check();
};
window.addEventListener('popstate', () => {
this.check();
});
}
/** 监听 Chunk 加载失败——很可能是版本过期 */
private listenChunkError() {
window.addEventListener('error', (event) => {
const target = event.target as HTMLElement;
// 判断是否为脚本或样式加载失败
if (
target instanceof HTMLScriptElement ||
target instanceof HTMLLinkElement
) {
console.warn('[VersionChecker] 资源加载失败,可能版本已过期');
this.check();
}
}, true); // 使用捕获阶段才能捕获资源加载错误
}
/** 执行版本检测(带防重入) */
private async check() {
if (this.checking) return;
this.checking = true;
try {
const latestVersion = await this.fetchVersion();
if (this.currentVersion && latestVersion !== this.currentVersion) {
this.notify();
}
} catch {
// 网络异常,静默跳过
} finally {
this.checking = false;
}
}
private async fetchVersion(): Promise<string> {
const res = await fetch(`${this.versionUrl}?t=${Date.now()}`, {
cache: 'no-cache',
});
const data = await res.json();
return data.hash || data.version;
}
private notify() {
// 这里可以替换为 UI 组件弹窗
const shouldRefresh = window.confirm(
'检测到系统已更新,是否刷新页面获取最新版本?'
);
if (shouldRefresh) {
window.location.reload();
}
}
}
// 使用
const checker = new SmartVersionChecker('/version.json');
checker.init();
visibilitychange覆盖了用户切换标签页 / 最小化后返回的场景,这是最高频的"版本过期"时机- Chunk 加载失败是最直接的过期信号,可以在异常发生时立即引导用户刷新
- 相比定时轮询,事件驱动的方式减少了约 80% 的无效请求
四、用户通知与刷新策略
检测到版本更新后,如何通知用户也是一门学问。好的更新提示应该不打断用户操作、信息明确、操作简单。
4.1 常见通知方式对比
4.2 推荐方案:非侵入式顶部横幅
// React 组件示例
import { useState, useEffect } from 'react';
function UpdateBanner() {
const [hasUpdate, setHasUpdate] = useState(false);
useEffect(() => {
const checker = new SmartVersionChecker('/version.json');
// 用自定义回调替代 confirm
checker.onUpdate = () => setHasUpdate(true);
checker.init();
return () => checker.destroy();
}, []);
if (!hasUpdate) return null;
return (
<div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
zIndex: 9999,
padding: '12px 24px',
backgroundColor: '#1976d2',
color: '#fff',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
gap: '12px',
fontSize: '14px',
}}
>
<span>🚀 发现新版本,请刷新页面获取最新功能</span>
<button
onClick={() => window.location.reload()}
style={{
padding: '4px 16px',
border: '1px solid #fff',
borderRadius: '4px',
backgroundColor: 'transparent',
color: '#fff',
cursor: 'pointer',
}}
>
立即刷新
</button>
<button
onClick={() => setHasUpdate(false)}
style={{
background: 'none',
border: 'none',
color: '#fff',
cursor: 'pointer',
fontSize: '18px',
}}
>
✕
</button>
</div>
);
}
4.3 处理用户忽略更新的情况
如果用户关闭了更新提示但不刷新,可以采用渐进式策略:
class ProgressiveNotifier {
private dismissCount = 0;
notify() {
this.dismissCount++;
if (this.dismissCount <= 2) {
// 前两次:温和提示
this.showBanner('发现新版本,建议刷新页面');
} else if (this.dismissCount <= 4) {
// 3-4 次:强调提示
this.showBanner('⚠️ 当前版本已过期,部分功能可能异常,请尽快刷新');
} else {
// 5 次以上:强制提示(不可关闭)
this.showModal('当前版本已严重过期,请立即刷新页面以确保正常使用');
}
}
private showBanner(message: string) { /* ... */ }
private showModal(message: string) { /* ... */ }
}
4.4 安全刷新:避免丢失用户数据
强制刷新前,务必考虑用户可能正在填写表单或编辑内容。
function safeReload() {
// 检查是否有未保存的表单数据
const hasUnsavedData = checkUnsavedForms();
if (hasUnsavedData) {
const confirmed = window.confirm(
'检测到您有未保存的内容。确定要刷新页面吗?\n刷新后未保存的内容将丢失。'
);
if (!confirmed) return;
}
window.location.reload();
}
function checkUnsavedForms(): boolean {
// 检查页面中是否有 dirty 状态的表单
const forms = document.querySelectorAll('form');
for (const form of forms) {
const formData = new FormData(form);
for (const [, value] of formData) {
if (value) return true;
}
}
return false;
}
五、与构建工具集成
5.1 Vite 插件实现
通过 Vite 插件,在每次构建时自动生成 version.json:
// vite-plugin-version.ts
import type { Plugin } from 'vite';
import { execSync } from 'child_process';
export function versionPlugin(): Plugin {
let version: string;
return {
name: 'vite-plugin-version',
// 构建开始时生成版本信息
buildStart() {
const gitHash = execSync('git rev-parse --short HEAD')
.toString()
.trim();
version = `${Date.now()}-${gitHash}`;
},
// 将版本信息注入到 HTML 中
transformIndexHtml(html) {
return html.replace(
'</head>',
`<meta name="app-version" content="${version}" />\n</head>`
);
},
// 生成 version.json 到输出目录
generateBundle() {
this.emitFile({
type: 'asset',
fileName: 'version.json',
source: JSON.stringify({
version,
buildTime: new Date().toISOString(),
}),
});
},
};
}
使用方式:
// vite.config.ts
import { defineConfig } from 'vite';
import { versionPlugin } from './vite-plugin-version';
export default defineConfig({
plugins: [versionPlugin()],
});
5.2 Webpack 插件实现
// webpack-version-plugin.js
const { execSync } = require('child_process');
const { RawSource } = require('webpack-sources');
class VersionPlugin {
apply(compiler) {
compiler.hooks.emit.tap('VersionPlugin', (compilation) => {
const gitHash = execSync('git rev-parse --short HEAD')
.toString()
.trim();
const versionInfo = JSON.stringify({
version: `${Date.now()}-${gitHash}`,
buildTime: new Date().toISOString(),
});
compilation.assets['version.json'] = new RawSource(versionInfo);
});
}
}
module.exports = VersionPlugin;
使用方式:
// webpack.config.js
const VersionPlugin = require('./webpack-version-plugin');
module.exports = {
plugins: [new VersionPlugin()],
};
5.3 与 CI/CD 配合
在 CI/CD 流程中,可以将构建号(Build Number)或 Git Commit Hash 注入为环境变量:
# GitHub Actions 示例
- name: Build
run: pnpm build
env:
VITE_APP_VERSION: ${{ github.sha }}
VITE_BUILD_NUMBER: ${{ github.run_number }}
在代码中通过环境变量获取版本信息:
// 在 Vite 项目中使用
const APP_VERSION = import.meta.env.VITE_APP_VERSION;
const BUILD_NUMBER = import.meta.env.VITE_BUILD_NUMBER;
六、生产级完整方案
将前面的各种技术整合为一个生产可用的完整方案:
下面是一个可以直接复制使用的完整实现:
// src/utils/version-checker.ts
interface VersionCheckerOptions {
/** 版本文件地址,默认 '/version.json' */
url?: string;
/** 定时轮询间隔(毫秒),默认 5 分钟 */
pollingInterval?: number;
/** 版本变化时的回调 */
onUpdate?: () => void;
}
export class VersionChecker {
private currentVersion = '';
private checking = false;
private timer: ReturnType<typeof setInterval> | null = null;
private destroyed = false;
private url: string;
private pollingInterval: number;
onUpdate: (() => void) | null;
constructor(options: VersionCheckerOptions = {}) {
this.url = options.url ?? '/version.json';
this.pollingInterval = options.pollingInterval ?? 5 * 60 * 1000;
this.onUpdate = options.onUpdate ?? null;
}
/** 初始化:记录当前版本 + 注册事件监听 */
async init() {
try {
this.currentVersion = await this.fetchVersion();
} catch {
console.warn('[VersionChecker] 初始化获取版本失败');
}
this.bindEvents();
this.startPolling();
}
/** 销毁实例,移除所有监听 */
destroy() {
this.destroyed = true;
this.stopPolling();
document.removeEventListener(
'visibilitychange',
this.handleVisibilityChange
);
window.removeEventListener('online', this.handleOnline);
}
// ===== 事件绑定 =====
private bindEvents() {
document.addEventListener(
'visibilitychange',
this.handleVisibilityChange
);
window.addEventListener('online', this.handleOnline);
this.patchHistoryApi();
this.listenResourceError();
}
private handleVisibilityChange = () => {
if (document.visibilityState === 'visible') {
this.check();
}
};
private handleOnline = () => {
this.check();
};
private patchHistoryApi() {
const originalPushState = history.pushState.bind(history);
const self = this;
history.pushState = function (...args) {
originalPushState(...args);
self.check();
};
}
private listenResourceError() {
window.addEventListener(
'error',
(event) => {
const target = event.target;
if (
target instanceof HTMLScriptElement ||
target instanceof HTMLLinkElement
) {
this.check();
}
},
true
);
}
// ===== 轮询 =====
private startPolling() {
this.timer = setInterval(() => this.check(), this.pollingInterval);
}
private stopPolling() {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
}
// ===== 核心检测 =====
private async check() {
if (this.checking || this.destroyed) return;
this.checking = true;
try {
const latest = await this.fetchVersion();
if (this.currentVersion && latest !== this.currentVersion) {
this.onUpdate?.();
}
} catch {
// 网络异常忽略
} finally {
this.checking = false;
}
}
private async fetchVersion(): Promise<string> {
const res = await fetch(`${this.url}?t=${Date.now()}`, {
cache: 'no-cache',
});
const data = await res.json();
return data.version || data.hash || '';
}
}
七、各方案对比总结
| 维度 | 定时轮询 | HTML Hash 解析 | Service Worker | 事件驱动 |
|---|---|---|---|---|
| 实现复杂度 | ⭐ 低 | ⭐⭐ 中 | ⭐⭐⭐ 高 | ⭐⭐ 中 |
| 实时性 | 取决于轮询间隔 | 取决于轮询间隔 | 高(浏览器自动检测) | 高(关键时刻触发) |
| 带宽消耗 | 较高(持续请求) | 较高(请求完整 HTML) | 低 | 低 |
| 兼容性 | 全兼容 | 全兼容 | 需要 HTTPS | 全兼容 |
| 额外依赖 | 需要版本文件 | 无 | 需要编写 SW 文件 | 需要版本文件 |
| 推荐场景 | 简单项目,快速集成 | 不想维护额外文件 | PWA 应用 | 中大型项目推荐 |
大多数项目推荐组合使用 "事件驱动检测 + 定时兜底轮询" 的方式——事件驱动覆盖关键时刻(切标签页、路由切换等),定时轮询作为兜底保障。这也是上面"生产级方案"所采用的策略。
八、常见问题与最佳实践
8.1 避免循环刷新
如果版本文件返回错误或解析异常,可能导致反复刷新:
// 防止循环刷新
const RELOAD_KEY = 'last_version_reload';
const RELOAD_COOLDOWN = 10 * 1000; // 10 秒冷却期
function safeReload() {
const lastReload = Number(sessionStorage.getItem(RELOAD_KEY) || 0);
const now = Date.now();
if (now - lastReload < RELOAD_COOLDOWN) {
console.warn('[VersionChecker] 冷却期内,跳过刷新');
return;
}
sessionStorage.setItem(RELOAD_KEY, String(now));
window.location.reload();
}
8.2 服务端缓存配置
确保 version.json 和 index.html 不被 CDN 或浏览器缓存:
# Nginx 配置示例
location = /version.json {
add_header Cache-Control "no-store, no-cache, must-revalidate";
add_header Pragma "no-cache";
expires -1;
}
location = /index.html {
add_header Cache-Control "no-cache";
}
# 带 hash 的静态资源可以长期缓存
location /assets/ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
8.3 灰度发布场景
在灰度发布场景下,不同用户可能获取到不同版本——此时不应简单地判断"版本不同就要刷新":
// 灰度兼容:只在版本号"更高"时才提示更新
function shouldUpdate(current: string, latest: string): boolean {
// 假设版本格式为 timestamp-hash
const currentTs = parseInt(current.split('-')[0], 10);
const latestTs = parseInt(latest.split('-')[0], 10);
return latestTs > currentTs;
}
九、开源方案推荐
如果不想从零实现,也可以使用社区已有的开源方案:
| 方案 | 特点 | 适用场景 |
|---|---|---|
| plugin-web-update-notification | 支持 Vite/Webpack/Umi 插件化接入,开箱即用 | 想快速集成的项目 |
| Workbox | Google 出品的 Service Worker 工具库 | PWA 项目 |
| 自研方案 | 完全可控,灵活定制 | 有特殊需求的中大型项目 |
十、面试高频问答
Q1: 前端如何检测到线上应用发布了新版本?
答:核心思路是"版本标记 + 变化检测 + 用户通知"三步走。
- 版本标记:构建时生成唯一标识(如
version.json中的 hash / 时间戳)。 - 变化检测:前端通过定时轮询或事件驱动(如
visibilitychange、路由切换)请求最新版本信息,与本地记录的版本做对比。 - 用户通知:发现版本变化后,通过非侵入式 UI(如顶部横幅)引导用户刷新页面。
Q2: 为什么不能直接用 window.location.reload() 强制刷新?
答:强制刷新存在几个问题:
- 用户体验差:如果用户正在填写表单或编辑内容,强制刷新会导致数据丢失。
- 循环刷新风险:如果新版本本身有问题,可能导致页面加载后立即再次检测到"更新",陷入无限刷新循环。
- 无告知:用户对突然的页面重载感到困惑,不知道发生了什么。
正确做法是提示用户,让用户自主选择合适的时机刷新,同时在刷新前检查未保存的数据。
Q3: 定时轮询和事件驱动各有什么优缺点?应该怎么选?
答:
| 维度 | 定时轮询 | 事件驱动 |
|---|---|---|
| 实现难度 | 简单(setInterval) | 中等(需监听多个事件) |
| 实时性 | 取决于间隔时间 | 关键时刻(切标签页等)几乎实时 |
| 带宽消耗 | 持续发请求,浪费较多 | 只在事件触发时请求,消耗少 |
| 覆盖度 | 全面(不遗漏) | 可能有边界场景遗漏 |
实际项目推荐两者结合:用事件驱动覆盖高频场景(visibilitychange、路由切换、资源加载失败),用一个较长间隔(如 5 分钟)的定时轮询作为兜底,确保不会遗漏。
Q4: 如何避免版本检测带来的循环刷新问题?
答:可以从以下几方面防护:
- 冷却期机制:在
sessionStorage中记录上次刷新的时间戳,短时间内(如 10 秒)不重复刷新。 - 刷新次数限制:记录连续刷新次数,超过阈值后停止自动刷新,改为仅提示用户。
- 版本比较逻辑:只在版本号"更新"(而非"不同")时才触发提示,避免灰度发布等场景下的误判。
- 错误处理:版本文件请求失败时静默忽略,不做任何刷新操作。
Q5: Chunk 加载失败(ChunkLoadError)和版本更新有什么关系?如何处理?
答:这是 SPA 中最常见的版本过期症状。原因如下:
- SPA 使用懒加载(
React.lazy/ 动态import()),路由切换时才请求对应的 JS chunk 文件。 - 新版本部署后,旧的 chunk 文件(如
chunk-abc123.js)可能已被删除或替换。 - 当仍在运行旧版本的用户切换路由时,就会触发
ChunkLoadError。
处理策略:
// 在路由层面捕获 Chunk 加载错误
router.onError((error) => {
if (/Loading chunk .+ failed/.test(error.message)) {
// 提示用户刷新,或在确认无未保存数据后自动刷新
safeReload();
}
});
Q6: Service Worker 方案的更新检测和轮询方案有什么本质区别?
答:本质区别在于谁负责检测更新:
- 轮询方案:前端 JS 主动发起请求,对比版本文件——一切逻辑在应用代码中。
- Service Worker 方案:浏览器自身会定期检查
sw.js是否有变化(每次导航或至少每 24 小时一次),如果有变化就安装新的 SW——更新检测由浏览器引擎驱动。
SW 方案的优势是不需要额外的轮询逻辑,但要求项目运行在 HTTPS 环境下,且需要编写和维护 Service Worker 文件,适合 PWA 类型的项目。