refactor: remove legacy service worker and manifest, integrate Vite PWA plugin
- 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:
@@ -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
2803
Web/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
111
Web/src/sw.js
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user