You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用动态时间戳更新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会尝试获取旧缓存键,失败后从网络请求,却忽略手动创建的新缓存。

请问我是否必须创建独立请求处理器来完全控制缓存机制,还是仍有办法实现缓存失效并为客户端替换新缓存值?

我的service-worker.js代码
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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 10:47:02