From 8e6efd1c51dca4cca7d3d27be2ee947042208ff9 Mon Sep 17 00:00:00 2001 From: biss Date: Fri, 17 Jul 2026 13:02:21 +0800 Subject: [PATCH] PWA --- public/images/apple-touch-icon.png | Bin 0 -> 2660 bytes public/images/pwa-192x192.png | Bin 0 -> 2783 bytes public/images/pwa-512x512.png | Bin 0 -> 7629 bytes public/images/pwa-maskable-512x512.png | Bin 0 -> 6685 bytes public/js/pwa.js | 104 +++++++++++++++++++++++ public/manifest.webmanifest | 46 +++++++++++ public/offline.html | 29 +++++++ public/sw.js | 110 +++++++++++++++++++++++++ src/components/PwaPrompt.astro | 11 +++ src/layouts/BaseLayout.astro | 7 ++ src/styles/site.css | 1 + src/styles/site/16-pwa.css | 47 +++++++++++ 12 files changed, 355 insertions(+) create mode 100644 public/images/apple-touch-icon.png create mode 100644 public/images/pwa-192x192.png create mode 100644 public/images/pwa-512x512.png create mode 100644 public/images/pwa-maskable-512x512.png create mode 100644 public/js/pwa.js create mode 100644 public/manifest.webmanifest create mode 100644 public/offline.html create mode 100644 public/sw.js create mode 100644 src/components/PwaPrompt.astro create mode 100644 src/styles/site/16-pwa.css diff --git a/public/images/apple-touch-icon.png b/public/images/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..54af06e6aa95bff7d1509eb30410c2795e3d4758 GIT binary patch literal 2660 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 { + if (!('serviceWorker' in navigator)) return; + + const toast = document.querySelector('[data-pwa-toast]'); + const title = toast?.querySelector('[data-pwa-title]'); + const message = toast?.querySelector('[data-pwa-message]'); + const primary = toast?.querySelector('[data-pwa-primary]'); + const close = toast?.querySelector('[data-pwa-close]'); + let installPrompt = null; + let waitingWorker = null; + let toastMode = ''; + let refreshing = false; + + const showToast = (mode, heading, detail, actionLabel) => { + if (!toast || !title || !message || !primary) return; + toastMode = mode; + title.textContent = heading; + message.textContent = detail; + primary.textContent = actionLabel; + primary.hidden = !actionLabel; + toast.hidden = false; + }; + + const hideToast = () => { + if (toast) toast.hidden = true; + }; + + const installDismissedRecently = () => { + try { + const dismissedAt = Number(localStorage.getItem('pwa-install-dismissed-at')); + return dismissedAt > 0 && Date.now() - dismissedAt < 30 * 86400000; + } catch { + return false; + } + }; + + window.addEventListener('beforeinstallprompt', (event) => { + event.preventDefault(); + installPrompt = event; + if (!installDismissedRecently()) { + window.setTimeout(() => { + if (installPrompt) showToast('install', '安装 Bi\'s Blog', '添加到桌面,像应用一样打开并阅读缓存过的文章。', '立即安装'); + }, 2500); + } + }); + + window.addEventListener('appinstalled', () => { + installPrompt = null; + showToast('ready', '安装完成', '博客已经添加到你的设备。', '知道了'); + }); + + primary?.addEventListener('click', async () => { + if (toastMode === 'install' && installPrompt) { + await installPrompt.prompt(); + await installPrompt.userChoice; + installPrompt = null; + hideToast(); + return; + } + + if (toastMode === 'update' && waitingWorker) { + refreshing = true; + waitingWorker.postMessage({ type: 'SKIP_WAITING' }); + return; + } + + hideToast(); + }); + + close?.addEventListener('click', () => { + if (toastMode === 'install') { + try { localStorage.setItem('pwa-install-dismissed-at', String(Date.now())); } catch {} + } + hideToast(); + }); + + navigator.serviceWorker.addEventListener('controllerchange', () => { + if (refreshing) window.location.reload(); + }); + + navigator.serviceWorker.register('/sw.js', { scope: '/' }).then((registration) => { + if (registration.waiting && navigator.serviceWorker.controller) { + waitingWorker = registration.waiting; + showToast('update', '发现新版本', '刷新后即可使用博客的最新内容和功能。', '立即更新'); + } + + registration.addEventListener('updatefound', () => { + const worker = registration.installing; + worker?.addEventListener('statechange', () => { + if (worker.state === 'installed' && navigator.serviceWorker.controller) { + waitingWorker = worker; + showToast('update', '发现新版本', '刷新后即可使用博客的最新内容和功能。', '立即更新'); + } + }); + }); + }).catch((error) => { + console.warn('PWA registration failed:', error); + }); + + const params = new URLSearchParams(window.location.search); + if (params.get('action') === 'search') { + window.setTimeout(() => document.querySelector('.search-typesense-trigger')?.click(), 350); + } +})(); diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest new file mode 100644 index 0000000..40337ed --- /dev/null +++ b/public/manifest.webmanifest @@ -0,0 +1,46 @@ +{ + "id": "/", + "name": "Bi's Blog", + "short_name": "Bi's Blog", + "description": "好奇心转化为代码的空间。", + "lang": "zh-CN", + "start_url": "/", + "scope": "/", + "display": "standalone", + "display_override": ["window-controls-overlay", "standalone", "minimal-ui"], + "theme_color": "#0f766e", + "background_color": "#f7f5ef", + "categories": ["blog", "education", "technology"], + "icons": [ + { + "src": "/images/pwa-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/images/pwa-512x512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "/images/pwa-maskable-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ], + "shortcuts": [ + { + "name": "搜索文章", + "short_name": "搜索", + "url": "/?action=search", + "icons": [{ "src": "/images/pwa-192x192.png", "sizes": "192x192" }] + }, + { + "name": "随机文章", + "short_name": "随机文章", + "url": "/go/", + "icons": [{ "src": "/images/pwa-192x192.png", "sizes": "192x192" }] + } + ] +} diff --git a/public/offline.html b/public/offline.html new file mode 100644 index 0000000..0fac7f4 --- /dev/null +++ b/public/offline.html @@ -0,0 +1,29 @@ + + + + + + + 暂时离线 | Bi's Blog + + + +
+ +

网络暂时走丢了

+

已经读过的文章仍可从浏览器历史或返回按钮打开。网络恢复后,再来继续探索吧。

+ + 返回首页 +
+ + diff --git a/public/sw.js b/public/sw.js new file mode 100644 index 0000000..091d1c1 --- /dev/null +++ b/public/sw.js @@ -0,0 +1,110 @@ +const CACHE_VERSION = '2026-07-17-v1'; +const CACHE_PREFIX = 'bis-blog-'; +const CORE_CACHE = `${CACHE_PREFIX}core-${CACHE_VERSION}`; +const PAGE_CACHE = `${CACHE_PREFIX}pages-${CACHE_VERSION}`; +const ASSET_CACHE = `${CACHE_PREFIX}assets-${CACHE_VERSION}`; +const IMAGE_CACHE = `${CACHE_PREFIX}images-${CACHE_VERSION}`; + +const CORE_ASSETS = [ + '/', + '/offline.html', + '/manifest.webmanifest', + '/images/Bi.ico', + '/images/pwa-192x192.png', + '/images/pwa-512x512.png', + '/images/pwa-maskable-512x512.png', + '/images/apple-touch-icon.png', +]; + +self.addEventListener('install', (event) => { + event.waitUntil( + caches.open(CORE_CACHE) + .then((cache) => cache.addAll(CORE_ASSETS)) + ); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil((async () => { + const cacheNames = await caches.keys(); + await Promise.all( + cacheNames + .filter((name) => name.startsWith(CACHE_PREFIX) && ![CORE_CACHE, PAGE_CACHE, ASSET_CACHE, IMAGE_CACHE].includes(name)) + .map((name) => caches.delete(name)) + ); + await self.clients.claim(); + const clients = await self.clients.matchAll({ type: 'window' }); + clients.forEach((client) => client.postMessage({ type: 'OFFLINE_READY' })); + })()); +}); + +self.addEventListener('message', (event) => { + if (event.data?.type === 'SKIP_WAITING') self.skipWaiting(); +}); + +async function trimCache(cacheName, maxEntries) { + const cache = await caches.open(cacheName); + const keys = await cache.keys(); + if (keys.length > maxEntries) { + await Promise.all(keys.slice(0, keys.length - maxEntries).map((key) => cache.delete(key))); + } +} + +async function networkFirst(request) { + try { + const response = await fetch(request); + if (response.ok) { + const cache = await caches.open(PAGE_CACHE); + await cache.put(request, response.clone()); + void trimCache(PAGE_CACHE, 40); + } + return response; + } catch { + return (await caches.match(request)) || (await caches.match('/offline.html')); + } +} + +async function cacheFirst(request, cacheName, maxEntries) { + const cached = await caches.match(request); + if (cached) return cached; + + const response = await fetch(request); + if (response.ok || response.type === 'opaque') { + const cache = await caches.open(cacheName); + await cache.put(request, response.clone()); + void trimCache(cacheName, maxEntries); + } + return response; +} + +async function staleWhileRevalidate(request) { + const cache = await caches.open(ASSET_CACHE); + const cached = await cache.match(request); + const network = fetch(request).then((response) => { + if (response.ok) cache.put(request, response.clone()); + return response; + }).catch(() => cached); + return cached || network; +} + +self.addEventListener('fetch', (event) => { + const { request } = event; + if (request.method !== 'GET') return; + + const url = new URL(request.url); + if (request.mode === 'navigate') { + event.respondWith(networkFirst(request)); + return; + } + + if (request.destination === 'image') { + event.respondWith(cacheFirst(request, IMAGE_CACHE, 100)); + return; + } + + if ( + url.origin === self.location.origin && + ['style', 'script', 'font', 'worker'].includes(request.destination) + ) { + event.respondWith(staleWhileRevalidate(request)); + } +}); diff --git a/src/components/PwaPrompt.astro b/src/components/PwaPrompt.astro new file mode 100644 index 0000000..90e2a38 --- /dev/null +++ b/src/components/PwaPrompt.astro @@ -0,0 +1,11 @@ + diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index 6034080..4df9f5c 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -2,6 +2,7 @@ import '../styles/site.css'; import CustomContextMenu from '@/components/CustomContextMenu.astro'; import HitokotoCard from '@/components/HitokotoCard.astro'; +import PwaPrompt from '@/components/PwaPrompt.astro'; import { publicSiteConfig, siteConfig } from '../../site.config.mjs'; interface Props { @@ -90,6 +91,10 @@ const consoleSites = [ {keywords.map((keyword) => )} {jsonLd && } + + + + +