Files
timeline-frontend/public/sw.js
jianghao 97e4a135e1
All checks were successful
test/timeline-frontend/pipeline/head This commit looks good
refactor(api): 统一将/story/路径修改为/api/story/
修改服务端API路径前缀以保持一致性,涉及前端请求、nginx配置和代理设置
2026-02-26 12:32:32 +08:00

248 lines
5.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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);
}
}
}