Files
FixIt/assets/js/modules/pwa.ts
T
Cell 5cad4d3df9 refactor(assets): PWA improvements and app config snake_case migration (#790)
- Service worker: versioned cache name, navigation preload, stale-while-revalidate for images
- New PWAModule extracted from MiscModule with async registration and update detection
- SW update notification toast in Hugo template with i18n support (16 languages)
- Web app manifest template (site.webmanifest) generated from [params.app] config
- [params.app] keys migrated to snake_case: name, short_name, pwa, no_favicon, svg_favicon, mask_color, tile_color, theme_color
- enablePWA moved from [params] to [params.app].pwa
- theme_color no longer accepts single-value, only {light, dark} map
- EventBus: new fixit:sw-update event
- SCSS: new _sw-toast.scss widget for update notification
2026-06-24 12:43:37 +08:00

73 lines
2.3 KiB
TypeScript

import type { CoreService, PWAService } from '../core/tokens'
import { eventBus } from '../core/event-bus'
/**
* PWA module — service worker registration and update notification.
*
* Responsibilities:
* - Register service worker with update detection.
* - Periodically check for service worker updates.
* - Show update notification toast and handle refresh.
*/
export class PWAModule implements PWAService {
constructor(private readonly core: CoreService) {}
/** Register the service worker with update detection and notification binding. */
async setup() {
const pwa = this.core.config.PWA
if (!pwa?.enable || !('serviceWorker' in navigator))
return
const registration = await navigator.serviceWorker
.register(pwa.serviceWorkerURL)
.catch((error: unknown) => {
console.error('Service Worker registration failed:', error)
return null
})
if (!registration)
return
// Check for updates periodically (every hour)
setInterval(() => registration.update(), 3600000)
// Listen for new service worker installing
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing
if (!newWorker)
return
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
eventBus.emit('fixit:sw-update')
}
})
})
// Reload when a new service worker takes control
navigator.serviceWorker.addEventListener('controllerchange', () => {
window.location.reload()
})
this.bindUpdateNotification(registration)
}
/**
* Bind the template-rendered update notification toast.
* Shows the toast on `fixit:sw-update` and wires the refresh button to skip waiting.
*/
private bindUpdateNotification(registration: ServiceWorkerRegistration) {
const toast = document.querySelector<HTMLElement>('.sw-update-notification')
if (!toast)
return
eventBus.on('fixit:sw-update', () => {
toast.querySelector('.sw-update-btn')!.addEventListener('click', () => {
registration.waiting?.postMessage({ type: 'SKIP_WAITING' })
toast.classList.remove('visible')
}, { once: true })
requestAnimationFrame(() => toast.classList.add('visible'))
})
}
}