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#安装