refactor: remove legacy service worker and manifest, integrate Vite PWA plugin
Some checks failed
Docker Build & Deploy / Build Docker Image (push) Failing after 40s
Docker Build & Deploy / Deploy to Production (push) Has been skipped
Docker Build & Deploy / Cleanup Dangling Images (push) Has been skipped

- Deleted the old service worker implementation and manifest file.
- Integrated Vite PWA plugin for improved service worker management.
- Updated main.js to remove service worker registration logic.
- Refactored registerServiceWorker.js to use the new PWA registration method.
- Added new service worker (sw.js) with caching strategies for API and static resources.
- Updated vite.config.js to include PWA configuration and manifest details.
This commit is contained in:
孙诚
2026-01-07 15:32:22 +08:00
parent 1bd6b688c1
commit 0c09d7aa14
8 changed files with 3013 additions and 347 deletions

View File

@@ -30,6 +30,11 @@
"globals": "^16.5.0",
"prettier": "3.6.2",
"vite": "^7.2.4",
"vite-plugin-vue-devtools": "^8.0.5"
"vite-plugin-pwa": "^1.2.0",
"vite-plugin-vue-devtools": "^8.0.5",
"workbox-expiration": "^7.4.0",
"workbox-precaching": "^7.4.0",
"workbox-routing": "^7.4.0",
"workbox-strategies": "^7.4.0"
}
}

2803
Web/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,76 +0,0 @@
{
"name": "账单",
"short_name": "账单",
"description": "个人账单管理与邮件解析",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#1989fa",
"orientation": "portrait-primary",
"icons": [
{
"src": "/icons/icon-72x72.svg",
"sizes": "72x72",
"type": "image/svg+xml",
"purpose": "any maskable"
},
{
"src": "/icons/icon-96x96.svg",
"sizes": "96x96",
"type": "image/svg+xml",
"purpose": "any maskable"
},
{
"src": "/icons/icon-128x128.svg",
"sizes": "128x128",
"type": "image/svg+xml",
"purpose": "any maskable"
},
{
"src": "/icons/icon-144x144.svg",
"sizes": "144x144",
"type": "image/svg+xml",
"purpose": "any maskable"
},
{
"src": "/icons/icon-152x152.svg",
"sizes": "152x152",
"type": "image/svg+xml",
"purpose": "any maskable"
},
{
"src": "/icons/icon-192x192.svg",
"sizes": "192x192",
"type": "image/svg+xml",
"purpose": "any maskable"
},
{
"src": "/icons/icon-384x384.svg",
"sizes": "384x384",
"type": "image/svg+xml",
"purpose": "any maskable"
},
{
"src": "/icons/icon-512x512.svg",
"sizes": "512x512",
"type": "image/svg+xml",
"purpose": "any maskable"
}
],
"categories": ["finance", "productivity"],
"screenshots": [],
"shortcuts": [
{
"name": "查看账单",
"short_name": "账单",
"description": "快速查看账单列表",
"url": "/",
"icons": [
{
"src": "/icons/icon-96x96.png",
"sizes": "96x96"
}
]
}
]
}

View File

@@ -1,172 +0,0 @@
const CACHE_NAME = 'emailbill-v1';
const urlsToCache = [
'/',
'/index.html',
'/favicon.ico',
'/manifest.json'
];
// 安装 Service Worker
self.addEventListener('install', (event) => {
console.log('[Service Worker] 安装中...');
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => {
console.log('[Service Worker] 缓存文件');
return cache.addAll(urlsToCache);
})
);
});
// 监听跳过等待消息
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
// 激活 Service Worker
self.addEventListener('activate', (event) => {
console.log('[Service Worker] 激活中...');
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (cacheName !== CACHE_NAME) {
console.log('[Service Worker] 删除旧缓存:', cacheName);
return caches.delete(cacheName);
}
})
);
}).then(() => self.clients.claim())
);
});
// 拦截请求
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
// 跳过跨域请求
if (url.origin !== location.origin) {
return;
}
// API请求使用网络优先策略
if (url.pathname.startsWith('/api/')) {
event.respondWith(
fetch(request)
.then((response) => {
// 克隆响应以便缓存
const responseClone = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(request, responseClone);
});
return response;
})
.catch(() => {
// 网络失败时尝试从缓存获取
return caches.match(request);
})
);
return;
}
// 静态资源使用缓存优先策略
event.respondWith(
caches.match(request)
.then((response) => {
if (response) {
return response;
}
return fetch(request).then((response) => {
// 检查是否是有效响应
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
const responseClone = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(request, responseClone);
});
return response;
});
})
.catch(() => {
// 返回离线页面或默认内容
if (request.destination === 'document') {
return caches.match('/index.html');
}
})
);
});
// 后台同步
self.addEventListener('sync', (event) => {
console.log('[Service Worker] 后台同步:', event.tag);
if (event.tag === 'sync-data') {
event.waitUntil(syncData());
}
});
// 推送通知
self.addEventListener('push', (event) => {
console.log('[Service Worker] 收到推送消息');
let data = { title: '账单管理', body: '您有新的消息', url: '/', icon: '/icons/icon-192x192.png' };
if (event.data) {
try {
const json = event.data.json();
data = { ...data, ...json };
} catch {
data.body = event.data.text();
}
}
const options = {
body: data.body,
icon: data.icon,
badge: '/icons/icon-72x72.png',
vibrate: [200, 100, 200],
tag: 'emailbill-notification',
requireInteraction: false,
data: { url: data.url }
};
event.waitUntil(
self.registration.showNotification(data.title, options)
);
});
// 通知点击
self.addEventListener('notificationclick', (event) => {
console.log('[Service Worker] 通知被点击');
event.notification.close();
const urlToOpen = event.notification.data?.url || '/';
event.waitUntil(
clients.matchAll({ type: 'window', includeUncontrolled: true }).then((windowClients) => {
// 如果已经打开了该 URL则聚焦
for (let i = 0; i < windowClients.length; i++) {
const client = windowClients[i];
if (client.url === urlToOpen && 'focus' in client) {
return client.focus();
}
}
// 否则打开新窗口
if (clients.openWindow) {
return clients.openWindow(urlToOpen);
}
})
);
});
// 数据同步函数
async function syncData() {
try {
// 这里添加需要同步的逻辑
console.log('[Service Worker] 执行数据同步');
} catch (error) {
console.error('[Service Worker] 同步失败:', error);
}
}

View File

@@ -10,9 +10,6 @@ import vant from 'vant'
import { ConfigProvider } from 'vant';
import 'vant/lib/index.css'
// 注册 Service Worker
import { register } from './registerServiceWorker'
const app = createApp(App)
app.use(createPinia())
@@ -22,7 +19,3 @@ app.use(ConfigProvider);
app.mount('#app')
// 在生产环境注册 Service Worker
if (import.meta.env.PROD) {
register()
}

View File

@@ -1,78 +1,40 @@
import { ref } from 'vue';
import { useRegisterSW } from 'virtual:pwa-register/vue';
export const needRefresh = ref(false);
let swRegistration = null;
export async function updateServiceWorker() {
if (swRegistration && swRegistration.waiting) {
await swRegistration.waiting.postMessage({ type: 'SKIP_WAITING' });
const {
needRefresh,
updateServiceWorker,
} = useRegisterSW({
onRegistered(r) {
if (r) {
// 每小时检查一次更新
setInterval(() => {
r.update();
}, 60 * 60 * 1000);
// 当页面重新获得焦点时检查更新
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
r.update();
console.log('[PWA] 页面进入前台,检查更新');
}
});
}
},
onRegisterError(error) {
console.error('[PWA] Service Worker 注册失败:', error);
}
}
});
export { needRefresh, updateServiceWorker };
// 兼容老代码的 register 调用
export function register() {
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
const swUrl = `/service-worker.js`;
navigator.serviceWorker
.register(swUrl)
.then((registration) => {
swRegistration = registration;
console.log('[SW] Service Worker 注册成功:', registration.scope);
// 检查更新
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
console.log('[SW] 发现新版本');
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// 新的 Service Worker 已安装,提示用户刷新
console.log('[SW] 新版本可用,请刷新页面');
needRefresh.value = true;
} else {
// 首次安装
console.log('[SW] 内容已缓存,可离线使用');
}
}
});
});
// 定期检查更新
setInterval(() => {
registration.update();
}, 60 * 60 * 1000); // 每小时检查一次
})
.catch((error) => {
console.error('[SW] Service Worker 注册失败:', error);
});
// 监听 Service Worker 控制器变化
navigator.serviceWorker.addEventListener('controllerchange', () => {
console.log('[SW] 控制器已更改,页面将刷新');
window.location.reload();
});
});
}
console.log('[PWA] 自动注册中...');
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready
.then((registration) => {
registration.unregister();
})
.catch((error) => {
console.error(error.message);
});
}
}
// 请求通知权限
export function requestNotificationPermission() {
if ('Notification' in window && 'serviceWorker' in navigator) {
if ('Notification' in window) {
Notification.requestPermission().then((permission) => {
if (permission === 'granted') {
console.log('[SW] 通知权限已授予');
@@ -81,7 +43,7 @@ export function requestNotificationPermission() {
}
}
// 后台同步
// 后台同步兼容性导出
export function registerBackgroundSync(tag = 'sync-data') {
if ('serviceWorker' in navigator && 'SyncManager' in window) {
navigator.serviceWorker.ready.then((registration) => {
@@ -93,3 +55,4 @@ export function registerBackgroundSync(tag = 'sync-data') {
});
}
}

111
Web/src/sw.js Normal file
View File

@@ -0,0 +1,111 @@
import { precacheAndRoute } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { NetworkFirst, CacheFirst } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
// 注入预缓存清单
precacheAndRoute(self.__WB_MANIFEST);
// API 请求使用网络优先策略
registerRoute(
({ url }) => url.pathname.startsWith('/api/'),
new NetworkFirst({
cacheName: 'api-cache',
plugins: [
new ExpirationPlugin({
maxEntries: 50,
maxAgeSeconds: 24 * 60 * 60, // 24 Hours
}),
],
})
);
// 静态资源使用缓存优先策略 (不在预缓存清单中的)
registerRoute(
({ request }) => request.destination === 'image' || request.destination === 'style' || request.destination === 'script',
new CacheFirst({
cacheName: 'static-resources',
plugins: [
new ExpirationPlugin({
maxEntries: 60,
maxAgeSeconds: 30 * 24 * 60 * 60, // 30 Days
}),
],
})
);
// 监听跳过等待消息
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
// 后台同步
self.addEventListener('sync', (event) => {
console.log('[Service Worker] 后台同步:', event.tag);
if (event.tag === 'sync-data') {
event.waitUntil(syncData());
}
});
// 推送通知
self.addEventListener('push', (event) => {
console.log('[Service Worker] 收到推送消息');
let data = { title: '账单管理', body: '您有新的消息', url: '/', icon: '/icons/icon-192x192.png' };
if (event.data) {
try {
const json = event.data.json();
data = { ...data, ...json };
} catch {
data.body = event.data.text();
}
}
const options = {
body: data.body,
icon: data.icon,
badge: '/icons/icon-72x72.png',
vibrate: [200, 100, 200],
tag: 'emailbill-notification',
requireInteraction: false,
data: { url: data.url }
};
event.waitUntil(
self.registration.showNotification(data.title, options)
);
});
// 通知点击
self.addEventListener('notificationclick', (event) => {
console.log('[Service Worker] 通知被点击');
event.notification.close();
const urlToOpen = event.notification.data?.url || '/';
event.waitUntil(
clients.matchAll({ type: 'window', includeUncontrolled: true }).then((windowClients) => {
// 如果已经打开了该 URL则聚焦
for (let i = 0; i < windowClients.length; i++) {
const client = windowClients[i];
if (client.url === urlToOpen && 'focus' in client) {
return client.focus();
}
}
// 否则打开新窗口
if (clients.openWindow) {
return clients.openWindow(urlToOpen);
}
})
);
});
// 数据同步函数
async function syncData() {
try {
// 这里添加需要同步的逻辑
console.log('[Service Worker] 执行数据同步');
} catch (error) {
console.error('[Service Worker] 同步失败:', error);
}
}

View File

@@ -3,12 +3,97 @@ import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
import { VitePWA } from 'vite-plugin-pwa'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
VitePWA({
strategies: 'injectManifest',
srcDir: 'src',
filename: 'sw.js',
registerType: 'prompt', // 使用提示模式,以便在 App.vue 中显示刷新按钮
injectRegister: 'auto',
manifest: {
name: '账单',
short_name: '账单',
description: '个人账单管理与邮件解析',
theme_color: '#1989fa',
background_color: '#ffffff',
display: 'standalone',
orientation: 'portrait-primary',
icons: [
{
src: 'icons/icon-72x72.svg',
sizes: '72x72',
type: 'image/svg+xml',
purpose: 'any maskable'
},
{
src: 'icons/icon-96x96.svg',
sizes: '96x96',
type: 'image/svg+xml',
purpose: 'any maskable'
},
{
src: 'icons/icon-128x128.svg',
sizes: '128x128',
type: 'image/svg+xml',
purpose: 'any maskable'
},
{
src: 'icons/icon-144x144.svg',
sizes: '144x144',
type: 'image/svg+xml',
purpose: 'any maskable'
},
{
src: 'icons/icon-152x152.svg',
sizes: '152x152',
type: 'image/svg+xml',
purpose: 'any maskable'
},
{
src: 'icons/icon-192x192.svg',
sizes: '192x192',
type: 'image/svg+xml',
purpose: 'any maskable'
},
{
src: 'icons/icon-384x384.svg',
sizes: '384x384',
type: 'image/svg+xml',
purpose: 'any maskable'
},
{
src: 'icons/icon-512x512.svg',
sizes: '512x512',
type: 'image/svg+xml',
purpose: 'any maskable'
}
],
shortcuts: [
{
name: '查看账单',
short_name: '账单',
description: '快速查看账单列表',
url: '/',
icons: [
{
src: 'icons/icon-96x96.png',
sizes: '96x96'
}
]
}
]
},
devOptions: {
enabled: true,
type: 'module'
}
})
],
resolve: {
alias: {