PWA 渐进式 Web 应用实战:让工具站可安装、可离线
技术架构
什么是 PWA?
PWA(Progressive Web App)让 Web 应用具备接近原生 App 的体验:
| 能力 | 传统 Web | PWA |
|---|---|---|
| 安装到桌面 | ❌ | ✅ Add to Home Screen |
| 离线访问 | ❌ | ✅ Service Worker 缓存 |
| 推送通知 | ❌ | ✅ Push API |
| 全屏运行 | ❌ | ✅ standalone 模式 |
| 自动更新 | ✅ | ✅ 后台更新 SW |
对于工具库这类高频使用的工具站,PWA 意味着用户可以像原生 App 一样一键打开 PDF 合并、JSON 格式化等常用工具。
Web App Manifest
manifest.json 定义应用的安装信息:
{
"name": "工具库 - 在线工具合集",
"short_name": "工具库",
"start_url": "/zh-CN/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#2563eb",
"icons": [
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" }
]
}
| 字段 | 作用 |
|---|---|
display: standalone |
隐藏浏览器地址栏 |
start_url |
安装后打开的页面 |
theme_color |
状态栏/标题栏颜色 |
icons |
桌面图标(至少 192px 和 512px) |
Service Worker 缓存策略
缓存优先(工具静态资源)
// 适合:JS/CSS/WASM/字体等不常变化的资源
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then(cached => {
return cached || fetch(event.request).then(response => {
const clone = response.clone();
caches.open('static-v1').then(cache => cache.put(event.request, clone));
return response;
});
})
);
});
网络优先(HTML 页面)
// 适合:可能更新的 HTML 页面
event.respondWith(
fetch(event.request).catch(() => caches.match(event.request))
);
工具库的缓存分层
| 资源 | 策略 | TTL |
|---|---|---|
_next/static/** |
Cache First | 1 年 (hash 文件名) |
*.wasm |
Cache First | 1 年 |
*.html |
Network First | 1 小时 |
| i18n JSON | Stale While Revalidate | 1 天 |
离线工具体验
PWA 离线时的用户体验设计:
用户离线打开工具库
↓
Service Worker 返回缓存的 HTML/JS/CSS
↓
工具页面正常加载(SSG 预渲染的 HTML)
↓
Client Component 启动,工具逻辑在本地运行
↓
File API / WASM 处理文件 → 完全离线可用
工具库的所有核心工具(PDF、图片、JSON、加密)都基于浏览器本地 API,天然支持离线使用——PWA 只需确保静态资源被缓存。
安装提示最佳实践
let deferredPrompt: BeforeInstallPromptEvent;
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
deferredPrompt = e;
showInstallBanner(); // 显示自定义安装提示
});
installButton.addEventListener('click', async () => {
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
if (outcome === 'accepted') hideInstallBanner();
});
注意:不要过早显示安装提示。建议在用户成功使用 2-3 个工具后再提示,转化率更高。
总结
PWA 是工具站提升用户留存的有效手段。Web App Manifest 提供安装入口,Service Worker 实现离线缓存,而工具库本身的浏览器本地处理架构让离线工具体验与在线无异。
Service Worker 生命周期管理
Service Worker 有独立的生命周期,正确管理对用户体验至关重要:
安装 (install) → 等待激活 (waiting) → 激活 (activate) → 空闲 (idle) → 终止/重启
self.addEventListener('install', (event) => {
// 预缓存关键资源
event.waitUntil(
caches.open('static-v1').then(cache =>
cache.addAll(['/', '/styles/main.css', '/js/app.js'])
)
);
self.skipWaiting(); // 立即激活,不等待旧 SW 释放
});
self.addEventListener('activate', (event) => {
// 清理旧版本缓存
event.waitUntil(
caches.keys().then(keys =>
Promise.all(
keys.filter(key => key !== 'static-v1').map(key => caches.delete(key))
)
)
);
self.clients.claim(); // 立即接管所有页面
});
// 通知用户更新可用
self.addEventListener('message', (event) => {
if (event.data === 'SKIP_WAITING') self.skipWaiting();
});
关键规则:
skipWaiting()+clients.claim()确保更新后用户立即看到新版本- 每次修改 SW 代码,浏览器会自动触发 install → activate 流程
- 缓存版本号建议用构建时的 hash 替代手动命名
Workbox:SW 管理最佳实践
手写 Service Worker 容易出错,Google 的 Workbox 提供了声明式缓存策略:
import { registerRoute } from 'workbox-routing';
import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
// JS/CSS/WASM → 缓存优先,最多 200 个条目
registerRoute(
({ request }) => request.destination === 'script' || request.destination === 'style',
new CacheFirst({
cacheName: 'static-resources',
plugins: [new ExpirationPlugin({ maxEntries: 200, maxAgeSeconds: 30 * 24 * 3600 })]
})
);
// HTML/JSON → 网络优先,失败时回退缓存
registerRoute(
({ request }) => request.destination === 'document',
new NetworkFirst({ cacheName: 'pages', networkTimeoutSeconds: 3 })
);
工具库在生产中采用 Workbox,避免了手动实现 SW 带来的缓存一致性 Bug。
推送通知集成
PWA 支持 Web Push API,用于离线时触达用户:
// 请求通知权限
const permission = await Notification.requestPermission();
// 订阅推送
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(vapidPublicKey),
});
// 发送到后端保存
await fetch('/api/push/subscribe', {
method: 'POST',
body: JSON.stringify(subscription),
});
推送通知的实用场景:
- 批量文件处理完成通知("PDF 合并完成,共 15 页")
- 新工具上线提醒
- 长时间处理任务进度更新
本站提供浏览器本地工具,免注册即可试用 →
#PWA#Service Worker#离线#Web App Manifest#安装