Service Worker 与 PWA

Service Worker 的工作原理、生命周期及 PWA 应用场景

问题

什么是 Service Worker?它有哪些应用场景?如何用它构建 PWA?

解答

什么是 Service Worker

Service Worker 是运行在浏览器后台的脚本,独立于网页,充当浏览器与网络之间的代理。它可以拦截网络请求、缓存资源、实现离线访问。

特点:

  • 运行在独立线程,不阻塞主线程
  • 无法直接访问 DOM
  • 只能在 HTTPS 环境下运行(localhost 除外)
  • 完全异步,基于 Promise

生命周期

// sw.js - Service Worker 文件

// 1. 安装阶段:缓存静态资源
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open('v1').then((cache) => {
      return cache.addAll([
        '/',
        '/index.html',
        '/styles.css',
        '/app.js',
        '/offline.html'
      ]);
    })
  );
  // 跳过等待,立即激活
  self.skipWaiting();
});

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

// 3. 拦截请求
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((response) => {
      // 缓存命中,返回缓存
      if (response) {
        return response;
      }
      // 缓存未命中,发起网络请求
      return fetch(event.request);
    })
  );
});

注册 Service Worker

// main.js - 主页面注册

if ('serviceWorker' in navigator) {
  window.addEventListener('load', async () => {
    try {
      const registration = await navigator.serviceWorker.register('/sw.js');
      console.log('SW 注册成功,作用域:', registration.scope);
    } catch (error) {
      console.log('SW 注册失败:', error);
    }
  });
}

缓存策略

// 策略1:缓存优先(适合静态资源)
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((cached) => {
      return cached || fetch(event.request);
    })
  );
});

// 策略2:网络优先(适合 API 请求)
self.addEventListener('fetch', (event) => {
  event.respondWith(
    fetch(event.request)
      .then((response) => {
        // 请求成功,更新缓存
        const clone = response.clone();
        caches.open('v1').then((cache) => {
          cache.put(event.request, clone);
        });
        return response;
      })
      .catch(() => {
        // 网络失败,返回缓存
        return caches.match(event.request);
      })
  );
});

// 策略3:Stale While Revalidate(先返回缓存,后台更新)
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.open('v1').then((cache) => {
      return cache.match(event.request).then((cached) => {
        const fetchPromise = fetch(event.request).then((response) => {
          cache.put(event.request, response.clone());
          return response;
        });
        return cached || fetchPromise;
      });
    })
  );
});

PWA 完整示例

// manifest.json - PWA 配置文件
{
  "name": "My PWA App",
  "short_name": "PWA",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#007bff",
  "icons": [
    {
      "src": "/icon-192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/icon-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <!-- PWA 必需的 meta 标签 -->
  <meta name="theme-color" content="#007bff">
  <link rel="manifest" href="/manifest.json">
  <link rel="apple-touch-icon" href="/icon-192.png">
  <title>PWA Demo</title>
</head>
<body>
  <h1>PWA 示例</h1>
  <script src="/main.js"></script>
</body>
</html>

推送通知

// 请求通知权限并订阅
async function subscribePush() {
  const permission = await Notification.requestPermission();
  if (permission !== 'granted') return;

  const registration = await navigator.serviceWorker.ready;
  const subscription = await registration.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: 'YOUR_VAPID_PUBLIC_KEY'
  });
  
  // 发送订阅信息到服务器
  await fetch('/api/subscribe', {
    method: 'POST',
    body: JSON.stringify(subscription),
    headers: { 'Content-Type': 'application/json' }
  });
}

// sw.js - 接收推送
self.addEventListener('push', (event) => {
  const data = event.data.json();
  event.waitUntil(
    self.registration.showNotification(data.title, {
      body: data.body,
      icon: '/icon-192.png'
    })
  );
});

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

后台同步

// 主页面:注册同步任务
async function syncData() {
  const registration = await navigator.serviceWorker.ready;
  await registration.sync.register('sync-data');
}

// sw.js - 处理同步
self.addEventListener('sync', (event) => {
  if (event.tag === 'sync-data') {
    event.waitUntil(
      // 从 IndexedDB 读取待同步数据,发送到服务器
      syncPendingData()
    );
  }
});

关键点

  • 独立线程:Service Worker 运行在独立线程,不能访问 DOM,通过 postMessage 与页面通信
  • 生命周期:install → waiting → activate → fetch,更新时新 SW 会等待旧 SW 释放控制权
  • HTTPS 必需:出于安全考虑,只能在 HTTPS 或 localhost 下运行
  • 缓存策略:根据资源类型选择缓存优先、网络优先或 Stale While Revalidate
  • PWA 三要素:HTTPS + manifest.json + Service Worker,满足后可安装到桌面