Skip to content

Service Worker

Service Worker 是一种运行在浏览器后台的 Worker,可以作为网络代理拦截和处理页面请求,实现离线缓存、后台同步和推送通知等功能。它是 Progressive Web App (PWA) 的核心技术。本节介绍 Service Worker 的生命周期、Cache API 以及基本的离线缓存实现。

前置知识

阅读本节前,建议先了解:SharedWorker

基础概念

Service Worker 的定位

Service Worker 本质上是一个网络代理,位于 Web 应用和服务器之间,可以拦截和控制所有的网络请求。与普通 Worker 不同,Service Worker 具有以下特点:

特性说明
网络代理拦截页面发出的所有请求
离线支持可以缓存资源,实现离线访问
生命周期独立于页面,可在后台运行
安全要求必须在 HTTPS 下使用
作用域控制在其注册路径下的所有页面

Service Worker 生命周期

注册 Service Worker

  ├── 1. 安装(install)
  │     └── 预缓存静态资源

  ├── 2. 激活(activate)
  │     └── 清理旧缓存

  ├── 3. 控制(fetch)
  │     └── 拦截网络请求

  └── 4. 更新(update)
        └── 重新安装和激活

语法与注册

注册 Service Worker

javascript
// 主线程 - 注册 Service Worker
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js', {
    scope: '/' // 控制范围(默认为 sw.js 所在目录)
  })
  .then(registration => {
    console.log('Service Worker 注册成功:', registration.scope);
  })
  .catch(error => {
    console.error('注册失败:', error);
  });

  // 监听更新
  navigator.serviceWorker.addEventListener('updatefound', () => {
    const newWorker = registration.installing;
    console.log('发现新版本的 Service Worker');
  });
}

Service Worker 脚本 (sw.js)

javascript
// sw.js - Service Worker 脚本

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

  // 预缓存核心资源
  event.waitUntil(
    caches.open('app-cache-v1')
      .then(cache => {
        return cache.addAll([
          '/',
          '/index.html',
          '/styles.css',
          '/app.js',
          '/logo.png'
        ]);
      })
      .then(() => self.skipWaiting()) // 立即激活
  );
});

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

  // 清理旧版本缓存
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames
          .filter(name => name !== 'app-cache-v1')
          .map(name => caches.delete(name))
      );
    }).then(() => self.clients.claim()) // 立即控制所有页面
  );
});

// ========== 3. 请求拦截 ==========
self.addEventListener('fetch', (event) => {
  event.respondWith(
    // 优先从缓存读取,缓存未命中则从网络获取
    caches.match(event.request)
      .then(cachedResponse => {
        if (cachedResponse) {
          return cachedResponse;
        }
        return fetch(event.request).then(response => {
          // 缓存新获取的资源
          if (response.status === 200) {
            const responseClone = response.clone();
            caches.open('app-cache-v1').then(cache => {
              cache.put(event.request, responseClone);
            });
          }
          return response;
        });
      })
  );
});

详细说明

生命周期详解

install 事件

javascript
self.addEventListener('install', (event) => {
  // event.waitUntil() 确保 promise 完成后再完成安装
  event.waitUntil(
    caches.open('my-cache-v1').then(cache => {
      return cache.addAll([
        '/index.html',
        '/styles.css',
        '/app.js'
      ]);
    })
  );

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

activate 事件

javascript
self.addEventListener('activate', (event) => {
  event.waitUntil(
    // 获取所有缓存名称
    caches.keys().then(keys => {
      // 删除非当前版本的缓存
      return Promise.all(
        keys
          .filter(key => key !== 'my-cache-v1')
          .map(key => caches.delete(key))
      );
    })
  );

  // self.clients.claim() 立即控制所有页面
  // self.clients.claim();
});

fetch 事件

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

  // 只处理同源请求
  if (url.origin === location.origin) {
    event.respondWith(
      caches.match(event.request).then(cached => {
        return cached || fetch(event.request);
      })
    );
  }
});

Cache API 详解

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

// ========== 添加资源 ==========
// 添加单个
await cache.add('/index.html');

// 添加多个
await cache.addAll(['/styles.css', '/app.js', '/logo.png']);

// 手动添加(需要 Response 对象)
await cache.put('/api/data', new Response(JSON.stringify({ id: 1 })));

// ========== 查询资源 ==========
// 精确匹配
const response = await cache.match('/index.html');

// 模糊匹配
const responses = await cache.matchAll(request => {
  return request.url.includes('.css');
});

// ========== 删除资源 ==========
await cache.delete('/old-page.html');

// ========== 获取所有缓存 ==========
const keys = await caches.keys();
// ['my-cache-v1', 'my-cache-v2']

// ========== 删除缓存 ==========
await caches.delete('my-cache-v1');

缓存策略

策略说明适用场景
Cache First优先缓存,未命中再网络静态资源
Network First优先网络,失败再缓存API 请求
Stale While Revalidate返回缓存同时更新频繁更新的资源
Cache Only仅使用缓存离线页面
javascript
// 策略一:Cache First(缓存优先)
function cacheFirst(event) {
  event.respondWith(
    caches.match(event.request).then(cached => {
      return cached || fetch(event.request);
    })
  );
}

// 策略二:Network First(网络优先)
function networkFirst(event) {
  event.respondWith(
    fetch(event.request)
      .then(response => {
        const clone = response.clone();
        caches.open('dynamic-cache').then(cache => cache.put(event.request, clone));
        return response;
      })
      .catch(() => caches.match(event.request))
  );
}

// 策略三:Stale While Revalidate
function staleWhileRevalidate(event) {
  event.respondWith(
    caches.match(event.request).then(cached => {
      const fetchPromise = fetch(event.request).then(response => {
        caches.open('dynamic-cache').then(cache => cache.put(event.request, response.clone()));
        return response;
      });
      return cached || fetchPromise;
    })
  );
}

// 在 fetch 事件中使用
self.addEventListener('fetch', (event) => {
  const url = new URL(event.request.url);

  if (url.pathname.match(/\.(js|css|png|jpg|svg)$/)) {
    cacheFirst(event);  // 静态资源用缓存优先
  } else if (url.pathname.startsWith('/api/')) {
    networkFirst(event); // API 用网络优先
  }
});

实战示例

基础离线缓存 Service Worker

javascript
// sw.js - 完整的 Service Worker 示例
const CACHE_NAME = 'my-app-v1';
const STATIC_ASSETS = [
  '/',
  '/index.html',
  '/styles.css',
  '/app.js',
  '/favicon.ico'
];

// 安装:预缓存静态资源
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => cache.addAll(STATIC_ASSETS))
      .then(() => self.skipWaiting())
  );
});

// 激活:清理旧缓存
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then(keys => {
      return Promise.all(
        keys
          .filter(key => key !== CACHE_NAME)
          .map(key => caches.delete(key))
      );
    }).then(() => self.clients.claim())
  );
});

// 拦截请求:缓存优先 + 网络回退
self.addEventListener('fetch', (event) => {
  // 跳过非 GET 请求
  if (event.request.method !== 'GET') return;

  // 跳过跨域请求
  if (!event.request.url.startsWith(self.location.origin)) return;

  event.respondWith(
    caches.match(event.request).then(cached => {
      if (cached) {
        // 返回缓存,后台更新
        fetch(event.request).then(response => {
          if (response.ok) {
            caches.open(CACHE_NAME).then(cache => {
              cache.put(event.request, response);
            });
          }
        }).catch(() => {}); // 忽略网络错误
        return cached;
      }

      // 无缓存,从网络获取
      return fetch(event.request).then(response => {
        if (!response.ok) {
          throw new Error(`HTTP ${response.status}`);
        }
        const clone = response.clone();
        caches.open(CACHE_NAME).then(cache => {
          cache.put(event.request, clone);
        });
        return response;
      });
    }).catch(() => {
      // 网络和缓存都失败,返回离线页面
      if (event.request.mode === 'navigate') {
        return caches.match('/index.html');
      }
      return new Response('离线不可用', { status: 503 });
    })
  );
});
javascript
// 主页面注册代码
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/sw.js')
      .then(reg => {
        console.log('SW 注册成功');

        // 检测更新
        reg.addEventListener('updatefound', () => {
          const newWorker = reg.installing;
          newWorker.addEventListener('statechange', () => {
            if (newWorker.state === 'activated') {
              console.log('新版本已激活,刷新页面');
              // 可提示用户刷新
            }
          });
        });
      })
      .catch(err => console.error('SW 注册失败:', err));
  });
}

注意事项

  1. 必须使用 HTTPS:Service Worker 只能在安全上下文中注册(localhost 除外)
  2. 作用域限制:Service Worker 只能控制其注册路径下的页面
  3. 首次加载无缓存:第一次访问页面时 Service Worker 还未安装
  4. 需要用户交互才能更新:新版本需要所有标签页关闭后才能激活
  5. 调试工具:Chrome DevTools > Application > Service Workers

最佳实践

  1. 使用版本号管理缓存(cache-v1cache-v2
  2. install 中预缓存核心资源,fetch 中动态缓存其他资源
  3. activate 中清理旧版本缓存
  4. 为不同类型资源使用不同缓存策略
  5. 提供离线降级页面,提升用户体验

下一节

继续学习:WebSocket 基础

参考链接