背景介绍
在前端应用开发中,当我们部署新版本后,往往需要用户手动刷新页面才能获取最新内容。这种体验并不理想,本文将介绍如何使用 Service Worker 结合版本号实现自动检测更新并提示用户刷新页面的功能。
实现原理
- 使用 Service Worker 拦截并缓存应用资源
- 通过检测 version.json 中的版本号来判断是否有更新
- 在检测到更新时通过消息机制通知页面
- 提供用户更新提示,并在用户确认后刷新页面
具体实现
1. Service Worker 实现(sw.js)
在 public 目录下创建 sw.js 文件,主要功能包括:
- 缓存静态资源
- 拦截网络请求
- 定期检查版本更新
- 向页面发送更新通知
关键代码:
const CACHE_FILES = ["/", "/index.html", "./version.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) => {
// 特别处理 version.json 的请求,始终从网络获取最新版本
if (event.request.url.includes("./version.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("./version.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)
utils目录下serviceWorker.ts
创建工具函数处理 Service Worker 的注册和消息通信:
import { ElMessageBox, ElMessage } from "element-plus";
import { Local } from "/@/utils/storage";
// 显示更新提示
function showUpdateNotification() {
console.log("[Main] 显示更新提示");
ElMessageBox.confirm(
"系统发现新版本,为了更好的体验,建议立即更新",
"更新提示",
{
confirmButtonText: "立即更新",
cancelButtonText: "稍后更新",
type: "warning",
distinguishCancelAndClose: true,
}
)
.then(() => {
console.log("[Main] 用户确认更新");
// 检查是否支持 Cache API
if ("caches" in window) {
return caches.keys().then(function (names) {
return Promise.all(names.map((name) => caches.delete(name)))
.then(() => {
console.log("[Main] 缓存已清除,准备刷新页面");
Local.clear();
window.location.reload(true);
})
.catch((error) => {
console.error("[Main] 清除缓存失败:", error);
Local.clear();
// 如果清除缓存失败,仍然尝试刷新页面
window.location.reload(true);
});
});
} else {
Local.clear();
// 如果不支持 Cache API,直接刷新页面
console.log("[Main] 浏览器不支持 Cache API,直接刷新页面");
window.location.reload(true);
}
})
.catch((err) => {
if (err === "cancel") {
console.log("[Main] 用户取消更新");
ElMessage({
type: "info",
message: "您可以继续使用,但可能存在部分功能更新",
});
} else {
console.error("[Main] 更新过程发生错误:", err);
ElMessage({
type: "error",
message: "更新过程发生错误,请稍后重试",
});
}
});
}
// 注册 Service Worker
export const registerServiceWorker = async () => {
// 本地开发环境不注册 Service Worker
if (import.meta.env.DEV) {
console.log("[Main] 本地开发环境,注销 Service Worker");
if ("serviceWorker" in navigator) {
navigator.serviceWorker.getRegistrations().then(function (registrations) {
for (let registration of registrations) {
registration.unregister();
}
});
}
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");
console.log("[Main] 使用定时器检查更新");
setInterval(() => {
fetch("/version.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);
}
};
// 检查更新
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)
根目录下创建 autoIncrementVersion.ts
为了方便版本管理,实现了一个 Vite 插件在构建时自动递增版本号:
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("./public/version.json");
console.log("Reading version.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);
}
}
},
};
}
4. 创建版本号json(public/version.json)
在 public 目录下创建 version.json 文件
{
"version": "1.0.8",
"buildTime": "",
"environment": "production"
}
使用方法
- 在 main.ts 中注册 Service Worker:
// 注册 Service Worker
import { registerServiceWorker } from '/@/utils/serviceWorker';
registerServiceWorker();
- 在 vite.config.ts 中添加版本自增插件:
import autoIncrementVersion from './autoIncrementVersion'
export default defineConfig({
plugins: [autoIncrementVersion()],
build: {
plugins: [
{
name: 'version-inject',
buildStart() {
// 在构建开始时就创建 version.json
const versionData = {
version: process.env.npm_package_version,
buildTime: new Date().toISOString(),
environment: process.env.NODE_ENV || 'production'
};
// 写入到 public 目录
const fs = require('fs');
const path = require('path');
const publicDir = path.resolve(__dirname, 'public');
// 确保 public 目录存在
if (!fs.existsSync(publicDir)) {
fs.mkdirSync(publicDir, { recursive: true });
}
// 写入 version.json
fs.writeFileSync(
path.join(publicDir, 'version.json'),
JSON.stringify(versionData, null, 2)
);
console.log('[Build] 在 public 目录生成版本信息:', versionData);
},
generateBundle() {
// 在构建输出时创建 version.json
const versionData = {
version: process.env.npm_package_version,
buildTime: new Date().toISOString(),
environment: process.env.NODE_ENV || 'production'
};
console.log('[Build] 在构建目录生成版本信息:', versionData);
this.emitFile({
type: 'asset',
fileName: 'version.json',
source: JSON.stringify(versionData, null, 2)
});
}
}
]
},
})
注意事项
- Service Worker 必须在 HTTPS 环境下运行(本地开发环境除外)
- 本地开发环境(localhost)可以正常使用
- 为了保证版本检查的准确性,package.json 的请求始终绕过缓存
- 提供了降级方案,在不支持 Service Worker 的环境下使用定时器检查更新
总结
通过 Service Worker 结合版本号检查,我们实现了一个可靠的前端应用自动更新机制。该方案具有以下优点:
- 自动检测新版本
- 提供友好的更新提示
- 支持离线缓存
- 包含降级方案
- 自动化的版本管理
这个方案可以显著提升用户体验,确保用户始终使用最新版本的应用。