如何用动态时间戳更新Service Worker预缓存版本号
我正在为WordPress主题构建Service Worker缓存机制,流程如下:
首次请求Service Worker时,通过fetchSavedJson函数从/wp-content/saved-posts.json获取页面列表执行预缓存。该JSON文件由后端save_post动作持续更新,预缓存操作在Service Worker的install事件中通过precacheAndRoute完成,所有版本号取自初始saved-posts.json,缓存键包含路由及带版本号的GET参数__WB_REVISION__。
saved-posts.json示例:
{ "data": [ { "url": "/", "revision": "1716314366" } ] }
当saved-posts.json更新时,需触发对应路由的缓存失效,但我不清楚如何针对precacheAndRoute生成的缓存键和值实现该操作。我曾尝试删除precacheAndRoute生成的缓存键对应值,但Service Worker会尝试获取旧缓存键,失败后从网络请求,却忽略手动创建的新缓存。
请问我是否必须创建独立请求处理器来完全控制缓存机制,还是仍有办法实现缓存失效并为客户端替换新缓存值?
import * as googleAnalytics from 'workbox-google-analytics' import {setCacheNameDetails, skipWaiting, clientsClaim} from 'workbox-core' import {registerRoute, setDefaultHandler} from 'workbox-routing' import {precacheAndRoute} from 'workbox-precaching' import {StaleWhileRevalidate, CacheFirst} from 'workbox-strategies' import {ExpirationPlugin} from 'workbox-expiration' import {CacheableResponsePlugin} from 'workbox-cacheable-response' const VERSION = "1.1" const PREFIX = 'sw' const JSON_URL = '/wp-content/saved-posts.json' const HOUR_IN_SECONDS = 60 * 60 const DAY_IN_SECONDS = 24 * HOUR_IN_SECONDS const WEEK_IN_SECONDS = DAY_IN_SECONDS * 7 const YEAR_IN_SECONDS = DAY_IN_SECONDS * 365 const MONTH_IN_SECONDS = Math.round(YEAR_IN_SECONDS / 12) const IGNORE_GLOBALS = [ '/admin', '/wp-admin/', '/wp-admin/admin-ajax.php', '/wp-login.php', ] const IGNORE_CACHES = [ 'google-fonts-stylesheets', 'google-fonts-webfonts', ] self.addEventListener('install', (event) => { console.log('sw-src.js: install') event.waitUntil( (async () => { await clearCache() })().catch(error => { console.error('Failed to fetch and cache saved posts:', error) }) ) }) self.addEventListener('activate', (event) => { console.log('sw-src.js: activate') }) self.addEventListener('message', async (event) => { if (event.data.type === 'LOAD') { await compareAndUpdateCache() await self.skipWaiting() const clients = await self.clients.matchAll({includeUncontrolled: true}) clients.forEach(client => client.postMessage({type: 'NEW_VERSION_READY'})) } if (event.data.type === 'SKIP_WAITING') { await self.skipWaiting() } if (event.data.type === 'CLEAR_OLD_CACHE') { await clearCache() } }) async function fetchSavedJson() { const response = await fetch(JSON_URL, {cache: "no-store"}) const json = await response.json() return json.data } async function getCurrentUrlTimestamp(url, savedPosts) { const urlObj = new URL(url) const post = savedPosts.find(item => item.url === urlObj.pathname) if (post) { return post.revision } return false } async function updateCacheValues(cacheName = null, savedPosts) { console.log("sw-src.js: updateCacheValues") if (cacheName === null) return false const cache = await caches.open(cacheName) const keys = await cache.keys() for (const key of keys) { try { const url = new URL(key.url) const cacheRevision = url.searchParams.get('__WB_REVISION__') const currentRevision = await getCurrentUrlTimestamp(key.url, savedPosts) if (currentRevision && currentRevision !== cacheRevision) { url.searchParams.set('__WB_REVISION__', currentRevision) const response = await fetch(url, {cache: "no-cache"}) if (response.ok) { await cache.put(key, response) } else { console.error(`Failed to fetch ${key.url} for updating cache`) } } } catch (error) { console.error(`Error updating cache for ${key.url}:`, error) } } } async function clearCache() { console.log("sw-src.js: clearCache") const cacheNames = await caches.keys() const savedPosts = await fetchSavedJson() for (const cacheName of cacheNames) { if (cacheName.includes('precache')) { updateCacheValues(cacheName, savedPosts) } else if (!IGNORE_CACHES.includes(cacheName)) { await caches.delete(cacheName) } } } async function compareAndUpdateCache() { console.log("sw-src.js: compareAndUpdateCache") const savedPosts = await fetchSavedJson() // console.log(savedPosts[0].revision) const keys = await caches.keys() const cacheNames = keys.filter(key => key.includes('precache')) if (cacheNames.length) { const cacheName = cacheNames[0] updateCacheValues(cacheName, savedPosts) } } registerRoute( ({request}) => request.destination === 'script' || request.destination === 'style', new StaleWhileRevalidate({cacheName: 'static'}) ) registerRoute( ({request}) => request.destination === 'image', new CacheFirst({ cacheName: "images", plugins: [ new ExpirationPlugin({maxEntries: 60, maxAgeSeconds: WEEK_IN_SECONDS}), ], }) ) registerRoute( ({url}) => url.origin === 'https://fonts.googleapis.com', new StaleWhileRevalidate({cacheName: `google-fonts-stylesheets`}) ) registerRoute( ({url}) => url.origin === 'https://fonts.gstatic.com', new CacheFirst({ cacheName: `google-fonts-webfonts`, plugins: [ new CacheableResponsePlugin({statuses: [0, 200]}), new ExpirationPlugin({maxAgeSeconds: YEAR_IN_SECONDS, maxEntries: 30}), ], }) ) googleAnalytics.initialize() setCacheNameDetails({ prefix: `${PREFIX}`, suffix: `v${VERSION}`, precache: 'precache', runtime: 'runtime', googleAnalytics: 'ga', }) fetchSavedJson().then(savedPosts => { const urlsToCache = savedPosts.map(post => ({ url: post.url, revision: post.revision, })) precacheAndRoute(urlsToCache) }) setDefaultHandler(new StaleWhileRevalidate()) precacheAndRoute(self.__WB_MANIFEST) skipWaiting() clientsClaim()
不需要完全替换成独立请求处理器,Workbox的预缓存机制有特定的更新逻辑,你可以通过以下方式实现缓存失效和更新:
1. 理解Workbox预缓存核心逻辑
precacheAndRoute生成的缓存键基于url + __WB_REVISION__参数,但Workbox匹配请求时会自动忽略__WB_REVISION__,直接匹配原始URL。预缓存更新依赖Service Worker版本更新或预缓存清单变化,你之前的问题在于手动修改缓存键后,Workbox的预缓存路由仍期望旧的__WB_REVISION__对应的缓存项,导致不识别手动添加的新缓存。
2. 正确的缓存更新流程
步骤一:调整Service Worker激活逻辑
将缓存更新逻辑移到activate事件中,确保旧缓存清理完成后再加载新预缓存清单:
self.addEventListener('activate', (event) => { console.log('sw-src.js: activate') event.waitUntil( (async () => { // 获取最新的预缓存清单 const savedPosts = await fetchSavedJson(); const urlsToCache = savedPosts.map(post => ({ url: post.url, revision: post.revision, })); // 清理所有旧的预缓存 const cacheNames = await caches.keys(); await Promise.all( cacheNames.filter(name => name.includes('precache')).map(name => caches.delete(name)) ); // 重新预缓存新清单 precacheAndRoute(urlsToCache); // 立即控制所有客户端 await clientsClaim(); })() ); });
步骤二:优化消息监听逻辑
修改message事件处理,触发Service Worker更新并通知客户端刷新:
self.addEventListener('message', async (event) => { if (event.data.type === 'UPDATE_CACHE') { // 跳过等待,激活新Service Worker await self.skipWaiting(); // 通知客户端刷新页面以加载新缓存 const clients = await self.clients.matchAll({includeUncontrolled: true}); clients.forEach(client => client.postMessage({type: 'REFRESH_PAGE'})); } // 保留原有消息处理逻辑 if (event.data.type === 'SKIP_WAITING') { await self.skipWaiting(); } if (event.data.type === 'CLEAR_OLD_CACHE') { await clearCache(); } });
步骤三:前端配合触发更新
在WordPress页面中添加轮询逻辑,检测saved-posts.json的revision变化,触发Service Worker更新:
// 前端页面代码 let lastRevisionMap = {}; async function checkCacheUpdates() { try { const response = await fetch('/wp-content/saved-posts.json', {cache: 'no-store'}); const {data} = await response.json(); let needUpdate = false; data.forEach(post => { if (lastRevisionMap[post.url] && lastRevisionMap[post.url] !== post.revision) { needUpdate = true; } lastRevisionMap[post.url] = post.revision; }); if (needUpdate && 'serviceWorker' in navigator) { const registration = await navigator.serviceWorker.ready; registration.active.postMessage({type: 'UPDATE_CACHE'}); } } catch (err) { console.error('检查缓存更新失败:', err); } } // 每5分钟轮询一次 setInterval(checkCacheUpdates, 5 * 60 * 1000); // 监听Service Worker的刷新通知 navigator.serviceWorker.addEventListener('message', (event) => { if (event.data.type === 'REFRESH_PAGE') { window.location.reload(); } });
3. 优化现有代码的问题
- 移除
install事件中的clearCache调用,避免预缓存未完成就被清理。 - 删除
precacheAndRoute(self.__WB_MANIFEST),避免Workbox默认清单与自定义预缓存清单冲突。
内容的提问来源于stack exchange,提问作者Andrius Solopovas

