7.4 Service Worker 简介

7.4 Service Worker 简介

Service Worker 是现代Web平台的核心技术之一,它充当了Web应用程序与网络之间的代理层,赋予开发者对缓存策略和离线体验的精细控制能力。作为PWA(渐进式Web应用)的基石,Service Worker彻底改变了Web应用的离线能力和性能表现。

核心特性与工作原理

1. 基本特征

  • 独立线程运行:与主JavaScript线程分离,不阻塞UI
  • 可编程网络代理:拦截和处理网络请求
  • 完全异步设计:基于Promise实现
  • 生命周期独立:与关联页面无关,可自主运行
  • HTTPS要求:生产环境必须使用安全连接(localhost除外)

2. 生命周期阶段

stateDiagram-v2
    [*] --> 解析(Installing): 首次注册
    解析(Installing) --> 安装完成(Installed): install事件完成
    安装完成(Installed) --> 激活中(Activating): 等待旧SW失效
    激活中(Activating) --> 激活完成(Activated): activate事件完成
    激活完成(Activated) --> 闲置(Idle): 无任务
    闲置(Idle) --> 终止(Terminated): 节省内存
    终止(Terminated) --> 闲置(Idle): 新消息到达
    激活完成(Activated) --> [*]: 被新版本替换

基础实现步骤

1. 注册Service Worker

// 主线程代码(通常是main.js)
if ('serviceWorker' in navigator) {
  window.addEventListener('load', function() {
    navigator.serviceWorker.register('/sw.js')
      .then(registration => {
        console.log('注册成功,作用域:', registration.scope);
        
        // 检测更新
        registration.addEventListener('updatefound', () => {
          const newWorker = registration.installing;
          console.log('发现新版本:', newWorker.state);
        });
      })
      .catch(err => {
        console.error('注册失败:', err);
      });
  });
}

2. 基本Service Worker文件(sw.js)

// 定义缓存名称和预缓存资源
const CACHE_NAME = 'my-site-cache-v1';
const PRECACHE_URLS = [
  '/',
  '/styles/main.css',
  '/scripts/main.js',
  '/images/logo.png'
];

// 安装阶段 - 预缓存关键资源
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => {
        console.log('已打开缓存');
        return cache.addAll(PRECACHE_URLS);
      })
      .then(() => self.skipWaiting()) // 跳过等待直接激活
  );
});

// 激活阶段 - 清理旧缓存
self.addEventListener('activate', event => {
  const cacheWhitelist = [CACHE_NAME];
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.map(cacheName => {
          if (!cacheWhitelist.includes(cacheName)) {
            console.log('删除旧缓存:', cacheName);
            return caches.delete(cacheName);
          }
        })
      );
    })
    .then(() => self.clients.claim()) // 立即控制所有客户端
  );
});

// 请求拦截 - 基本缓存优先策略
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
      .then(response => response || fetch(event.request))
  );
});

高级缓存策略

1. 动态缓存策略组合

self.addEventListener('fetch', event => {
  const url = new URL(event.request.url);
  
  // API请求使用网络优先
  if (url.pathname.startsWith('/api/')) {
    event.respondWith(
      fetch(event.request)
        .then(response => {
          // 成功获取则缓存响应
          const clone = response.clone();
          caches.open('api-cache-v1').then(cache => {
            cache.put(event.request, clone);
          });
          return response;
        })
        .catch(() => {
          // 网络失败返回缓存
          return caches.match(event.request);
        })
    );
  }
  // 静态资源使用缓存优先
  else if (PRECACHE_URLS.includes(url.pathname)) {
    event.respondWith(
      caches.match(event.request)
        .then(response => response || fetch(event.request))
    );
  }
  // 其他请求尝试网络
  else {
    event.respondWith(fetch(event.request));
  }
});

2. 缓存过期与更新

// 定期清理过期缓存
self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.map(cacheName => {
          // 检查缓存版本
          if (!cacheName.startsWith('my-app-')) return;
          
          return caches.open(cacheName).then(cache => {
            return cache.keys().then(requests => {
              return Promise.all(
                requests.map(request => {
                  // 超过7天的缓存删除
                  return cache.match(request).then(response => {
                    if (!response) return;
                    
                    const cachedDate = new Date(
                      response.headers.get('date')
                    );
                    const ageInDays = (Date.now() - cachedDate) / (1000 * 60 * 60 * 24);
                    
                    if (ageInDays > 7) {
                      return cache.delete(request);
                    }
                  });
                })
              );
            });
          });
        })
      );
    })
  );
});

消息通信机制

1. 主线程与Service Worker通信

主线程发送消息

// 页面发送消息
navigator.serviceWorker.controller.postMessage({
  type: 'SYNC_DATA',
  payload: { /* 数据 */ }
});

// 接收Service Worker消息
navigator.serviceWorker.addEventListener('message', event => {
  if (event.data.type === 'UPDATE_AVAILABLE') {
    showUpdateNotification();
  }
});

Service Worker处理消息

self.addEventListener('message', event => {
  if (event.data.type === 'SYNC_DATA') {
    // 处理数据同步
    event.waitUntil(
      handleSyncData(event.data.payload)
        .then(() => {
          // 回复消息
          event.ports[0].postMessage({ status: 'SYNC_COMPLETE' });
        })
    );
  }
});

// 主动向客户端发送消息
self.clients.matchAll().then(clients => {
  clients.forEach(client => {
    client.postMessage({
      type: 'CACHE_UPDATED',
      payload: { version: '1.2.0' }
    });
  });
});

2. 后台同步(Background Sync)

// 主线程注册同步任务
navigator.serviceWorker.ready.then(registration => {
  registration.sync.register('sync-comments')
    .then(() => console.log('后台同步已注册'))
    .catch(err => console.error('同步注册失败', err));
});

// Service Worker处理同步事件
self.addEventListener('sync', event => {
  if (event.tag === 'sync-comments') {
    event.waitUntil(
      sendPendingComments()
        .then(() => console.log('评论同步成功'))
        .catch(() => console.log('评论同步失败'))
    );
  }
});

调试与最佳实践

1. Chrome开发者工具指南

  1. Application面板

    • 查看注册的Service Worker
    • 手动触发更新/卸载
    • 模拟离线状态
  2. 离线测试技巧

    // 强制进入离线状态
    self.addEventListener('fetch', event => {
      if (event.request.url.includes('test-offline')) {
        return new Response('离线模拟响应', {
          headers: { 'Content-Type': 'text/html' }
        });
      }
    });
    
  3. 日志记录建议

    const DEBUG = true;
    
    function log(...args) {
      if (DEBUG) {
        console.log('[SW]', ...args);
      }
    }
    
    self.addEventListener('install', event => {
      log('安装事件触发');
      // ...
    });
    

2. 性能优化策略

  1. 缓存策略选择

    策略 适用场景 实现方式
    缓存优先 静态资源 `caches.match()
    网络优先 频繁更新的内容 fetch().catch(caches.match)
    仅缓存 关键离线资源 caches.match()
    仅网络 实时数据 fetch()
  2. 资源版本控制

    const ASSET_VERSION = 'v1.2';
    const CACHE_NAME = `app-cache-${ASSET_VERSION}`;
    
    function getCacheKey(url) {
      const parsed = new URL(url);
      return `${parsed.pathname}?v=${ASSET_VERSION}`;
    }
    
  3. 预加载关键资源

    self.addEventListener('install', event => {
      event.waitUntil(
        caches.open(CACHE_NAME)
          .then(cache => {
            const criticalResources = [
              '/',
              '/app-shell',
              '/styles/main.css'
            ];
            return cache.addAll(criticalResources);
          })
      );
    });
    

实际应用案例

1. 离线新闻阅读应用

// sw.js
const CACHE_NAME = 'news-app-v2';
const DYNAMIC_CACHE = 'news-dynamic-v1';
const API_CACHE = 'news-api-v1';

self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => cache.addAll([
        '/',
        '/index.html',
        '/styles/app.css',
        '/scripts/app.js',
        '/images/offline.png'
      ]))
  );
});

self.addEventListener('fetch', event => {
  const { request } = event;
  const url = new URL(request.url);
  
  // API请求缓存策略
  if (url.pathname.startsWith('/api/news')) {
    event.respondWith(
      caches.open(API_CACHE).then(cache => {
        return fetch(request).then(networkResponse => {
          cache.put(request, networkResponse.clone());
          return networkResponse;
        }).catch(() => {
          return cache.match(request)
            .then(response => response || caches.match('/offline.json'));
        });
      })
    );
  }
  // 静态资源策略
  else {
    event.respondWith(
      caches.match(request).then(response => {
        return response || fetch(request).then(networkResponse => {
          return caches.open(DYNAMIC_CACHE).then(cache => {
            if (request.method === 'GET') {
              cache.put(request, networkResponse.clone());
            }
            return networkResponse;
          });
        }).catch(() => {
          if (request.headers.get('accept').includes('text/html')) {
            return caches.match('/offline.html');
          }
        });
      })
    );
  }
});

2. 自动更新提示

// 主线程检测更新
navigator.serviceWorker.register('/sw.js').then(reg => {
  reg.addEventListener('updatefound', () => {
    const newWorker = reg.installing;
    
    newWorker.addEventListener('statechange', () => {
      if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
        showUpdateToast('新版本可用,点击刷新');
      }
    });
  });
});

// 用户点击刷新
document.getElementById('refresh-btn').addEventListener('click', () => {
  navigator.serviceWorker.controller.postMessage({ type: 'SKIP_WAITING' });
});
// Service Worker接收跳过等待命令
self.addEventListener('message', event => {
  if (event.data.type === 'SKIP_WAITING') {
    self.skipWaiting();
  }
});

self.addEventListener('controllerchange', () => {
  window.location.reload();
});

安全注意事项

  1. HTTPS强制要求

    • 生产环境必须部署HTTPS
    • 本地开发可使用localhost
  2. 内容安全策略(CSP)

    Content-Security-Policy: 
      default-src 'self';
      script-src 'self' 'unsafe-eval';
      style-src 'self' 'unsafe-inline';
      img-src 'self' data:;
      connect-src 'self' api.example.com;
    
  3. 敏感数据保护

    • 不要缓存隐私相关资源
    • 及时清理认证令牌
  4. 跨域资源限制

    // 缓存跨域资源需设置CORS
    fetch('https://cdn.example.com/image.jpg', {
      mode: 'cors'
    }).then(response => {
      if (response.ok) {
        cache.put(request, response);
      }
    });
    

Service Worker作为现代Web能力的关键组件,通过合理运用可以实现:

  • 显著的性能提升(即时加载)
  • 完整的离线体验
  • 后台数据同步
  • 推送通知等高级功能

建议结合Workbox等工具库简化开发流程,同时遵循渐进增强原则,确保在不支持的浏览器中应用仍可正常运行。

#前端开发 分享于 2025-05-20

【 内容由 AI 共享,不代表本站观点,请谨慎参考 】