Skip to content

Service Worker原理与应用

graph TD
    A[Web应用] -->|请求| B{Service Worker}
    B -->|拦截请求| C[缓存策略]
    B -->|无匹配缓存| D[网络请求]
    C -->|返回缓存| A
    D -->|返回资源并缓存| C
    E[浏览器] -->|注册| B
    B -->|控制| A
    style B fill:#f9d77e,stroke:#333,stroke-width:2px
    style C fill:#a8e6cf,stroke:#333,stroke-width:1px
    style D fill:#ffd3b6,stroke:#333,stroke-width:1px

1. Service Worker概述

Service Worker是一种运行在浏览器背景中的脚本,它独立于网页,为现代Web应用提供了丰富的离线体验、定期的后台同步以及推送通知等功能。作为PWA(Progressive Web App,渐进式Web应用)的核心技术,Service Worker充当了Web应用程序、浏览器与网络之间的代理服务器,拦截和修改网络请求,精细控制缓存资源。

1.1 主要特性

  • 运行在独立线程:Service Worker在工作线程(Worker)环境中运行,不会阻塞主线程
  • 完全异步:不支持同步XHR和localStorage等同步API操作
  • 不能直接访问DOM:无法直接操作页面DOM,需通过postMessage与页面通信
  • 安全限制:仅在HTTPS环境下工作(开发环境下localhost例外)
  • 生命周期独立于页面:即使用户关闭了网站,Service Worker仍可在后台运行
  • 支持离线运行:通过缓存机制,可以在离线环境下提供Web应用服务

2. Service Worker的生命周期

Service Worker拥有完全独立于Web页面的生命周期,理解这一生命周期对于有效开发和调试Service Worker至关重要。

stateDiagram-v2
    [*] --> 下载
    下载 --> 安装: install事件
    安装 --> 等待: 安装成功
    安装 --> 废弃: 安装失败
    等待 --> 激活: activate事件
    激活 --> 空闲: 激活成功
    空闲 --> 终止: 一段时间无操作
    终止 --> 空闲: 有新请求
    空闲 --> 处理请求: fetch事件
    处理请求 --> 空闲: 请求完成
    激活 --> 更新: 检测到新版本
    更新 --> 安装: 安装新版本
    废弃 --> [*]

2.1 注册阶段

Service Worker的使用始于注册过程,通常在应用的主JavaScript文件中进行:

javascript
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js')
    .then(registration => {
      console.log('Service Worker注册成功,作用域为:', registration.scope);
    })
    .catch(error => {
      console.error('Service Worker注册失败:', error);
    });
}

注册代码会告诉浏览器Service Worker脚本的位置。

2.2 下载与解析

浏览器会下载、解析并执行Service Worker文件。如果这个过程中出现任何错误(如语法错误),注册将会失败,Service Worker将不会被安装。

2.3 安装阶段

下载并执行成功后,Service Worker会触发install事件。这是Service Worker生命周期中仅发生一次的事件,通常用于缓存应用所需的静态资源:

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

event.waitUntil()方法接受一个Promise,它会延长事件的生命周期直到Promise被解决。这确保Service Worker不会在安装完成前就被终止。

2.4 激活阶段

安装成功后,Service Worker进入激活阶段,触发activate事件。这个阶段通常用于清理旧版本的缓存:

javascript
self.addEventListener('activate', event => {
  const cacheWhitelist = ['v1'];
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.map(cacheName => {
          if (cacheWhitelist.indexOf(cacheName) === -1) {
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});

新安装的Service Worker可能会延迟激活,直到使用旧版本的页面都已关闭。这确保了只有一个版本的Service Worker在运行。

2.5 控制页面

激活后,Service Worker将能够控制在其作用域内的页面,监听fetch事件并拦截网络请求:

javascript
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request).then(response => {
      // 如果在缓存中找到了匹配的请求,返回它
      // caches.match()方法在Cache对象中查找与请求匹配的缓存响应
      // 它会检查URL和请求方法等是否匹配缓存中的条目
      if (response) {
        return response;
      }
      // 否则发起网络请求
      return fetch(event.request);
    })
  );
});

2.6 更新机制

当用户再次访问网站时,浏览器会检查服务器上的Service Worker文件是否有更新。如有更新(字节不同),会安装新版本,但不会立即激活

sequenceDiagram
    participant 浏览器
    participant 旧SW as 旧Service Worker
    participant 新SW as 新Service Worker
    participant 网络
    
    浏览器->>网络: 检查SW文件更新
    网络-->>浏览器: 返回新版本SW
    浏览器->>新SW: 安装(install事件)
    新SW-->>浏览器: 安装完成
    
    Note over 浏览器: 此时旧SW仍在控制页面
    
    浏览器->>旧SW: 继续处理请求
    
    Note over 浏览器,新SW: 等待所有使用旧SW的页面关闭
    
    浏览器->>新SW: 激活(activate事件)
    新SW-->>浏览器: 激活完成
    浏览器->>新SW: 后续请求由新SW处理

3. Service Worker的通信机制

Service Worker虽然不能直接操作DOM,但可以通过以下方式与Web页面通信:

3.1 使用postMessage通信

Service Worker与页面之间可以通过postMessageAPI进行双向通信:

从页面向Service Worker发送消息:

javascript
navigator.serviceWorker.controller.postMessage({
  type: 'COMMAND',
  payload: 'Hello from page!'
});

从Service Worker接收消息:

javascript
self.addEventListener('message', event => {
  console.log('收到来自页面的消息:', event.data);
});

从Service Worker向页面发送消息:

javascript
self.clients.matchAll().then(clients => {
  clients.forEach(client => {
    client.postMessage({
      type: 'UPDATE',
      payload: 'Hello from Service Worker!'
    });
  });
});

在页面中接收消息:

javascript
navigator.serviceWorker.addEventListener('message', event => {
  console.log('收到来自Service Worker的消息:', event.data);
});

4. 缓存策略

缓存是Service Worker实现离线功能的关键。根据应用需求的不同,可以采用不同的缓存策略:

graph TD
    subgraph CacheOnly["缓存优先 (Cache Only)"]
        A1[请求] --> B1[缓存]
        B1 --> C1[返回]
    end
    
    subgraph NetworkOnly["网络优先 (Network Only)"]
        A2[请求] --> B2[网络]
        B2 --> C2[返回]
    end
    
    
    
    style CacheOnly fill:#e6f7ff,stroke:#1890ff,stroke-width:2px
    style NetworkOnly fill:#fff7e6,stroke:#fa8c16,stroke-width:2px

graph TD
    subgraph CacheFirst["缓存优先,网络备用 (CacheFirst)"]
      A3[请求] --> B3{缓存?}
      B3 -->|有| C3[返回缓存]
      B3 -->|无| D3[网络请求]
      D3 --> E3[返回并缓存]
    end
  
    subgraph NetworkFirst["网络优先,缓存备用 (NetworkFirst)"]
      A4[请求] --> B4[网络请求]
      B4 -->|成功| C4[返回并缓存]
      B4 -->|失败| D4[查找缓存]
      D4 --> E4[返回缓存]
    end
  
    subgraph StaleWhileRevalidate["使用旧缓存并更新(Stale While Revalidate)"]
      A5[请求] --> B5{缓存?}
      B5 -->|有| C5[返回缓存]
      B5 -->|无| D5[网络请求]
      C5 --> E5[同时更新缓存]
      D5 --> E5
      E5 --> F5[更新完成]
    end

    style CacheFirst fill:#f6ffed,stroke:#52c41a,stroke-width:2px
    style NetworkFirst fill:#fff2e8,stroke:#fa541c,stroke-width:2px
    style StaleWhileRevalidate fill:#f9f0ff,stroke:#722ed1,stroke-width:2px

4.1 Cache Only (仅缓存)

所有请求直接从缓存中获取,完全不使用网络。适用于不会改变的静态资源。

javascript
self.addEventListener('fetch', event => {
  event.respondWith(caches.match(event.request));
});

4.2 Network Only (仅网络)

所有请求必须从网络获取,完全不使用缓存。适用于需要最新数据的API请求。

javascript
self.addEventListener('fetch', event => {
  event.respondWith(fetch(event.request));
});

4.3 Cache First (缓存优先,网络备用)

先尝试从缓存中获取资源,如果没有找到再从网络获取。适用于性能优先且资源很少更新的场景。

javascript
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request).then(response => {
      return response || fetch(event.request).then(response => {
        // 将从网络获取的资源存入缓存
        return caches.open('v1').then(cache => {
          cache.put(event.request, response.clone());
          return response;
        });
      });
    })
  );
});

4.4 Network First (网络优先,缓存备用)

先尝试从网络获取最新资源,如果网络不可用则从缓存获取。适用于需要最新数据但又要提供离线体验的场景。

javascript
self.addEventListener('fetch', event => {
  event.respondWith(
    fetch(event.request).catch(() => {
      return caches.match(event.request);
    })
  );
});

4.5 Stale While Revalidate (使用旧缓存同时更新)

先返回缓存的资源(无论是否过期),同时在后台发起网络请求获取最新资源并更新缓存。适用于需要快速响应且内容可以稍微滞后的场景。

javascript
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.open('v1').then(cache => {
      return cache.match(event.request).then(response => {
        const fetchPromise = fetch(event.request).then(networkResponse => {
          cache.put(event.request, networkResponse.clone());
          return networkResponse;
        });
        return response || fetchPromise;
      });
    })
  );
});

5. 使用Workbox简化开发

Workbox是Google提供的一套库,用于简化Service Worker的开发过程。它提供了一系列工具,可以轻松实现各种缓存策略和Service Worker功能。

5.1 Workbox概述

Workbox是一个由多个库组成的集合,每个库负责Service Worker的不同方面:

  • workbox-routing:处理路由匹配
  • workbox-strategies:实现缓存策略
  • workbox-precaching:预缓存资源
  • workbox-expiration:管理缓存过期
  • workbox-background-sync:支持后台同步
  • workbox-window:简化Service Worker注册和更新
graph TD
    A[Workbox核心] --> B[workbox-routing]
    A --> C[workbox-strategies]
    A --> D[workbox-precaching]
    A --> E[workbox-expiration]
    A --> F[workbox-background-sync]
    A --> G[workbox-window]
    
    B --> H[路由匹配]
    C --> I[缓存策略实现]
    D --> J[资源预缓存]
    E --> K[缓存过期管理]
    F --> L[后台同步]
    G --> M[Service Worker注册和更新]
    
    style A fill:#f9d77e,stroke:#333,stroke-width:2px
    style B,C,D,E,F,G fill:#a8e6cf,stroke:#333,stroke-width:1px

5.2 使用Workbox实现缓存策略

使用Workbox可以简化缓存策略的实现:

javascript
// 导入Workbox
importScripts('https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-sw.js');

// 预缓存重要资源
workbox.precaching.precacheAndRoute([
  { url: '/', revision: '1' },
  { url: '/index.html', revision: '1' },
  { url: '/styles.css', revision: '1' },
  { url: '/app.js', revision: '1' }
]);

// 为图片应用Cache First策略
workbox.routing.registerRoute(
  ({ request }) => request.destination === 'image',
  new workbox.strategies.CacheFirst({
    cacheName: 'images',
    plugins: [
      new workbox.expiration.ExpirationPlugin({
        maxEntries: 50,
        maxAgeSeconds: 30 * 24 * 60 * 60, // 30天
      }),
    ],
  })
);

// 为API请求应用Network First策略
workbox.routing.registerRoute(
  new RegExp('/api/'),
  new workbox.strategies.NetworkFirst({
    cacheName: 'api-responses',
    networkTimeoutSeconds: 3,
  })
);

// 为静态资源应用Stale While Revalidate策略
workbox.routing.registerRoute(
  ({ request }) => request.destination === 'script' || 
                   request.destination === 'style',
  new workbox.strategies.StaleWhileRevalidate({
    cacheName: 'static-resources',
  })
);

6. Service Worker高级功能

除了缓存和离线功能外,Service Worker还支持许多高级功能:

6.1 后台同步 (Background Sync)

允许在用户重新联网时执行操作,例如发送之前离线时未能发送的表单数据:

javascript
self.addEventListener('sync', event => {
  if (event.tag === 'outbox') {
    event.waitUntil(sendOutboxMessages());
  }
});
sequenceDiagram
    participant 用户
    participant 页面
    participant SW as Service Worker
    participant 服务器
    
    用户->>页面: 提交表单
    页面->>SW: 注册sync任务(outbox)
    
    Note over 用户,服务器: 用户离线
    
    页面->>SW: 存储表单数据
    
    Note over 用户,服务器: 用户重新联网
    
    SW->>SW: 触发sync事件
    SW->>服务器: 发送存储的表单数据
    服务器-->>SW: 响应成功
    SW->>页面: 通知提交成功

6.2 推送通知 (Push Notifications)

允许Web应用即使在未打开的情况下也能接收推送消息:

javascript
self.addEventListener('push', event => {
  const notificationData = event.data.json();
  self.registration.showNotification(notificationData.title, {
    body: notificationData.body,
    icon: notificationData.icon
  });
});

self.addEventListener('notificationclick', event => {
  event.notification.close();
  event.waitUntil(
    clients.openWindow('https://example.com')
  );
});
sequenceDiagram
    participant 推送服务
    participant SW as Service Worker
    participant 浏览器
    participant 用户
    
    推送服务->>SW: 发送推送消息
    SW->>SW: 触发push事件
    SW->>浏览器: 显示通知
    浏览器->>用户: 展示通知给用户
    用户->>浏览器: 点击通知
    浏览器->>SW: 触发notificationclick事件
    SW->>浏览器: 打开相关页面

6.3 导航预加载 (Navigation Preload)

加速导航请求,在Service Worker启动的同时并行地发起网络请求:

javascript
self.addEventListener('activate', event => {
  event.waitUntil(
    self.registration.navigationPreload.enable()
  );
});

self.addEventListener('fetch', event => {
  event.respondWith(async function() {
    // 尝试获取预加载的响应
    const preloadResponse = await event.preloadResponse;
    if (preloadResponse) {
      return preloadResponse;
    }
    
    // 如果没有预加载响应,回退到正常的网络请求
    return fetch(event.request);
  }());
});
sequenceDiagram
    participant 用户
    participant 浏览器
    participant SW as Service Worker
    participant 网络
    
    用户->>浏览器: 点击链接或输入URL
    浏览器->>SW: 启动Service Worker
    浏览器->>网络: 同时发起预加载请求
    
    par 并行处理
        SW->>SW: 启动并运行
    and
        网络-->>浏览器: 返回预加载响应
    end
    
    浏览器->>SW: 触发fetch事件(带有预加载响应)
    SW->>浏览器: 使用预加载响应

7. 实战应用案例

7.1 实现离线功能的PWA

下面是一个实现了完整离线功能的PWA应用的Service Worker示例:

javascript
const CACHE_NAME = 'my-app-v1';
const STATIC_ASSETS = [
  '/',
  '/index.html',
  '/app.js',
  '/styles.css',
  '/images/logo.png',
  '/images/offline.png',
  '/offline.html'
];

// 安装阶段:预缓存静态资源
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => cache.addAll(STATIC_ASSETS))
      .then(() => self.skipWaiting()) // 强制新安装的Service Worker激活
  );
});

// 激活阶段:清理旧缓存
self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames
          .filter(name => name !== CACHE_NAME)
          .map(name => caches.delete(name))
      );
    }).then(() => self.clients.claim()) // 控制未受控制的客户端
  );
});

// 拦截请求
self.addEventListener('fetch', event => {
  // 处理导航请求
  if (event.request.mode === 'navigate') {
    event.respondWith(
      fetch(event.request)
        .catch(() => caches.match('/offline.html'))
    );
    return;
  }
  
  // 对API请求使用"网络优先,缓存备用"策略
  if (event.request.url.includes('/api/')) {
    event.respondWith(
      fetch(event.request)
        .then(response => {
          // 将有效响应存入缓存
          if (response.ok) {
            const responseClone = response.clone();
            caches.open(CACHE_NAME).then(cache => {
              cache.put(event.request, responseClone);
            });
          }
          return response;
        })
        .catch(() => caches.match(event.request))
    );
    return;
  }
  
  // 对其他请求使用"缓存优先,网络备用"策略
  event.respondWith(
    caches.match(event.request)
      .then(cachedResponse => {
        if (cachedResponse) {
          return cachedResponse;
        }
        
        return fetch(event.request)
          .then(response => {
            // 将有效响应存入缓存
            if (response.ok && response.type === 'basic') {
              const responseClone = response.clone();
              caches.open(CACHE_NAME).then(cache => {
                cache.put(event.request, responseClone);
              });
            }
            return response;
          })
          .catch(error => {
            // 如果请求的是图片,返回备用离线图片
            if (event.request.destination === 'image') {
              return caches.match('/images/offline.png');
            }
            throw error;
          });
      })
  );
});

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

// 处理推送通知
self.addEventListener('push', event => {
  const data = event.data.json();
  self.registration.showNotification(data.title, {
    body: data.body,
    icon: '/images/notification-icon.png',
    badge: '/images/notification-badge.png',
    data: data.data
  });
});

// 处理通知点击
self.addEventListener('notificationclick', event => {
  event.notification.close();
  if (event.notification.data && event.notification.data.url) {
    event.waitUntil(
      clients.openWindow(event.notification.data.url)
    );
  }
});

8. Service Worker调试技巧

8.1 使用Chrome DevTools

Chrome浏览器提供了强大的Service Worker调试工具:

  1. 打开DevTools (F12)
  2. 切换到Application标签
  3. 在左侧面板找到Service Workers
graph LR
    A[Chrome DevTools] --> B[Application标签]
    B --> C[Service Workers面板]
    C --> D[状态查看]
    C --> E[强制更新]
    C --> F[模拟离线]
    C --> G[Update on reload]
    C --> H[错误日志]
    C --> I[卸载]
    
    style C fill:#f9d77e,stroke:#333,stroke-width:2px
    style D,E,F,G,H,I fill:#a8e6cf,stroke:#333,stroke-width:1px

这里可以进行以下操作:

  • 查看Service Worker的状态
  • 强制更新Service Worker
  • 模拟离线状态
  • 切换"Update on reload"选项(每次刷新页面时强制更新Service Worker)
  • 查看Service Worker的错误日志
  • 卸载Service Worker

8.2 常见调试问题及解决方案

  1. Service Worker无法注册

    • 确保使用HTTPS或localhost
    • 检查Service Worker文件路径是否正确
    • 确保Service Worker作用域适当
  2. 缓存不生效

    • 检查缓存名称是否一致
    • 确认fetch事件处理器正确拦截请求
    • 验证路径匹配规则
  3. Service Worker不更新

    • 确保新Service Worker文件内容有变化
    • 检查更新流程中可能阻塞的waitUntil调用
    • 可以在DevTools中尝试"Update on reload"选项
  4. 无法清除旧缓存

    • 确保在activate事件中正确处理清理逻辑
    • 检查缓存键名匹配

9. 最佳实践与性能优化

9.1 Service Worker最佳实践

  1. 不要在Service Worker中缓存过大的资源:每个浏览器对缓存大小有限制

  2. 避免在install事件中缓存大量资源:可能导致安装失败

  3. 实现适当的更新策略:使用版本控制和合理的缓存过期策略

  4. 处理跨域请求:注意CORS和不透明响应(opaque responses)的处理

  5. 提供回退机制:当网络和缓存都失败时,提供适当的回退页面

  6. 注意作用域:Service Worker只能控制在其作用域下的页面

9.2 性能优化

  1. 使用导航预加载:减少Service Worker启动延迟对导航请求的影响

  2. 选择合适的缓存策略:不同类型的资源应使用不同的缓存策略

  3. 缓存管理:定期清理过期缓存以释放存储空间

  4. 最小化Service Worker文件大小:减少解析和执行时间

  5. 避免不必要的网络请求:合理使用缓存可以减少网络负载

10. 未来发展

Service Worker技术仍在不断发展,未来可能会有更多功能:

mindmap
  root((Service Worker未来))
    后台处理
      更复杂的任务执行
      更长的运行时间
    Web API整合
      Web Share
      Web Payments
      Web Bluetooth
    缓存控制
      更精细的策略
      更智能的存储管理
    跨浏览器支持
      统一实现标准
      更一致的行为
    原生平台整合
      更接近原生体验
      更深度的系统集成
  1. 更强大的后台处理能力:执行更复杂的后台任务

  2. 与其他Web API的深度整合:如Web Share、Web Payments等API

  3. 更精细的缓存控制:更灵活的缓存策略和存储管理

  4. 更好的跨浏览器支持:各浏览器实现更加统一

  5. 与原生平台的更深度整合:提供更接近原生体验的功能

参考资料

  1. MDN Web Docs - Service Worker API
  2. Google Developers - Service Workers: an Introduction
  3. web.dev - Service workers and the Cache Storage API
  4. Workbox 官方文档
  5. Jake Archibald's Offline Cookbook

Q&A

基础概念类问题

什么是Service Worker?它与Web Worker有什么区别?

  • Service Worker是一种特殊的Web Worker,作为网页与网络之间的代理服务器,能够拦截、修改网络请求并缓存资源
  • 区别:
    • 功能范围:Service Worker可以拦截网络请求、持久存在、有生命周期事件,而普通Web Worker只是执行JavaScript计算任务
    • 持久性:Service Worker在页面关闭后仍可存在,而Web Worker随页面关闭而终止
    • 上下文环境:Service Worker运行在自己的全局上下文中,与页面JavaScript环境完全分离
    • 通信方式:Service Worker使用postMessage与页面通信,还可以通过Fetch API拦截请求
    • 注册方式:Service Worker需要通过特定的注册流程,而Web Worker通过构造函数创建

为什么Service Worker只能在HTTPS环境下运行?

  • 由于Service Worker可以拦截和修改网络请求,在HTTP环境下容易被中间人攻击利用,攻击者可能注入恶意代码
  • 安全性考虑:
    • 防止恶意代码篡改网络通信和用户数据
    • 保护用户隐私,避免敏感信息被窃取
    • 确保Service Worker代码的完整性和可信度
    • 符合现代Web安全最佳实践
  • 例外情况:localhost(本地开发环境)可以不使用HTTPS,便于开发调试

Service Worker的生命周期有哪些阶段?每个阶段的作用是什么?

  • 注册(Registration):应用告知浏览器Service Worker的位置
  • 下载(Download):浏览器下载Service Worker脚本文件
  • 安装(Install)
    • 触发install事件
    • 适合预缓存静态资源(HTML、CSS、JS、图片等应用外壳资源)
    • 通过event.waitUntil()确保安装完成
  • 等待(Waiting)
    • 确保同一时间只有一个版本在运行,避免版本冲突
    • 等待旧版本Service Worker控制的页面全部关闭
    • 可以通过skipWaiting()跳过等待阶段
  • 激活(Activate)
    • 触发activate事件
    • 适合清理旧缓存和旧版本资源
    • 可以通过clients.claim()立即控制未被控制的客户端
  • 运行/空闲(Running/Idle):处理fetch、push等事件
  • 终止(Terminated):浏览器可能在空闲时终止Service Worker以节省资源
  • 更新(Update):当检测到新版本时重新开始生命周期

Service Worker作用域(scope)是什么?如何配置?

  • 作用域决定了Service Worker可以控制的页面范围,定义了其"管辖权限"
  • 默认为Service Worker文件所在的路径及其子路径
  • 配置方法:
    • 通过register方法的第二个参数配置:navigator.serviceWorker.register('/sw.js', {scope: '/app/'})
    • 作用域必须是Service Worker所在路径的子路径或相同路径
    • 要控制更高级别的路径,需要设置Service-Worker-Allowed HTTP头
  • 实际应用:
    • 可以为不同部分的应用使用不同的Service Worker
    • 例如:主应用使用一个Service Worker,而管理后台使用另一个

缓存策略类问题

请解释Service Worker的5种缓存策略及其适用场景

  • Cache Only(仅缓存)

    • 实现:直接从缓存返回资源,不发起网络请求
    • 适用场景:静态资源,如应用外壳(App Shell)、字体、图标等永远不变或很少变化的资源
    • 优势:响应速度最快,无网络依赖
    • 风险:如果资源未预缓存,将无法获取
  • Network Only(仅网络)

    • 实现:始终从网络获取资源,完全不使用缓存
    • 适用场景:需要实时数据的API请求、支付交易、用户特定内容等
    • 优势:始终获取最新数据
    • 风险:离线时完全无法工作
  • Cache First(缓存优先,网络备用)

    • 实现:先检查缓存,有则使用;无则从网络获取并缓存
    • 适用场景:不常变化的资源,如CSS、JavaScript库、字体、图标等
    • 优势:大幅提升性能,减少网络请求
    • 风险:更新频率低,用户可能看到过时内容
  • Network First(网络优先,缓存备用)

    • 实现:先尝试从网络获取,失败则从缓存获取
    • 适用场景:频繁更新但又要提供离线体验的内容,如新闻文章、博客、产品信息
    • 优势:在线时获取最新内容,离线时仍可访问
    • 风险:网络较慢时用户体验不佳
  • Stale While Revalidate(使用旧缓存同时更新)

    • 实现:立即返回缓存内容(无论是否过期),同时在后台更新缓存
    • 适用场景:既需要快速响应又需要最新内容的场景,如社交媒体feed、评论系统
    • 优势:结合了速度和新鲜度,用户体验最佳
    • 风险:用户可能先看到旧内容,后台更新失败时不会通知用户

如何处理Service Worker中的缓存过期问题?

  • 使用缓存版本号进行标记

    • 为每个缓存创建唯一的版本标识符(如'cache-v1'、'cache-v2')
    • 当需要更新缓存策略时,增加版本号
    • 在activate事件中清理旧版本缓存
  • 在activate事件中清理旧版本缓存

    javascript
    self.addEventListener('activate', event => {
      const currentCaches = ['cache-v2']; // 当前使用的缓存版本
      event.waitUntil(
        caches.keys().then(cacheNames => {
          return Promise.all(
            cacheNames.map(cacheName => {
              if (!currentCaches.includes(cacheName)) {
                return caches.delete(cacheName); // 删除旧版本缓存
              }
            })
          );
        })
      );
    });
  • 使用Workbox的ExpirationPlugin

    • 设置缓存时间限制:maxAgeSeconds(资源最大存活时间)
    • 设置缓存数量限制:maxEntries(缓存项目数量上限)
    • 自动清理过期资源:
    javascript
    workbox.routing.registerRoute(
      /\.(?:png|jpg|jpeg|svg|gif)$/,
      new workbox.strategies.CacheFirst({
        cacheName: 'images',
        plugins: [
          new workbox.expiration.ExpirationPlugin({
            maxEntries: 60,
            maxAgeSeconds: 30 * 24 * 60 * 60, // 30天
          }),
        ],
      })
    );
  • 基于HTTP缓存头的策略

    • 尊重资源的Cache-Control和Expires头
    • 实现条件请求(使用ETag或Last-Modified)

如何处理大型文件(如视频)的缓存策略?

  • 避免直接缓存大文件

    • Cache API有存储限制(通常为浏览器总存储空间的一定比例)
    • 大文件缓存可能导致其他重要资源被挤出缓存
  • 使用range requests处理分段请求

    • 实现对视频等大文件的部分请求处理
    • 只缓存用户实际观看的视频片段
    javascript
    self.addEventListener('fetch', event => {
      const url = new URL(event.request.url);
      if (url.pathname.endsWith('.mp4') && event.request.headers.has('range')) {
        // 处理范围请求逻辑
      }
    });
  • 考虑使用IndexedDB代替Cache API存储大文件

    • IndexedDB通常有更大的存储限制
    • 可以实现更精细的存储管理
    • 支持结构化存储和查询
    javascript
    // 存储视频到IndexedDB
    function storeVideo(videoId, videoBlob) {
      const dbPromise = indexedDB.open('video-store', 1);
      // 实现存储逻辑
    }
  • 实现智能缓存策略

    • 只缓存视频的元数据和初始片段
    • 基于用户行为预测和缓存(如常看的视频)
    • 实现缓存优先级(重要视频保留更长时间)
    • 监控设备存储状态,在存储紧张时清理低优先级缓存

实现与调试类问题

Service Worker如何实现离线访问功能?请详述完整流程

  1. 注册Service Worker

    javascript
    if ('serviceWorker' in navigator) {
      window.addEventListener('load', () => {
        navigator.serviceWorker.register('/sw.js')
          .then(registration => {
            console.log('SW注册成功:', registration.scope);
          })
          .catch(error => {
            console.log('SW注册失败:', error);
          });
      });
    }
  2. install事件中缓存核心资源

    javascript
    const CACHE_NAME = 'offline-v1';
    const OFFLINE_URLS = [
      '/',
      '/index.html',
      '/styles/main.css',
      '/scripts/main.js',
      '/images/logo.png',
      '/offline.html'  // 离线回退页面
    ];
    
    self.addEventListener('install', event => {
      event.waitUntil(
        caches.open(CACHE_NAME)
          .then(cache => {
            console.log('缓存核心资源');
            return cache.addAll(OFFLINE_URLS);
          })
          .then(() => self.skipWaiting())  // 立即激活
      );
    });
  3. fetch事件中拦截请求,返回缓存内容

    javascript
    self.addEventListener('fetch', event => {
      // 网络优先策略
      event.respondWith(
        fetch(event.request)
          .catch(() => {
            // 网络请求失败,尝试从缓存获取
            return caches.match(event.request)
              .then(response => {
                if (response) {
                  return response;  // 返回缓存的资源
                }
                
                // 如果是HTML请求,返回离线页面
                if (event.request.mode === 'navigate') {
                  return caches.match('/offline.html');
                }
                
                // 其他资源无法提供
                return new Response('网络不可用', {
                  status: 503,
                  statusText: 'Service Unavailable'
                });
              });
          })
      );
    });
  4. 提供离线回退页面

    • 创建专门的offline.html页面,告知用户当前处于离线状态
    • 确保该页面所需的所有资源(CSS、图片等)都已缓存
    • 在页面中提供有用信息和可能的离线功能
  5. 激活并更新缓存

    javascript
    self.addEventListener('activate', event => {
      // 清理旧缓存
      event.waitUntil(
        caches.keys().then(cacheNames => {
          return Promise.all(
            cacheNames.map(cacheName => {
              if (cacheName !== CACHE_NAME) {
                return caches.delete(cacheName);
              }
            })
          );
        }).then(() => self.clients.claim())  // 接管现有客户端
      );
    });

Service Worker的更新机制是什么?如何强制更新Service Worker?

  • 更新触发机制

    • 浏览器检测到Service Worker文件字节差异时触发更新
    • 页面导航时会检查Service Worker更新(至少24小时检查一次)
    • 通过registration.update()手动触发更新检查
  • 更新流程

    1. 浏览器下载新版本Service Worker
    2. 新版本进入安装阶段(触发install事件)
    3. 安装成功后进入等待状态(waiting)
    4. 当所有使用旧版本的页面关闭后,新版本激活
  • 使用skipWaiting()跳过等待状态

    javascript
    self.addEventListener('install', event => {
      event.waitUntil(
        // 缓存资源后立即跳过等待
        caches.open('v2').then(cache => {
          return cache.addAll(resources);
        }).then(() => {
          return self.skipWaiting();  // 强制激活
        })
      );
    });
  • 使用clients.claim()接管现有客户端

    javascript
    self.addEventListener('activate', event => {
      event.waitUntil(
        // 清理旧缓存
        caches.keys().then(/* 清理逻辑 */)
          .then(() => {
            return self.clients.claim();  // 接管所有客户端
          })
      );
    });
  • 通过版本号变更或添加查询参数触发更新检测

    javascript
    // 添加时间戳或版本号强制更新
    navigator.serviceWorker.register('/sw.js?v=' + Date.now())
  • 实现更新通知和刷新机制

    javascript
    // 在页面中检测Service Worker更新
    navigator.serviceWorker.addEventListener('controllerchange', () => {
      // Service Worker已更新并接管页面
      if (refreshing) return;
      refreshing = true;
      window.location.reload();  // 刷新页面应用更新
    });

如何调试Service Worker?有哪些常用工具和方法?

  • Chrome DevTools的Application面板

    • 查看Service Worker状态和生命周期
    • 启用"Update on reload"自动更新
    • 模拟离线状态测试离线功能
    • 查看缓存存储内容
    • 查看Service Worker日志和错误
  • Firefox的调试工具

    • 在"调试"面板中的"Service Workers"选项
    • 查看注册的Service Worker和状态
    • 检查缓存存储
  • "Update on reload"选项

    • 在Chrome DevTools中启用此选项
    • 每次刷新页面时强制更新Service Worker
    • 加速开发和测试周期
  • 清除Service Worker的方法

    javascript
    // 编程方式注销Service Worker
    navigator.serviceWorker.getRegistrations().then(registrations => {
      for(let registration of registrations) {
        registration.unregister();
      }
    });
  • 使用Lighthouse审计

    • 评估PWA功能和性能
    • 检查Service Worker实现是否符合最佳实践
  • Service Worker调试技巧

    • 使用self.skipWaiting()和clients.claim()加速更新
    • 添加详细日志便于调试:
      javascript
      console.log('[ServiceWorker] 安装中...');
    • 使用Chrome DevTools的"Preserve log"选项保留日志
    • 使用workbox-cli等工具生成Service Worker

如何处理Service Worker中的跨域请求?

  • 理解不透明响应(opaque responses)

    • 当请求跨域资源且没有CORS头时返回
    • 状态码为0,无法读取内容
    • 缓存大小可能比实际资源大
  • CORS策略配置

    • 在服务器端添加适当的CORS头:
      Access-Control-Allow-Origin: *
    • 或针对特定域名:
      Access-Control-Allow-Origin: https://your-app.com
  • 使用mode: 'cors'配置fetch请求

    javascript
    fetch('https://api.example.com/data', {
      mode: 'cors',  // 明确请求CORS响应
      credentials: 'same-origin'  // 控制是否发送cookies
    })
  • 处理opaque responses的缓存限制

    • 避免缓存不透明响应,或谨慎处理
    • 使用no-cors模式获取跨域资源:
      javascript
      fetch('https://third-party.com/image.jpg', {
        mode: 'no-cors'  // 允许请求,但返回不透明响应
      })
    • 考虑使用代理服务器中转请求,避免跨域问题
  • 预检请求(Preflight)处理

    • 复杂请求会触发OPTIONS预检请求
    • 确保服务器正确响应OPTIONS请求:
      Access-Control-Allow-Methods: GET, POST, PUT
      Access-Control-Allow-Headers: Content-Type

高级应用类问题

描述如何使用Service Worker实现推送通知功能

  1. 获取用户授权

    javascript
    // 请求通知权限
    function requestNotificationPermission() {
      return Notification.requestPermission().then(permission => {
        if (permission !== 'granted') {
          throw new Error('通知权限被拒绝');
        }
        return permission;
      });
    }
  2. 订阅推送服务

    javascript
    // 获取推送订阅
    function subscribeToPushNotifications(serviceWorkerReg) {
      const applicationServerKey = urlBase64ToUint8Array(
        'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U'
      );
      
      return serviceWorkerReg.pushManager.subscribe({
        userVisibleOnly: true,  // 承诺通知对用户可见
        applicationServerKey: applicationServerKey
      })
      .then(subscription => {
        // 将订阅信息发送到服务器
        return fetch('/api/save-subscription', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(subscription)
        });
      });
    }
  3. 监听push事件

    javascript
    // 在Service Worker中处理推送消息
    self.addEventListener('push', event => {
      let notificationData = {};
      
      try {
        notificationData = event.data.json();
      } catch (e) {
        notificationData = {
          title: '新消息',
          body: event.data ? event.data.text() : '无内容'
        };
      }
      
      // 确保Service Worker保持活动状态直到通知显示完成
      event.waitUntil(
        self.registration.showNotification(notificationData.title, {
          body: notificationData.body,
          icon: '/images/notification-icon.png',
          badge: '/images/badge-icon.png',
          data: notificationData.data || {},
          actions: notificationData.actions || []
        })
      );
    });
  4. 显示通知并处理点击事件

    javascript
    // 处理通知点击
    self.addEventListener('notificationclick', event => {
      event.notification.close();
      
      // 提取通知数据
      const notificationData = event.notification.data;
      let urlToOpen = new URL('/', self.location.origin).href;
      
      if (notificationData && notificationData.url) {
        urlToOpen = notificationData.url;
      }
      
      // 处理特定操作按钮点击
      if (event.action === 'view-details') {
        urlToOpen = notificationData.detailsUrl || urlToOpen;
      }
      
      // 打开或聚焦窗口
      event.waitUntil(
        self.clients.matchAll({
          type: 'window',
          includeUncontrolled: true
        })
        .then(clientList => {
          // 检查是否已有打开的窗口
          for (let client of clientList) {
            if (client.url === urlToOpen && 'focus' in client) {
              return client.focus();
            }
          }
          // 如果没有打开的窗口,则打开新窗口
          if (self.clients.openWindow) {
            return self.clients.openWindow(urlToOpen);
          }
        })
      );
    });

Service Worker与PWA的关系是什么?PWA的核心特性有哪些?

  • Service Worker是PWA的核心技术之一

    • 提供PWA的离线功能和网络弹性
    • 实现后台同步和推送通知
    • 控制缓存策略,提升性能
    • 作为PWA与网络之间的代理
  • PWA核心特性

    • 可安装(Installable)

      • 通过Web App Manifest实现
      • 添加到主屏幕,类似原生应用
      • 全屏或沉浸式体验
      • 自定义启动画面和图标
    • 离线工作(Offline Capability)

      • 通过Service Worker缓存关键资源
      • 提供离线页面和功能
      • 实现网络弹性,在弱网络环境下仍可使用
    • 推送通知(Push Notifications)

      • 通过Push API和Notification API实现
      • 即使用户未打开应用也能接收通知
      • 提高用户参与度和留存率
    • 后台同步(Background Sync)

      • 在网络恢复时自动同步数据
      • 确保用户操作最终完成
      • 提升弱网络环境下的用户体验
    • 渐进增强(Progressive Enhancement)

      • 在支持PWA功能的浏览器中提供增强体验
      • 在不支持的浏览器中仍能基本工作
      • 随着用户浏览器升级,体验自动提升
  • 与Web App Manifest的配合使用

    json
    {
      "name": "我的PWA应用",
      "short_name": "PWA应用",
      "start_url": "/index.html",
      "display": "standalone",
      "background_color": "#ffffff",
      "theme_color": "#4285f4",
      "icons": [
        {
          "src": "/images/icon-192.png",
          "sizes": "192x192",
          "type": "image/png"
        },
        {
          "src": "/images/icon-512.png",
          "sizes": "512x512",
          "type": "image/png"
        }
      ]
    }
  • PWA的技术组成

    • Service Worker:离线功能、缓存、推送通知
    • Web App Manifest:安装体验、外观配置
    • HTTPS:安全通信
    • 响应式设计:适应不同设备
    • App Shell架构:快速加载核心UI

Workbox库的作用是什么?它如何简化Service Worker开发?

  • Workbox是Google开发的Service Worker库

    • 一套用于构建PWA的JavaScript库
    • 简化Service Worker开发的复杂性
    • 遵循性能最佳实践
    • 模块化设计,可按需引入功能
  • 提供预定义的缓存策略

    javascript
    // 使用Workbox的缓存优先策略
    workbox.routing.registerRoute(
      /\.(?:png|jpg|jpeg|svg|gif)$/,
      new workbox.strategies.CacheFirst({
        cacheName: 'images',
        plugins: [
          new workbox.expiration.ExpirationPlugin({
            maxEntries: 60,
            maxAgeSeconds: 30 * 24 * 60 * 60, // 30天
          }),
        ],
      })
    );
    
    // 使用网络优先策略
    workbox.routing.registerRoute(
      /\/api\/.*$/,
      new workbox.strategies.NetworkFirst({
        cacheName: 'api-responses',
        networkTimeoutSeconds: 3,
      })
    );
    
    // 使用Stale While Revalidate策略
    workbox.routing.registerRoute(
      /\.(?:js|css)$/,
      new workbox.strategies.StaleWhileRevalidate({
        cacheName: 'static-resources',
      })
    );
  • 简化路由匹配和请求处理

    • 支持正则表达式、字符串、自定义函数匹配
    • 提供直观的API处理不同类型的请求
    javascript
    // 精确URL匹配
    workbox.routing.registerRoute(
      '/index.html',
      new workbox.strategies.NetworkFirst()
    );
    
    // 自定义匹配函数
    workbox.routing.registerRoute(
      ({url, request, event}) => {
        return url.pathname.startsWith('/special/');
      },
      new workbox.strategies.NetworkFirst()
    );
  • 提供缓存过期、后台同步等模块

    • workbox-expiration:管理缓存大小和过期时间
    • workbox-background-sync:实现离线请求的后台同步
    • workbox-broadcast-update:通知页面缓存更新
    • workbox-cacheable-response:基于状态码或头信息决定是否缓存
    • workbox-range-requests:支持视频等资源的范围请求
  • 构建工具集成

    • 与webpack、Rollup等构建工具集成
    • 自动生成Service Worker文件
    • 预缓存清单管理
    javascript
    // webpack配置
    const { GenerateSW } = require('workbox-webpack-plugin');
    
    module.exports = {
      // 其他webpack配置...
      plugins: [
        new GenerateSW({
          clientsClaim: true,
          skipWaiting: true,
          // 其他Workbox配置
        })
      ]
    };
  • 调试和测试支持

    • 开发模式下详细日志
    • 与Chrome DevTools集成
    • 提供测试辅助工具

如何优化Service Worker的性能?

  • 减小Service Worker文件大小

    • 移除不必要的代码和依赖
    • 使用代码分割和懒加载
    • 压缩和最小化Service Worker文件
    • 使用Workbox的模块化引入而非完整库
    javascript
    // 按需导入Workbox模块
    importScripts('https://storage.googleapis.com/workbox-cdn/releases/6.1.5/workbox-sw.js');
    
    workbox.loadModule('workbox-strategies');
    workbox.loadModule('workbox-routing');
    // 仅加载需要的模块
  • 使用导航预加载(Navigation Preload)

  • 减小Service Worker文件大小

  • 使用导航预加载(Navigation Preload)

  • 避免过度缓存

  • 合理选择缓存策略

  • 优化激活时间

11. Service Worker常用API

Service Worker提供了丰富的API,使开发者能够实现各种离线功能、缓存管理和后台处理能力。以下是Service Worker中最常用的API及其用法:

11.1 注册与生命周期API

ServiceWorkerContainer

navigator.serviceWorker对象提供了注册和管理Service Worker的方法:

javascript
// 注册Service Worker
navigator.serviceWorker.register('/sw.js', {scope: '/'})
  .then(registration => {
    console.log('注册成功,作用域为:', registration.scope);
  });

// 获取当前控制页面的Service Worker
const swController = navigator.serviceWorker.controller;

// 检查Service Worker更新
navigator.serviceWorker.ready.then(registration => {
  registration.update();
});

// 注销Service Worker
navigator.serviceWorker.ready.then(registration => {
  registration.unregister();
});

// 监听控制权变化事件
navigator.serviceWorker.addEventListener('controllerchange', () => {
  console.log('页面控制权已转移到新的Service Worker');
});

生命周期方法

Service Worker内部可以使用以下方法控制其生命周期:

javascript
// 在安装阶段跳过等待,直接激活
self.skipWaiting();

// 在激活阶段接管未被控制的客户端页面
self.clients.claim();

11.2 缓存API (Cache API)

Cache API是Service Worker实现离线功能的核心,提供了缓存请求和响应的能力:

javascript
// 打开缓存
caches.open('cache-v1')
  .then(cache => {
    // 添加资源到缓存
    cache.add('/index.html');
    
    // 批量添加资源
    cache.addAll([
      '/styles.css',
      '/script.js',
      '/images/logo.png'
    ]);
    
    // 手动添加请求-响应对
    cache.put(
      new Request('/api/data'),
      new Response('{"data": "cached"}', {
        headers: {'Content-Type': 'application/json'}
      })
    );
  });

// 查找缓存中的响应
caches.match('/index.html')
  .then(response => {
    if (response) {
      return response;
    }
    return fetch('/index.html');
  });

// 删除缓存
caches.delete('old-cache');

// 获取所有缓存名称
caches.keys()
  .then(cacheNames => {
    console.log('当前缓存:', cacheNames);
  });

11.3 Fetch API

Service Worker可以拦截并处理网络请求:

javascript
// 拦截fetch请求
self.addEventListener('fetch', event => {
  // 使用自定义响应
  event.respondWith(
    caches.match(event.request)
      .then(response => {
        return response || fetch(event.request);
      })
  );
  
  // 修改请求
  event.respondWith(
    fetch(new Request(event.request.url, {
      headers: new Headers({
        'X-Custom-Header': 'value'
      })
    }))
  );
  
  // 创建自定义响应
  event.respondWith(
    new Response('<h1>离线页面</h1>', {
      headers: {'Content-Type': 'text/html'}
    })
  );
});

// 发起网络请求
fetch('/api/data', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({key: 'value'})
})
.then(response => response.json())
.then(data => console.log(data));

11.4 消息通信API

Service Worker与页面之间可以通过消息机制进行通信:

javascript
// 在Service Worker中接收消息
self.addEventListener('message', event => {
  console.log('收到消息:', event.data);
  
  // 回复消息
  event.source.postMessage({
    reply: '收到你的消息',
    originalMessage: event.data
  });
});

// 在Service Worker中向所有客户端发送消息
self.clients.matchAll()
  .then(clients => {
    clients.forEach(client => {
      client.postMessage({
        type: 'UPDATE',
        payload: '资源已更新'
      });
    });
  });

// 在页面中向Service Worker发送消息
navigator.serviceWorker.controller.postMessage({
  type: 'COMMAND',
  action: 'clearCache'
});

// 在页面中接收Service Worker消息
navigator.serviceWorker.addEventListener('message', event => {
  console.log('收到Service Worker消息:', event.data);
});

11.5 后台同步API

允许在用户重新联网时执行操作:

javascript
// 在页面中注册同步任务
navigator.serviceWorker.ready.then(registration => {
  registration.sync.register('sync-task')
    .then(() => {
      console.log('后台同步任务已注册');
    })
    .catch(err => {
      console.error('后台同步注册失败:', err);
    });
});

// 在Service Worker中处理同步事件
self.addEventListener('sync', event => {
  if (event.tag === 'sync-task') {
    event.waitUntil(
      // 执行同步操作,如发送存储的表单数据
      sendCachedData()
    );
  }
});

11.6 推送通知API

实现服务器推送和通知功能:

javascript
// 订阅推送服务
navigator.serviceWorker.ready.then(registration => {
  return registration.pushManager.subscribe({
    userVisibleOnly: true,  // 推送必须对用户可见
    applicationServerKey: urlBase64ToUint8Array('BEl62iUY...')  // VAPID公钥
  });
})
.then(subscription => {
  // 将订阅信息发送到服务器
  return fetch('/api/save-subscription', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify(subscription)
  });
});

// 在Service Worker中处理推送事件
self.addEventListener('push', event => {
  const data = event.data.json();
  
  event.waitUntil(
    self.registration.showNotification(data.title, {
      body: data.body,
      icon: '/images/icon.png',
      badge: '/images/badge.png',
      data: {url: data.url},
      actions: [
        {action: 'view', title: '查看详情'},
        {action: 'close', title: '关闭'}
      ]
    })
  );
});

// 处理通知点击事件
self.addEventListener('notificationclick', event => {
  event.notification.close();
  
  if (event.action === 'view') {
    event.waitUntil(
      clients.openWindow(event.notification.data.url)
    );
  }
});

11.7 导航预加载API

加速导航请求,在Service Worker启动的同时并行发起网络请求:

javascript
// 在激活事件中启用导航预加载
self.addEventListener('activate', event => {
  event.waitUntil(
    self.registration.navigationPreload.enable()
  );
});

// 在fetch事件中使用预加载响应
self.addEventListener('fetch', event => {
  if (event.request.mode === 'navigate') {
    event.respondWith(async function() {
      try {
        // 尝试使用预加载的响应
        const preloadResponse = await event.preloadResponse;
        if (preloadResponse) {
          return preloadResponse;
        }
        
        // 如果没有预加载响应,回退到正常的网络请求
        return await fetch(event.request);
      } catch (error) {
        // 网络错误,使用缓存
        return caches.match('/offline.html');
      }
    }());
  }
});

11.8 Clients API

管理Service Worker控制的页面:

javascript
// 获取所有受控制的窗口客户端
self.clients.matchAll({type: 'window'})
  .then(clients => {
    clients.forEach(client => {
      console.log('客户端URL:', client.url);
    });
  });

// 聚焦现有窗口或打开新窗口
self.clients.matchAll({type: 'window'})
  .then(clients => {
    for (let client of clients) {
      if (client.url === '/index.html' && 'focus' in client) {
        return client.focus();
      }
    }
    // 如果没有找到匹配的窗口,打开新窗口
    return self.clients.openWindow('/index.html');
  });

// 声明接管所有客户端
self.clients.claim();

11.9 IndexedDB API

Service Worker可以使用IndexedDB进行数据存储:

javascript
// 打开数据库
const dbPromise = indexedDB.open('my-db', 1);

dbPromise.onupgradeneeded = event => {
  const db = event.target.result;
  // 创建对象存储
  const store = db.createObjectStore('offline-data', {keyPath: 'id'});
  store.createIndex('timestamp', 'timestamp');
};

// 存储数据
function saveData(data) {
  return dbPromise.then(db => {
    const tx = db.transaction('offline-data', 'readwrite');
    const store = tx.objectStore('offline-data');
    store.put(data);
    return tx.complete;
  });
}

// 读取数据
function getData(id) {
  return dbPromise.then(db => {
    const tx = db.transaction('offline-data', 'readonly');
    const store = tx.objectStore('offline-data');
    return store.get(id);
  });
}

11.10 工具函数

Service Worker开发中常用的辅助函数:

javascript
// 将Base64编码的VAPID公钥转换为Uint8Array
function urlBase64ToUint8Array(base64String) {
  const padding = '='.repeat((4 - base64String.length % 4) % 4);
  const base64 = (base64String + padding)
    .replace(/\-/g, '+')
    .replace(/_/g, '/');
  
  const rawData = window.atob(base64);
  const outputArray = new Uint8Array(rawData.length);
  
  for (let i = 0; i < rawData.length; ++i) {
    outputArray[i] = rawData.charCodeAt(i);
  }
  return outputArray;
}

// 生成唯一ID
function generateUniqueId() {
  return Date.now().toString(36) + Math.random().toString(36).substr(2, 5);
}

// 格式化网络错误信息
function formatNetworkError(error) {
  return {
    type: 'NetworkError',
    message: error.message,
    timestamp: new Date().toISOString()
  };
}