Service Worker API

深入理解 Service Worker 的生命周期、Cache Storage API、离线优先策略、Background Sync、Push Notification,以及作用域和限制。

Service Worker API

一、为什么需要 Service Worker

1.1 传统 Web 的局限性

传统 Web 应用无法在后台运行,无法在用户没有网络连接时提供体验,无法像原生应用一样推送通知。Service Worker 填补了这个空白。

1.2 Service Worker 的能力

Service Worker 是一种运行在浏览器后台的脚本,独立于网页:

  • 拦截网络请求:可以决定如何响应请求
  • 缓存管理:完全控制缓存策略
  • 后台同步:在网络恢复后执行任务
  • 推送通知:接收和显示推送通知
  • 离线体验:提供离线或弱网下的内容

二、生命周期

2.1 注册和安装

// 在主线程注册
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js')
    .then(registration => {
      console.log('Service Worker 注册成功:', registration.scope);
    })
    .catch(error => {
      console.error('Service Worker 注册失败:', error);
    });
}
// sw.js - Service Worker 文件
const CACHE_NAME = 'v1';
const urlsToCache = [
  '/',
  '/styles/main.css',
  '/scripts/main.js',
  '/images/logo.png'
];

// 安装事件
self.addEventListener('install', event => {
  console.log('Service Worker 安装中...');

  // 等待缓存完成
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => {
        console.log('缓存已打开');
        return cache.addAll(urlsToCache);
      })
      .then(() => {
        // 跳过等待,立即激活
        return self.skipWaiting();
      })
  );
});

2.2 激活

// 激活事件
self.addEventListener('activate', event => {
  console.log('Service Worker 激活中...');

  // 清理旧缓存
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.map(cacheName => {
          // 删除不用的缓存
          if (cacheName !== CACHE_NAME) {
            console.log('删除旧缓存:', cacheName);
            return caches.delete(cacheName);
          }
        })
      );
    }).then(() => {
      // 接管所有页面
      return self.clients.claim();
    })
  );
});

2.3 完整的生命周期图

注册 → 安装中 → 安装完成
            ↓
        激活中 → 激活完成
            ↓
        空闲 →(fetch 事件)→ 处理中 → 空闲

三、fetch 事件处理

3.1 基础缓存策略

// 缓存优先
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
      .then(response => {
        // 缓存命中,直接返回
        if (response) {
          return response;
        }

        // 缓存未命中,发起网络请求
        return fetch(event.request)
          .then(networkResponse => {
            // 检查是否有效
            if (!networkResponse || networkResponse.status !== 200) {
              return networkResponse;
            }

            // 缓存新资源
            const responseToCache = networkResponse.clone();
            caches.open(CACHE_NAME)
              .then(cache => {
                cache.put(event.request, responseToCache);
              });

            return networkResponse;
          });
      })
  );
});

3.2 离线优先策略

// 离线优先:先缓存,没有缓存才网络请求
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
      .then(response => {
        return response || fetch(event.request);
      })
      .catch(() => {
        // 网络和缓存都没有,返回离线页面
        return caches.match('/offline.html');
      })
  );
});

3.3 网络优先策略

// 网络优先:先请求,失败时用缓存
self.addEventListener('fetch', event => {
  event.respondWith(
    fetch(event.request)
      .then(response => {
        // 有效响应,缓存后返回
        if (response && response.status === 200) {
          const responseToCache = response.clone();
          caches.open(CACHE_NAME)
            .then(cache => {
              cache.put(event.request, responseToCache);
            });
        }
        return response;
      })
      .catch(() => {
        // 网络失败,使用缓存
        return caches.match(event.request);
      })
  );
});

四、Cache Storage API

4.1 基本操作

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

// 添加资源
await cache.add('/styles.css');
await cache.addAll(['/page1.html', '/page2.html']);

// 检查是否包含
const has = await cache.has('/styles.css');

// 获取资源
const response = await cache.match('/styles.css');

// 删除缓存
await cache.delete('/styles.css');

// 获取所有缓存键
const keys = await cache.keys();

4.2 缓存版本管理

const CACHE_VERSION = 'v2';
const CACHE_NAME = `app-cache-${CACHE_VERSION}`;

// 安装时使用新版缓存
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME).then(cache => {
      return cache.addAll([
        '/',
        '/index.html',
        '/styles/main.css'
      ]);
    })
  );
});

// 激活时清理旧缓存
self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames
          .filter(name => name.startsWith('app-cache-') && name !== CACHE_NAME)
          .map(name => caches.delete(name))
      );
    })
  );
});

五、后台同步

5.1 Background Sync API

// 主线程:请求后台同步
async function sendData(data) {
  // 立即显示给用户
  showToUser(data);

  // 尝试发送,如果失败则排队
  try {
    await fetch('/api/submit', {
      method: 'POST',
      body: JSON.stringify(data)
    });
  } catch (error) {
    // 注册后台同步
    const registration = await navigator.serviceWorker.ready;
    await registration.sync.register('sync-submit');
  }
}
// Service Worker:处理后台同步
self.addEventListener('sync', event => {
  if (event.tag === 'sync-submit') {
    event.waitUntil(syncData());
  }
});

async function syncData() {
  // 从 IndexedDB 获取待发送的数据
  const data = await getDataFromIndexedDB();

  if (data) {
    await fetch('/api/submit', {
      method: 'POST',
      body: JSON.stringify(data)
    });
    await deleteDataFromIndexedDB(data.id);
  }
}

六、推送通知

6.1 Web Push 流程

  1. 用户授权接收通知
  2. 订阅用户到推送服务
  3. 推送服务发送推送
  4. Service Worker 接收并显示

6.2 订阅推送

async function subscribeToPush() {
  const registration = await navigator.serviceWorker.ready;

  const subscription = await registration.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: 'YOUR_PUBLIC_KEY'
  });

  // 发送到服务器
  await fetch('/api/push/subscribe', {
    method: 'POST',
    body: JSON.stringify(subscription)
  });
}

6.3 处理推送

self.addEventListener('push', event => {
  if (!event.data) return;

  const data = event.data.json();

  const options = {
    body: data.body,
    icon: '/images/icon.png',
    badge: '/images/badge.png',
    vibrate: [100, 50, 100],
    data: {
      url: data.url
    }
  };

  event.waitUntil(
    self.registration.showNotification(data.title, options)
  );
});

self.addEventListener('notificationclick', event => {
  event.notification.close();

  event.waitUntil(
    clients.openWindow(event.notification.data.url)
  );
});

七、作用域和限制

7.1 作用域

// 注册时指定作用域
navigator.serviceWorker.register('/sw.js', {
  scope: '/app/'
});

// Service Worker 只能拦截在自己的作用域内的请求
// scope: '/app/' 只能拦截 '/app/' 下的请求

7.2 限制

  • 只能拦截通过 HTTP/HTTPS 发送的请求(localhost 除外)
  • 不能访问 localStorage(需要使用 Cache API 和 IndexedDB)
  • 异步操作需要使用 Promise
  • 不能访问 window 对象
  • 不能直接操作 DOM

延展阅读