PWA 实战:从零打造离线可用的 Web 应用

什么是 PWA?

PWA(Progressive Web App)是一种结合了 Web 和原生 App 优势的技术方案。它让 Web 应用能够:

  • 📱 添加到桌面:像原生 App 一样安装
  • 🔌 离线可用:没有网络也能访问
  • 🔔 推送通知:接收后台消息推送
  • 秒开体验:缓存策略优化加载速度
核心三要素:
  1. manifest.json - 应用配置文件
  2. Service Worker - 离线与缓存核心
  3. HTTPS - 安全传输(开发环境 localhost 除外)

manifest.json 配置

这是 PWA 的「身份证」,告诉浏览器如何展示你的应用:

{
  "name": "戒色打卡",
  "short_name": "打卡",
  "description": "自律给我自由 - 习惯追踪器",
  "theme_color": "#6366f1",
  "background_color": "#030712",
  "display": "standalone",
  "orientation": "portrait",
  "scope": "/",
  "start_url": "/",
  "icons": [
    {
      "src": "/icon-192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/icon-512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "any maskable"
    }
  ]
}

关键配置说明

  • display: "standalone" - 隐藏浏览器地址栏,像原生 App
  • theme_color - 顶部状态栏颜色
  • purpose: "maskable" - 支持 Android 自适应图标
⚠️ 图标要求:
  • 至少提供 192x192 和 512x512 两个尺寸
  • 推荐 PNG 格式,支持透明背景
  • maskable 图标需要安全区域设计(中心 80%)

Service Worker 核心

Service Worker 是 PWA 的「大脑」,运行在独立线程,拦截所有网络请求。

1. 注册 Service Worker

在 HTML 中注册:

<script>
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/sw.js')
      .then((registration) => {
        console.log('✅ Service Worker 注册成功');
      })
      .catch((error) => {
        console.log('❌ 注册失败:', error);
      });
  });
}
</script>

2. Service Worker 生命周期

// 安装事件 - 预缓存静态资源
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      return cache.addAll([
        '/',
        '/index.html',
        '/manifest.json',
        '/favicon.svg'
      ]);
    })
  );
  self.skipWaiting(); // 跳过等待,立即激活
});

// 激活事件 - 清理旧缓存
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((cacheNames) => {
      return Promise.all(
        cacheNames
          .filter((name) => name !== CACHE_NAME)
          .map((name) => caches.delete(name))
      );
    })
  );
  self.clients.claim(); // 立即控制所有页面
});

缓存策略详解

选择正确的缓存策略是 PWA 性能的关键:

策略一:缓存优先(Cache First)

适用于不常变化的静态资源:

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((cached) => {
      return cached || fetch(event.request);
    })
  );
});

策略二:网络优先(Network First)

适用于需要实时性的 API 请求:

self.addEventListener('fetch', (event) => {
  if (event.request.url.includes('/api/')) {
    event.respondWith(
      fetch(event.request)
        .then((response) => {
          // 缓存成功响应
          const clone = response.clone();
          caches.open(CACHE_NAME).then((cache) => {
            cache.put(event.request, clone);
          });
          return response;
        })
        .catch(() => caches.match(event.request)) // 离线用缓存
    );
  }
});

策略三:Stale While Revalidate

先返回缓存,后台更新:

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((cached) => {
      const fetchPromise = fetch(event.request).then((response) => {
        caches.open(CACHE_NAME).then((cache) => {
          cache.put(event.request, response.clone());
        });
        return response;
      });
      return cached || fetchPromise;
    })
  );
});
✅ 最佳实践:
  • 静态资源(JS/CSS/图片)→ 缓存优先
  • API 数据 → 网络优先 + 离线缓存
  • HTML 页面 → Stale While Revalidate

推送通知

PWA 支持后台推送,即使应用未打开也能收到通知:

// 监听推送事件
self.addEventListener('push', (event) => {
  const data = event.data?.json() || {};
  
  event.waitUntil(
    self.registration.showNotification(data.title || '新消息', {
      body: data.body || '您有一条新通知',
      icon: '/icon-192.png',
      badge: '/favicon-32x32.png',
      tag: 'notification-' + Date.now(),
      vibrate: [200, 100, 200],
      data: { url: data.url || '/' }
    })
  );
});

// 点击通知跳转
self.addEventListener('notificationclick', (event) => {
  event.notification.close();
  event.waitUntil(
    clients.openWindow(event.notification.data.url)
  );
});

部署与测试

1. 本地测试

Chrome DevTools 提供了完整的 PWA 调试工具:

  • 打开 DevTools → Application 面板
  • 查看 Manifest、Service Workers、Cache Storage
  • 使用 Lighthouse 跑 PWA 审计

2. 常见问题

⚠️ Service Worker 不更新?

修改 CACHE_NAME 版本号,或者:

  • DevTools → Application → Service Workers → Update
  • 勾选「Update on reload」开发模式
  • 清除浏览器缓存后刷新

3. Lighthouse 审计

运行 Lighthouse 审计确保 PWA 标准:

  • ✅ installable - 可安装
  • ✅ offline - 离线可用
  • ✅ splash-screen - 启动画面
  • ✅ themed-omnibox - 地址栏主题色

总结

PWA 让 Web 应用拥有了原生 App 的体验,核心是:

  1. manifest.json 定义应用元数据
  2. Service Worker 实现离线与缓存
  3. 缓存策略 根据资源类型选择
  4. 推送通知 提升用户粘性

🎉 在我的项目 no-fap-tracker-web 中,已完整实现 PWA 支持,欢迎参考源码!


参考资料: