跳到主要内容

前端应用构建更新检测

你是否遇到过这样的场景:线上项目发布了新版本,但用户浏览器中仍然运行着旧代码——导致接口报错、页面白屏,甚至数据丢失?

这就是"前端应用构建更新检测"要解决的核心问题:让前端应用有能力感知到"版本已过期",并引导用户刷新到最新版本。

一、为什么需要构建更新检测?

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 响应头本身携带的 ETagLast-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.jsonindex.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 插件化接入,开箱即用想快速集成的项目
WorkboxGoogle 出品的 Service Worker 工具库PWA 项目
自研方案完全可控,灵活定制有特殊需求的中大型项目

十、面试高频问答

Q1: 前端如何检测到线上应用发布了新版本?

:核心思路是"版本标记 + 变化检测 + 用户通知"三步走。

  1. 版本标记:构建时生成唯一标识(如 version.json 中的 hash / 时间戳)。
  2. 变化检测:前端通过定时轮询或事件驱动(如 visibilitychange、路由切换)请求最新版本信息,与本地记录的版本做对比。
  3. 用户通知:发现版本变化后,通过非侵入式 UI(如顶部横幅)引导用户刷新页面。

Q2: 为什么不能直接用 window.location.reload() 强制刷新?

:强制刷新存在几个问题:

  • 用户体验差:如果用户正在填写表单或编辑内容,强制刷新会导致数据丢失。
  • 循环刷新风险:如果新版本本身有问题,可能导致页面加载后立即再次检测到"更新",陷入无限刷新循环。
  • 无告知:用户对突然的页面重载感到困惑,不知道发生了什么。

正确做法是提示用户,让用户自主选择合适的时机刷新,同时在刷新前检查未保存的数据。

Q3: 定时轮询和事件驱动各有什么优缺点?应该怎么选?

维度定时轮询事件驱动
实现难度简单(setInterval中等(需监听多个事件)
实时性取决于间隔时间关键时刻(切标签页等)几乎实时
带宽消耗持续发请求,浪费较多只在事件触发时请求,消耗少
覆盖度全面(不遗漏)可能有边界场景遗漏

实际项目推荐两者结合:用事件驱动覆盖高频场景(visibilitychange、路由切换、资源加载失败),用一个较长间隔(如 5 分钟)的定时轮询作为兜底,确保不会遗漏。

Q4: 如何避免版本检测带来的循环刷新问题?

:可以从以下几方面防护:

  1. 冷却期机制:在 sessionStorage 中记录上次刷新的时间戳,短时间内(如 10 秒)不重复刷新。
  2. 刷新次数限制:记录连续刷新次数,超过阈值后停止自动刷新,改为仅提示用户。
  3. 版本比较逻辑:只在版本号"更新"(而非"不同")时才触发提示,避免灰度发布等场景下的误判。
  4. 错误处理:版本文件请求失败时静默忽略,不做任何刷新操作。

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 类型的项目。