Skip to content

深入理解 Service Worker

生命周期、缓存策略、离线应用、推送通知、Background Sync 与 Workbox 实战

什么是 Service Worker?

定义:Service Worker 是浏览器提供的一种可编程的网络代理,运行在独立于主线程的后台线程中。它拦截页面发出的所有网络请求,开发者可以通过编程决定如何响应——从缓存返回、从网络获取、或生成自定义响应。它是 PWA(渐进式 Web 应用)的核心技术。

涉及场景

  • 离线应用:缓存关键资源,在无网络时仍能正常使用(如 Google Docs 离线编辑)
  • 缓存策略:对不同类型的资源(静态文件、API、图片)应用不同的缓存策略
  • 推送通知:即使页面关闭,仍可接收服务端推送并显示系统通知
  • 后台同步:离线时提交的表单/消息在恢复网络后自动发送
  • 预加载 / 预缓存:提前缓存下一页的资源,加速页面跳转
  • CDN 回退:CDN 不可用时自动切换到备用源

作用

  1. 网络弹性:让 Web 应用在弱网和离线环境下依然可用
  2. 性能提升:缓存命中时响应速度接近原生应用(0ms 延迟)
  3. 用户留存:推送通知让 Web 应用具备原生 App 级别的用户触达能力
  4. 面试重点:生命周期(install → waiting → activate)、缓存策略选择、更新机制是常见考题
浏览器架构中的 Service Worker:
┌─────────────────────────────────────────────┐
│  主线程(Main Thread)                        │
│  ├── DOM、JS执行、事件处理                     │
│  └── navigator.serviceWorker                 │
├─────────────────────────────────────────────┤
│  Service Worker 线程(独立线程)               │
│  ├── 无法访问 DOM                             │
│  ├── 通过 postMessage 与主线程通信             │
│  ├── 拦截 fetch 请求                          │
│  ├── 管理 Cache Storage                       │
│  └── 接收 push / sync 事件                    │
├─────────────────────────────────────────────┤
│  网络(Network)                              │
└─────────────────────────────────────────────┘

前置条件

javascript
// 1. 必须使用 HTTPS(localhost 除外)
// 2. 浏览器支持检测
if ('serviceWorker' in navigator) {
  // 支持 Service Worker
}

生命周期

注册 → 安装(install)→ 等待(waiting)→ 激活(activate)→ 运行 → 销毁
                                ↑                                    │
                                └────────── 新版本等待中 ──────────────┘

详细流程:
1. 页面调用 register()
2. 浏览器下载 SW 脚本
3. 解析并执行 → 触发 install 事件
4. install 成功 → 进入 waiting 状态(如果有旧 SW 在运行)
5. 旧 SW 控制的所有页面关闭后 → 新 SW 激活
6. 触发 activate 事件
7. SW 开始控制页面,监听 fetch/push/sync 等事件
8. 空闲一段时间后 → SW 线程被终止(下次事件触发时重新启动)

注册

javascript
// main.js
async function registerSW() {
  try {
    const registration = await navigator.serviceWorker.register('/sw.js', {
      scope: '/',             // 控制范围(默认为 SW 文件所在目录)
      type: 'module',         // 支持 ES Modules(新)
      updateViaCache: 'none'  // 不使用 HTTP 缓存检查更新
    });

    console.log('SW 注册成功,scope:', registration.scope);

    // 监听更新
    registration.addEventListener('updatefound', () => {
      const newWorker = registration.installing;
      newWorker.addEventListener('statechange', () => {
        console.log('新 SW 状态:', newWorker.state);
        // installing → installed → activating → activated
      });
    });

    // 手动检查更新
    await registration.update();

  } catch (err) {
    console.error('SW 注册失败:', err);
  }
}

// registration 对象属性
// registration.installing  — 正在安装的 SW
// registration.waiting     — 等待激活的 SW
// registration.active      — 当前激活的 SW
// registration.scope       — 控制范围
// registration.updateViaCache — 更新策略

安装(install)

javascript
// sw.js
const CACHE_NAME = 'app-v1';
const PRECACHE_URLS = [
  '/',
  '/index.html',
  '/styles/main.css',
  '/scripts/app.js',
  '/images/logo.png',
  '/offline.html'
];

self.addEventListener('install', (event) => {
  console.log('SW 安装中...');

  // event.waitUntil() 确保异步操作完成后才进入下一阶段
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => {
        console.log('预缓存资源');
        return cache.addAll(PRECACHE_URLS);
      })
  );

  // 跳过等待,立即激活(谨慎使用)
  // self.skipWaiting();
});

激活(activate)

javascript
self.addEventListener('activate', (event) => {
  console.log('SW 激活中...');

  // 清理旧缓存
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames
          .filter(name => name !== CACHE_NAME)
          .map(name => {
            console.log('删除旧缓存:', name);
            return caches.delete(name);
          })
      );
    })
  );

  // 立即接管所有页面(不等待页面刷新)
  // self.clients.claim();
});

拦截请求(fetch)

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

缓存策略

1. Cache First(缓存优先)

javascript
// 适用:静态资源(JS/CSS/图片/字体)
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(CACHE_NAME).then(cache => cache.put(event.request, clone));
        return response;
      });
    })
  );
});

2. Network First(网络优先)

javascript
// 适用:API 请求、频繁更新的内容
self.addEventListener('fetch', (event) => {
  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(() => {
        // 网络失败 → 返回缓存
        return caches.match(event.request);
      })
  );
});

3. Stale While Revalidate(返回缓存 + 后台更新)

javascript
// 适用:用户头像、文章内容(允许短暂不一致)
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.open(CACHE_NAME).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;
      });
    })
  );
});

4. Cache Only

javascript
// 适用:纯离线应用
self.addEventListener('fetch', (event) => {
  event.respondWith(caches.match(event.request));
});

5. Network Only

javascript
// 适用:不需要缓存的请求(如分析统计)
self.addEventListener('fetch', (event) => {
  event.respondWith(fetch(event.request));
});

综合策略

javascript
self.addEventListener('fetch', (event) => {
  const { request } = event;
  const url = new URL(request.url);

  // API 请求 → Network First
  if (url.pathname.startsWith('/api/')) {
    event.respondWith(networkFirst(request));
    return;
  }

  // 静态资源 → Cache First
  if (request.destination === 'style' ||
      request.destination === 'script' ||
      request.destination === 'image') {
    event.respondWith(cacheFirst(request));
    return;
  }

  // HTML 页面 → Network First + 离线回退
  if (request.mode === 'navigate') {
    event.respondWith(
      fetch(request).catch(() => caches.match('/offline.html'))
    );
    return;
  }

  // 其他 → Stale While Revalidate
  event.respondWith(staleWhileRevalidate(request));
});

Cache API

javascript
// 打开缓存
const cache = await caches.open('my-cache-v1');

// 添加资源
await cache.add('/api/data');                    // fetch + put
await cache.addAll(['/a.js', '/b.css']);          // 批量添加

// 手动存储
await cache.put(request, response);              // 存储请求-响应对
await cache.put('/custom-key', new Response('data')); // 自定义键

// 查询
const response = await cache.match(request);     // 精确匹配
const response2 = await cache.match(request, {
  ignoreSearch: true,    // 忽略 URL 查询参数
  ignoreMethod: true,    // 忽略请求方法
  ignoreVary: true       // 忽略 Vary 头
});

const responses = await cache.matchAll(request); // 所有匹配

// 删除
await cache.delete(request);

// 列出所有缓存的请求
const keys = await cache.keys();

// 管理缓存
const names = await caches.keys();               // 所有缓存名
const exists = await caches.has('my-cache');      // 检查是否存在
await caches.delete('old-cache');                 // 删除整个缓存
const matched = await caches.match(request);     // 跨所有缓存查找

通信

javascript
// 主线程 → SW
navigator.serviceWorker.controller.postMessage({
  type: 'SKIP_WAITING'
});

// SW → 主线程
// sw.js
self.addEventListener('message', (event) => {
  if (event.data.type === 'SKIP_WAITING') {
    self.skipWaiting();
  }

  // 回复消息
  event.source.postMessage({ type: 'REPLY', data: 'OK' });
});

// SW → 所有页面
self.clients.matchAll().then(clients => {
  clients.forEach(client => {
    client.postMessage({ type: 'UPDATE_AVAILABLE' });
  });
});

// 主线程监听 SW 消息
navigator.serviceWorker.addEventListener('message', (event) => {
  console.log('来自 SW:', event.data);
});

// MessageChannel 双向通信
const channel = new MessageChannel();
channel.port1.onmessage = (event) => {
  console.log('SW 回复:', event.data);
};
navigator.serviceWorker.controller.postMessage(
  { type: 'INIT_PORT' },
  [channel.port2]
);

推送通知(Push API)

javascript
// 1. 请求通知权限
const permission = await Notification.requestPermission();
// 'granted' | 'denied' | 'default'

// 2. 订阅推送
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
  userVisibleOnly: true, // 必须显示通知
  applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY)
});
// 将 subscription 发送到服务器

// 3. SW 接收推送
// 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',
      badge: '/badge-72.png',
      image: data.image,
      vibrate: [100, 50, 100],
      data: { url: data.url },
      actions: [
        { action: 'open', title: '查看' },
        { action: 'dismiss', title: '忽略' }
      ],
      tag: data.tag,         // 相同 tag 的通知会替换
      renotify: true,        // 替换时重新提醒
      requireInteraction: false // 是否需要用户手动关闭
    })
  );
});

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

  if (event.action === 'open') {
    event.waitUntil(
      clients.openWindow(event.notification.data.url || '/')
    );
  }
});

Background Sync

javascript
// 主线程:注册同步任务
const registration = await navigator.serviceWorker.ready;
await registration.sync.register('sync-messages');

// sw.js:处理同步
self.addEventListener('sync', (event) => {
  if (event.tag === 'sync-messages') {
    event.waitUntil(syncMessages());
  }
});

async function syncMessages() {
  // 从 IndexedDB 读取待发送的消息
  const messages = await getQueuedMessages();
  for (const msg of messages) {
    try {
      await fetch('/api/messages', {
        method: 'POST',
        body: JSON.stringify(msg)
      });
      await removeFromQueue(msg.id);
    } catch (err) {
      // 失败时 SW 会自动重试
      throw err;
    }
  }
}

// Periodic Background Sync(定期同步,需要权限)
const registration = await navigator.serviceWorker.ready;
await registration.periodicSync.register('update-content', {
  minInterval: 24 * 60 * 60 * 1000 // 至少每24小时一次
});

// sw.js
self.addEventListener('periodicsync', (event) => {
  if (event.tag === 'update-content') {
    event.waitUntil(updateContent());
  }
});

更新策略

javascript
// 检测 SW 更新并提示用户
// main.js
let refreshing = false;

navigator.serviceWorker.addEventListener('controllerchange', () => {
  if (!refreshing) {
    refreshing = true;
    window.location.reload();
  }
});

async function checkForUpdate() {
  const registration = await navigator.serviceWorker.ready;
  registration.addEventListener('updatefound', () => {
    const newWorker = registration.installing;

    newWorker.addEventListener('statechange', () => {
      if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
        // 新版本已安装,提示用户更新
        if (confirm('新版本可用,是否更新?')) {
          newWorker.postMessage({ type: 'SKIP_WAITING' });
        }
      }
    });
  });
}

总结

Service Worker 核心知识点:
┌──────────────────────────────────────────────────────────┐
│ 生命周期                                                  │
│ • register → install → waiting → activate → fetch        │
│ • skipWaiting() 跳过等待,clients.claim() 立即接管         │
│ • install 中预缓存,activate 中清理旧缓存                  │
├──────────────────────────────────────────────────────────┤
│ 缓存策略                                                  │
│ • Cache First:静态资源                                   │
│ • Network First:API、动态内容                             │
│ • Stale While Revalidate:允许短暂不一致的内容              │
│ • Cache/Network Only:极端场景                             │
├──────────────────────────────────────────────────────────┤
│ 核心 API                                                  │
│ • Cache API:open / add / put / match / delete            │
│ • Fetch 拦截:event.respondWith()                         │
│ • 通信:postMessage / MessageChannel / clients            │
│ • Push API + Notification API:推送通知                    │
│ • Background Sync:离线操作队列                            │
├──────────────────────────────────────────────────────────┤
│ 注意事项                                                  │
│ • 必须 HTTPS(localhost 除外)                             │
│ • 不能访问 DOM,通过 postMessage 通信                      │
│ • 空闲时线程被终止,不要依赖全局变量持久化                    │
│ • scope 决定控制范围                                       │
└──────────────────────────────────────────────────────────┘