背景介绍
在前端应用开发中,当我们部署新版本后,往往需要用户手动刷新页面才能获取最新内容。这种体验并不理想,本文将介绍如何使用 Service Worker 结合版本号实现自动检测更新并提示用户刷新页面的功能。
实现原理
- 使用 Service Worker 拦截并缓存应用资源
- 通过检测 package.json 中的版本号来判断是否有更新
- 在检测到更新时通过消息机制通知页面
- 提供用户更新提示,并在用户确认后刷新页面
具体实现
1. Service Worker 实现(sw.js)
在 public 目录下创建 sw.js 文件,主要功能包括:
- 缓存静态资源
- 拦截网络请求
- 定期检查版本更新
- 向页面发送更新通知
关键代码:
const
const CACHE_FILES = [
'/',
'/index.html',
'/package.json'
];
let currentVersion = null;
// 安装 Service Worker
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(CACHE_FILES);
})
);
});
// 激活 Service Worker
self.addEventListener('activate', (event) => {
event.waitUntil(
Promise.all([
// 清理旧缓存
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter((name) => name !== CACHE_NAME)
.map((name) => caches.delete(name))
);
}),
// 立即接管页面
self.clients.claim()
]).then(() => {
// 激活后立即检查版本
checkVersion();
})
);
});
// 添加请求拦截器
self.addEventListener('fetch', (event) => {
// 特别处理 package.json 的请求,始终从网络获取最新版本
if (event.request.url.includes('package.json')) {
event.respondWith(
fetch(event.request, {
cache: 'no-cache',
headers: {
'Cache-Control': 'no-cache',
'Pragma': 'no-cache'
}
}).then(response => {
return response;
}).catch(error => {
throw error;
})
);
return;
}
event.respondWith(
caches.match(event.request).then((response) => {
// 如果在缓存中找到响应,则返回缓存的版本
if (response) {
return response;
}
// 否则发起网络请求
return fetch(event.request);
})
);
});
// 检查版本更新
async function checkVersion() {
try {
console.log('[SW] 开始检查版本...');
const response = await fetch('/package.json', {
cache: 'no-cache',
headers: {
'Cache-Control': 'no-cache',
'Pragma': 'no-cache'
}
});
const data = await response.json();
if (data.version !== currentVersion) {
console.log('[SW] 检测到新版本');
// 使用 includeUncontrolled: true 和 type: 'window' 参数
const clients = await self.clients.matchAll({
includeUncontrolled: true,
type: 'window'
});
console.log('[SW] 找到的客户端数量:', clients.length);
if (clients.length === 0) {
// 如果没有找到客户端,使用 BroadcastChannel
const bc = new BroadcastChannel('version-update');
bc.postMessage({
type: 'VERSION_UPDATE',
version: data.version,
showAlert: true
});
} else {
// 如果找到客户端,使用常规方式发送消息
clients.forEach((client) => {
client.postMessage({
type: 'VERSION_UPDATE',
version: data.version,
showAlert: true
});
});
}
currentVersion = data.version;
}
} catch (error) {
console.error('[SW] 版本检查失败:', error);
}
}
// 监听消息
self.addEventListener('message', (event) => {
console.log('[SW] 收到消息:', event.data);
if (event.data === 'CHECK_VERSION') {
console.log('[SW] 收到检查版本请求');
checkVersion();
}
});
// 每分钟检查一次版本(测试阶段)
setInterval(checkVersion, 5 * 60 * 1000);
// 立即进行首次检查
checkVersion();
2. Service Worker 注册和消息处理(serviceWorker.ts)
创建工具函数处理 Service Worker 的注册和消息通信:
typescript:utils/serviceWorker.ts
import { ElMessageBox, ElMessage } from 'element-plus';
// 显示更新提示
function showUpdateNotification() {
console.log('[Main] 显示更新提示');
ElMessageBox.confirm(
'系统发现新版本,为了更好的体验,建议立即更新',
'更新提示',
{
confirmButtonText: '立即更新',
cancelButtonText: '稍后更新',
type: 'warning',
}
)
.then(() => {
console.log('[Main] 用户确认更新');
// 清除缓存并刷新页面
return caches.keys().then(function(names) {
return Promise.all(names.map(name => caches.delete(name)));
});
})
.then(() => {
console.log('[Main] 缓存已清除,准备刷新页面');
window.location.reload(true);
})
.catch(() => {
console.log('[Main] 用户取消更新');
ElMessage({
type: 'info',
message: '您可以继续使用,但可能存在部分功能更新'
});
});
}
// 注册 Service Worker
export const registerServiceWorker = async () => {
// 本地开发环境不注册 Service Worker
if (import.meta.env.DEV) {
console.log('[Main] 本地开发环境,跳过 Service Worker 注册');
return;
}
if ('serviceWorker' in navigator) {
try {
console.log('[Main] 开始注册 Service Worker...');
// 注册 Service Worker
const registration = await navigator.serviceWorker.register('/sw.js');
console.log('[Main] Service Worker 注册成功:', registration);
// 添加常规消息监听
const messageHandler = (event: MessageEvent) => {
console.log('[Main] 收到 Service Worker 消息:', event.data);
if (event.data.type === 'VERSION_UPDATE') {
console.log('[Main] 收到版本更新通知');
showUpdateNotification();
}
};
// 添加 BroadcastChannel 监听
const bc = new BroadcastChannel('version-update');
bc.onmessage = (event) => {
console.log('[Main] 收到广播消息:', event.data);
if (event.data.type === 'VERSION_UPDATE') {
console.log('[Main] 收到版本更新通知');
showUpdateNotification();
}
};
navigator.serviceWorker.addEventListener('message', messageHandler);
if (registration.active) {
console.log('[Main] Service Worker 已激活,发送检查版本消息');
registration.active.postMessage('CHECK_VERSION');
}
} catch (error) {
console.error('[Main] Service Worker 注册失败:', error);
}
} else {
console.warn('[Main] 浏览器不支持 Service Worker');
// 不支持 Service Worker 时,使用定时器检查更新
console.log('[Main] 使用定时器检查更新');
setInterval(() => {
fetch('/package.json', {
cache: 'no-cache',
headers: {
'Cache-Control': 'no-cache',
'Pragma': 'no-cache'
}
})
.then(response => response.json())
.then(data => {
const currentVersion = localStorage.getItem('app-version');
if (currentVersion && currentVersion !== data.version) {
console.log('[Main] 检测到新版本');
showUpdateNotification();
}
localStorage.setItem('app-version', data.version);
})
.catch(error => {
console.error('[Main] 版本检查失败:', error);
});
}, 5 * 60 * 1000); // 每5分钟检查一次
}
};
// 检查更新
export const checkForUpdates = () => {
if (navigator.serviceWorker && navigator.serviceWorker.controller) {
console.log('[Main] 手动触发版本检查');
navigator.serviceWorker.controller.postMessage('CHECK_VERSION');
} else {
console.warn('[Main] Service Worker 未就绪,无法检查更新');
}
};
3. 自动版本号递增(autoIncrementVersion.ts)
为了方便版本管理,实现了一个 Vite 插件在构建时自动递增版本号:
typescript:autoIncrementVersion.ts
import { Plugin } from 'vite'
import fs from 'fs'
import path from 'path';
//版本号自增
const incrementVersion = (version: string) => {
const parts = version.split('.').map(Number);
parts[2]++;
if (parts[2] > 9) {
parts[2] = 0;
parts[1]++;
if (parts[1] > 9) {
parts[1] = 0;
parts[0]++;
}
}
return parts.join('.');
}
export default function autoIncrementVersion(): Plugin {
return {
name: 'vite:autoIncrementVersion',
apply: 'build',
//构建开始时的钩子
buildStart(options) {
if (options) {
try {
const pkgPath = path.resolve("./package.json");
console.log('Reading package.json from:', pkgPath);
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
console.log('Current version:', pkg.version);
pkg.version = incrementVersion(pkg.version);
console.log('New version:', pkg.version);
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
} catch (error) {
console.error('Error updating version:', error);
}
}
},
}
}
使用方法
- 在 main.ts 中注册 Service Worker:
typescript:main.ts
// 注册 Service Worker
import { registerServiceWorker } from '/@/utils/serviceWorker';
registerServiceWorker();
- 在 vite.config.ts 中添加版本自增插件:
typescript:vite.config.ts
import autoIncrementVersion from './autoIncrementVersion'
export default defineConfig({
plugins: [autoIncrementVersion()]
})
注意事项
- Service Worker 必须在 HTTPS 环境下运行(本地开发环境除外)
- 本地开发环境(localhost)可以正常使用
- 为了保证版本检查的准确性,package.json 的请求始终绕过缓存
- 提供了降级方案,在不支持 Service Worker 的环境下使用定时器检查更新
总结
通过 Service Worker 结合版本号检查,我们实现了一个可靠的前端应用自动更新机制。该方案具有以下优点:
- 自动检测新版本
- 提供友好的更新提示
- 支持离线缓存
- 包含降级方案
- 自动化的版本管理
这个方案可以显著提升用户体验,确保用户始终使用最新版本的应用。