/** * Service Worker - PWA 离线缓存 * * 功能描述: * 提供 PWA 离线访问能力,缓存关键资源。 * * 缓存策略: * - 静态资源:Cache First * - API 请求:Network First * - 图片资源:Stale While Revalidate * * @author Timeline Team * @date 2024 */ const CACHE_NAME = 'timeline-v1'; const STATIC_CACHE = 'timeline-static-v1'; const API_CACHE = 'timeline-api-v1'; const IMAGE_CACHE = 'timeline-image-v1'; // 需要缓存的静态资源 const STATIC_ASSETS = [ '/', '/index.html', '/manifest.json', '/icons/icon-192x192.png', '/icons/icon-512x512.png', ]; // 安装事件 self.addEventListener('install', (event) => { console.log('[ServiceWorker] 安装'); event.waitUntil( caches.open(STATIC_CACHE).then((cache) => { console.log('[ServiceWorker] 缓存静态资源'); return cache.addAll(STATIC_ASSETS); }) ); // 立即激活 self.skipWaiting(); }); // 激活事件 self.addEventListener('activate', (event) => { console.log('[ServiceWorker] 激活'); event.waitUntil( caches.keys().then((cacheNames) => { return Promise.all( cacheNames.map((cacheName) => { // 删除旧版本缓存 if (cacheName !== STATIC_CACHE && cacheName !== API_CACHE && cacheName !== IMAGE_CACHE) { console.log('[ServiceWorker] 删除旧缓存:', cacheName); return caches.delete(cacheName); } }) ); }) ); // 立即控制所有页面 self.clients.claim(); }); // 请求拦截 self.addEventListener('fetch', (event) => { const { request } = event; const url = new URL(request.url); // 只处理同源请求 if (url.origin !== location.origin) { return; } // 根据请求类型选择缓存策略 if (isApiRequest(url)) { // API 请求:Network First event.respondWith(networkFirst(request, API_CACHE)); } else if (isImageRequest(request)) { // 图片请求:Stale While Revalidate event.respondWith(staleWhileRevalidate(request, IMAGE_CACHE)); } else { // 静态资源:Cache First event.respondWith(cacheFirst(request, STATIC_CACHE)); } }); /** * 判断是否为 API 请求 */ function isApiRequest(url) { return url.pathname.startsWith('/api/') || url.pathname.startsWith('/api/story/') || url.pathname.startsWith('/file/') || url.pathname.startsWith('/user/'); } /** * 判断是否为图片请求 */ function isImageRequest(request) { return request.destination === 'image' || request.url.match(/\.(jpg|jpeg|png|gif|webp|svg)$/i); } /** * Cache First 策略 * 优先使用缓存,缓存不存在时请求网络 */ async function cacheFirst(request, cacheName) { const cache = await caches.open(cacheName); const cached = await cache.match(request); if (cached) { return cached; } try { const response = await fetch(request); if (response.ok) { cache.put(request, response.clone()); } return response; } catch (error) { // 网络请求失败,返回离线页面 if (request.mode === 'navigate') { return caches.match('/index.html'); } throw error; } } /** * Network First 策略 * 优先请求网络,失败时使用缓存 */ async function networkFirst(request, cacheName) { const cache = await caches.open(cacheName); try { const response = await fetch(request); if (response.ok) { cache.put(request, response.clone()); } return response; } catch (error) { const cached = await cache.match(request); if (cached) { return cached; } throw error; } } /** * Stale While Revalidate 策略 * 立即返回缓存,同时后台更新 */ async function staleWhileRevalidate(request, cacheName) { const cache = await caches.open(cacheName); const cached = await cache.match(request); // 后台更新缓存 const fetchPromise = fetch(request).then((response) => { if (response.ok) { cache.put(request, response.clone()); } return response; }); // 返回缓存或等待网络响应 return cached || fetchPromise; } // 推送通知 self.addEventListener('push', (event) => { console.log('[ServiceWorker] 收到推送'); const data = event.data ? event.data.json() : {}; const options = { body: data.body || '您有新的消息', icon: '/icons/icon-192x192.png', badge: '/icons/badge-72x72.png', vibrate: [100, 50, 100], data: { url: data.url || '/' }, actions: [ { action: 'open', title: '查看' }, { action: 'close', title: '关闭' } ] }; event.waitUntil( self.registration.showNotification(data.title || 'Timeline', options) ); }); // 通知点击 self.addEventListener('notificationclick', (event) => { console.log('[ServiceWorker] 通知点击'); event.notification.close(); if (event.action === 'open' || !event.action) { event.waitUntil( clients.openWindow(event.notification.data.url) ); } }); // 后台同步 self.addEventListener('sync', (event) => { console.log('[ServiceWorker] 后台同步:', event.tag); if (event.tag === 'sync-timeline') { event.waitUntil(syncTimeline()); } }); /** * 同步时间线数据 */ async function syncTimeline() { // 获取待同步的数据 const pendingData = await getPendingData(); for (const item of pendingData) { try { await fetch('/api/story/item', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(item) }); // 同步成功,删除待同步记录 await removePendingData(item.id); } catch (error) { console.error('[ServiceWorker] 同步失败:', error); } } }