Service Worker 注册与生命周期:install、activate、fetch 事件、更新机制

4.1 原理概述

Service Worker 是浏览器在后台独立运行的一种脚本,能够拦截网络请求、管理缓存,从而实现离线缓存、消息推送、后台同步等功能。其生命周期是离线缓存设计的核心,主要包含 注册、安装(install)、激活(activate)、空闲(idle) 和 终止(terminate) 五个阶段。与应用业务相关的最关键事件是 install、activate 和 fetch。

核心角色:Service Worker 作为客户端与 REST API 之间的代理,通过监听事件决定是返回缓存还是发起网络请求,从而在无网络时提供离线体验。

4.2 生命周期与事件流

一个 Service Worker 从注册到接管页面,经历以下流程:

  1. 注册(Registration): 页面 JavaScript 调用 navigator.serviceWorker.register(),浏览器下载并解析脚本。
  2. 安装(install): 首次安装时触发 install 事件,通常在此预缓存静态资源(HTML、CSS、JS、API 响应模板等)。
  3. 等待激活(Waiting): 若已有旧版 SW 在控制页面,新版进入等待状态。
  4. 激活(activate): 新版本取代旧版本时触发,常用于清理旧缓存、迁移数据。
  5. 控制页面(Controlling): 激活后,SW 开始控制所有同源页面,监听 fetch 事件。
  6. 更新(Update): 浏览器定期或通过 updateViaCache 策略检查脚本变更,进入新一轮安装—激活流程。
Service Worker 生命周期与更新机制 Service Worker 生命周期与更新机制 注册 (register) install 事件 (预缓存) 等待 (Waiting) activate 事件 (清理旧缓存) 控制页面 (Controlling) fetch 事件 (拦截请求) 更新 (新版本) 更新机制: 1. 浏览器检测脚本变化 2. 新SW进入waiting状态 3. skipWaiting()强制激活

4.3 注册与生命周期事件实现

4.3.1 注册 Service Worker

在主线程(通常为页面 index.js)中注册 SW 脚本:

// 注册 Service Worker
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js')
    .then(registration => {
      console.log('SW registered at scope:', registration.scope);
    })
    .catch(err => {
      console.error('SW registration failed:', err);
    });
}
提示:注册时需确保 SW 脚本存放在站点根目录或同源路径下,且响应头 Content-Type: text/javascript。

4.3.2 install 事件——预缓存策略

install 事件发生在 SW 首次下载成功时;通常用 event.waitUntil() 延长安装生命周期,直到缓存操作完成。若 promise 失败,安装被视为失败,SW 不会激活。

// sw.js
const CACHE_NAME = 'rest-api-cache-v1';
const PRECACHE_URLS = [
  '/',
  '/index.html',
  '/api/templates',
  '/offline.html'
];

self.addEventListener('install', event => {
  console.log('SW installing…');
  event.waitUntil(
    caches.open(CACHE_NAME).then(cache => {
      return cache.addAll(PRECACHE_URLS);
    })
  );
});

4.3.3 activate 事件——清理旧缓存

activate 事件在 SW 激活时触发;通常用于删除不符合当前版本命名规范的旧缓存,避免磁盘膨胀和重复数据。

self.addEventListener('activate', event => {
  console.log('SW activating…');
  const cacheWhitelist = [CACHE_NAME];  // 只保留当前版本

  event.waitUntil(
    caches.keys().then(keyList => {
      return Promise.all(
        keyList.map(key => {
          if (!cacheWhitelist.includes(key)) {
            console.log('Deleting old cache:', key);
            return caches.delete(key);
          }
        })
      );
    }).then(() => self.clients.claim())  // 立即控制已打开的页面
  );
});
注意:调用 self.clients.claim() 可使新激活的 SW 立即接管所有页面,否则需要页面重新加载才会受控。

4.3.4 fetch 事件——离线响应与网络优先策略

每次请求都会触发 fetch 事件;SW 决定是否使用缓存、回退网络或组合策略。以下实现 网络优先 策略,适用于 REST API 缓存:

self.addEventListener('fetch', event => {
  const request = event.request;
  // 仅处理 GET 请求(API 只缓存 GET)
  if (request.method !== 'GET') return;

  event.respondWith(
    fetch(request)
      .then(networkResponse => {
        // 将最新响应存入缓存(排除非成功状态)
        const clonedResponse = networkResponse.clone();
        caches.open(CACHE_NAME).then(cache => {
          cache.put(request, clonedResponse);
        });
        return networkResponse;
      })
      .catch(() => {
        // 网络失败,尝试缓存
        return caches.match(request).then(cached => {
          if (cached) return cached;
          // 回退离线页面
          return caches.match('/offline.html');
        });
      })
  );
});

4.4 更新机制

浏览器每次导航/每 24 小时检查 SW 字节变化。一旦发现新版本,生命周期再次从 install 开始。默认新版本会进入 waiting 状态,直到所有被控制的页面关闭。可通过 self.skipWaiting() 在安装完成后立即激活:

self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME).then(cache => cache.addAll(PRECACHE_URLS))
      .then(() => self.skipWaiting())
  );
});

同时在页面监听 controllerchange 事件,提示用户刷新:

navigator.serviceWorker.addEventListener('controllerchange', () => {
  console.log('新版 SW 已激活,建议刷新页面');
  // 可在此弹出提示或自动刷新
  window.location.reload();
});

4.5 调试方法

工具/方式操作步骤
Chrome DevTools → Application打开 "Service Workers" 面板,可查看注册状态、点击 "Unregister"、"Update"、"Push" 测试。
Cache Storage 面板查看所有缓存名称和内容,验证缓存是否按预期生成。
Network 面板查看请求是否由 Service Worker 响应(响应头显示 from ServiceWorker)。
Console 日志SW 内部的 console.log 会输出到专用控制台(需在 DevTools 中开启 "Show console for service workers")。
Offline 模拟在 Network 面板切换 "Offline" 模式,验证离线缓存是否生效。

4.6 部署注意事项

  • 脚本不可跨域:Service Worker 文件必须与页面同源(或通过 Access-Control-Allow-Origin 允许),scope 默认是脚本路径的父目录。
  • HTTPS 强制:生产环境必须使用 HTTPS(localhost 除外)。
  • 缓存版本管理:每次更新时修改 CACHE_NAME(如 rest-api-cache-v2),激活事件中清理旧版本。
  • 注册时机:不要在页面生命周期前注册 SW,建议在 DOMContentLoaded 或 load 事件中进行。

4.7 常见错误与排查

错误现象可能原因解决方案
SW 安装失败,控制台报 404sw.js 路径错误或未部署检查注册路径,确认文件存在。
缓存未生效,请求仍走网络fetch 事件未正确响应,或策略未匹配请求检查 event.respondWith 是否执行,打印请求 URL。
旧缓存未清理,磁盘占用大activate 事件中未实现缓存清理逻辑在 activate 中遍历缓存名,删除不在白名单的缓存。
新版 SW 不激活,一直等待未调用 self.skipWaiting(),且页面未关闭在 install 末尾调用 self.skipWaiting()。
跨域 API 请求不被缓存fetch 事件没有处理 opaque 请求对跨域请求使用 no-cors 模式,并谨慎处理缓存策略。

4.8 性能与稳定性优化

  • 细粒度缓存版本控制:将静态资源与 API 响应分入不同缓存(如 static-v1、api-v1),避免全量清理。
  • 使用 stale-while-revalidate 策略:先返回缓存,后台发起请求更新缓存,减少等待时间。
  • 限制缓存容量:定期检查缓存大小并删除最早项,避免超出浏览器配额(尤其是移动端)。
  • 减少 install 预缓存数量:只缓存关键页面和核心 API 模板,其他资源按需缓存。
  • 配合 updateViaCache 配置:注册时可设置 updateViaCache: 'none' 避免浏览器缓存 SW 脚本。
// 示例:stale-while-revalidate 策略
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request).then(cached => {
      const fetchPromise = fetch(event.request).then(network => {
        // 更新缓存
        caches.open(CACHE_NAME).then(cache => cache.put(event.request, network.clone()));
        return network;
      }).catch(() => cached);  // 网络失败返回缓存
      return cached || fetchPromise;
    })
  );
});
推荐实践:在开发阶段,使用 DevTools 的 "Update on reload" 选项(Application → Service Workers)每次刷新都重新注册 SW,加速调试。
同时保留 skipWaiting() 和 clients.claim() 便于迭代测试。