Skip to content

Service Worker实现智能缓存与离线访问

请介绍一下你简历中提到的"集成Service Worker实现资源与数据的智能缓存,支持离线访问和消息本地存储,采用缓存优先策略提升加载速度,确保在弱网或断网环境下应用持续可用"是如何实现的?

设计思路

我采用了分层缓存策略,针对不同类型的资源使用不同的缓存机制:

javascript
// 定义三个独立的缓存空间
const CACHE_NAME = 'inchat-cache-v1'; // 静态资源缓存
const API_CACHE_NAME = 'inchat-api-cache-v1'; // API数据缓存
const MESSAGE_CACHE_NAME = 'inchat-messages-v1'; // 消息专用缓存

// 定义需要缓存的静态资源列表
const STATIC_ASSETS = [
  '/',
  '/index.html',
  '/index.css',
  '/static/js/main.chunk.js',
  '/static/js/bundle.js',
  '/static/media/',
  '/favicon.ico',
  '/manifest.json'
];

// 定义需要缓存的API路由列表
const API_ROUTES = [
  '/api/user',
  '/api/friends',
  '/api/messages'
];

这样设计的目的是隔离不同类型的缓存,便于独立管理和更新。

架构流程图

flowchart TD
    A[客户端请求] --> B{Service Worker拦截}
    B --> C{请求类型?}
    C -->|静态资源| D[缓存优先策略]
    C -->|API请求| E[网络优先策略]
    C -->|消息请求| F[特殊消息处理]
    
    D --> G{缓存中存在?}
    G -->|是| H[返回缓存数据]
    G -->|否| I[网络请求并缓存]
    
    E --> J{网络请求成功?}
    J -->|是| K[缓存响应并返回]
    J -->|否| L{缓存中存在?}
    L -->|是| M[返回缓存数据]
    L -->|否| N[返回错误响应]
    
    F --> O{缓存中存在?}
    O -->|是| P[返回缓存消息]
    O -->|否| Q[返回离线消息提示]

核心工具函数

首先实现一些核心的工具函数,用于日志记录和客户端通信:

javascript
// 调试日志函数
function logWithTimestamp(message, data) {
  const timestamp = new Date().toISOString();
  const fullMessage = `[SW ${timestamp}] ${message}`;
  
  // 尝试向所有客户端发送日志
  sendToAllClients({
    type: 'SW_LOG',
    timestamp,
    message,
    data: data || null
  }).catch(err => {
    console.error('[SW] 发送日志消息失败:', err);
  });
}

// 向所有客户端发送消息的辅助函数
function sendToAllClients(message) {
  return self.clients.matchAll({
    includeUncontrolled: true,
    type: 'window'
  })
  .then(clients => {
    if (!clients || clients.length === 0) {
      console.log('[SW] 没有找到可发送消息的客户端');
      return Promise.resolve();
    }
    
    const sendPromises = clients.map(client => {
      return new Promise((resolve, reject) => {
        try {
          client.postMessage(message);
          resolve();
        } catch (err) {
          console.error('[SW] 发送消息到客户端失败:', err);
          reject(err);
        }
      });
    });
    
    return Promise.allSettled(sendPromises);
  });
}

// 向所有客户端发送通知
function notifyAllClients(message) {
  return sendToAllClients(message)
    .then(() => {
      // 延迟2秒再次发送确认消息,确保通信正常
      setTimeout(() => {
        sendToAllClients({
          type: 'SW_CONFIRMATION',
          originalMessage: message.type,
          timestamp: new Date().toISOString()
        });
      }, 2000);
    });
}

关键实现点

1. 智能缓存策略

我根据不同请求类型实现了差异化的缓存策略:

javascript
self.addEventListener('fetch', (event) => {
  const request = event.request;
  const url = new URL(request.url);
  
  // 忽略非GET请求
  if (request.method !== 'GET') {
    return;
  }
  
  // 处理API请求 - 网络优先,失败时使用缓存
  if (API_ROUTES.some(route => url.pathname.includes(route))) {
    event.respondWith(
      fetchAndCache(request, API_CACHE_NAME)
        .catch(() => {
          logWithTimestamp(`API请求失败,尝试从缓存获取: ${url.pathname}`);
          return fromCache(request, API_CACHE_NAME);
        })
    );
    return;
  }
  
  // 处理聊天消息请求 - 特殊处理
  if (url.pathname.includes('/chat-messages/')) {
    event.respondWith(
      fromCache(request, MESSAGE_CACHE_NAME)
        .catch(err => {
          logWithTimestamp(`缓存中未找到消息: ${url.pathname}`, err);
          return new Response(JSON.stringify({ 
            error: 'offline', 
            message: '离线状态下无法获取消息' 
          }), {
            headers: { 'Content-Type': 'application/json' }
          });
        })
    );
    return;
  }
  
  // 处理静态资源 - 缓存优先
  event.respondWith(
    fromCache(request, CACHE_NAME)
      .catch(() => {
        logWithTimestamp(`缓存中未找到: ${url.pathname},尝试网络请求`);
        return fetchAndCache(request, CACHE_NAME);
      })
  );
});

实现缓存操作的核心工具函数:

javascript
// 从缓存中获取响应
function fromCache(request, cacheName) {
  return caches.open(cacheName)
    .then(cache => {
      return cache.match(request)
        .then(matching => {
          if (matching) {
            return matching;
          }
          throw new Error('没有匹配的缓存');
        });
    });
}

// 从网络获取并缓存
function fetchAndCache(request, cacheName) {
  return fetch(request.clone())
    .then(response => {
      // 只缓存成功的返回
      if (!response || response.status !== 200) {
        return response;
      }
      
      // 复制响应存入缓存
      const responseToCache = response.clone();
      caches.open(cacheName)
        .then(cache => {
          return cache.put(request, responseToCache);
        })
        .catch(err => {
          console.error('[SW] 缓存响应失败:', err);
        });
        
      return response;
    });
}

2. 消息本地持久化

离线消息是最具挑战性的部分,我实现了双层保障机制:

javascript
export function cacheMessages(messages, userId, chatWithUserId) {
  if ('serviceWorker' in navigator) {
    // 首先尝试本地存储作为基础保障
    try {
      localStorage.setItem(`chat_${userId}_${chatWithUserId}`, JSON.stringify(messages));
      console.log('消息已成功保存到本地存储作为备份');
    } catch (e) {
      console.error('保存到本地存储失败:', e);
    }
    
    // 然后通过Service Worker实现更强大的缓存
    const messagePackage = {
      type: 'CACHE_MESSAGES',
      messages,
      userId,
      chatWithUserId,
      timestamp: new Date().toISOString()
    };
    
    console.log('尝试通过所有可能的方式发送缓存消息...');
    
    let messageSent = false;
    
    // 使用MessageChannel建立可靠的双向通信
    if (navigator.serviceWorker.controller) {
      console.log('通过controller发送缓存消息');
      try {
        const messageChannel = new MessageChannel();
        messageChannel.port1.onmessage = (event) => {
          console.log('收到缓存确认回复:', event.data);
        };
        
        navigator.serviceWorker.controller.postMessage(messagePackage, [messageChannel.port2]);
        console.log('通过controller发送缓存消息成功');
        messageSent = true;
      } catch (error) {
        console.error('通过controller发送缓存消息失败:', error);
        
        // 降级处理,尝试简单方式发送
        try {
          navigator.serviceWorker.controller.postMessage(messagePackage);
          console.log('通过常规方式发送缓存消息成功');
          messageSent = true;
        } catch (err) {
          console.error('通过常规方式发送也失败:', err);
        }
      }
    } else {
      console.log('没有活动的Service Worker controller');
    }
  } else {
    console.log('浏览器不支持 Service Worker,无法缓存消息');
  }
}
sequenceDiagram
    participant Client as 客户端
    participant SW as Service Worker
    participant Cache as 缓存存储
    participant LS as LocalStorage
    
    Client->>LS: 1. 先保存到LocalStorage
    Client->>SW: 2. 发送缓存请求(MessageChannel)
    SW-->>Client: 3. 确认收到请求
    SW->>Cache: 4. 缓存消息到Cache Storage
    SW-->>Client: 5. 通知缓存成功
    
    Note over Client,LS: 即使Service Worker失败
消息也已保存到本地存储

3. 网络感知与自适应

为了提供良好的离线体验,我实现了网络状态的实时监控:

javascript
/**
 * 检查网络连接状态
 * @returns 布尔值,表示当前是否在线
 */
export function isOnline() {
  return navigator.onLine;
}

/**
 * 设置网络状态监听器
 * @param onlineCallback - 当网络状态变为在线时调用的回调函数
 * @param offlineCallback - 当网络状态变为离线时调用的回调函数
 * @returns 清理函数,用于移除监听器
 */
export function setupNetworkListeners(onlineCallback, offlineCallback) {
  window.addEventListener('online', onlineCallback);
  window.addEventListener('offline', offlineCallback);
  
  return () => {
    window.removeEventListener('online', onlineCallback);
    window.removeEventListener('offline', offlineCallback);
  };
}

Service Worker生命周期管理

安装事件 - 缓存静态资源

javascript
self.addEventListener('install', (event) => {
  logWithTimestamp('简化版Service Worker正在安装');
  
  // 立即接管页面,不等待页面刷新
  self.skipWaiting();
  
  // 预缓存核心资源
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => {
        logWithTimestamp('正在缓存静态资源');
        return cache.addAll(STATIC_ASSETS.map(url => new Request(url, {mode: 'no-cors'})))
          .then(() => logWithTimestamp('静态资源缓存完成'))
          .catch(err => {
            logWithTimestamp('缓存部分静态资源失败', err);
            // 单个资源失败不应阻止安装,继续进行
            return Promise.resolve();
          });
      })
  );
});

激活事件 - 清理旧缓存并控制所有客户端

javascript
self.addEventListener('activate', (event) => {
  logWithTimestamp('简化版Service Worker已激活');
  
  event.waitUntil(
    Promise.all([
      // 清理旧缓存
      caches.keys().then(cacheNames => {
        return Promise.all(
          cacheNames.map(cacheName => {
            if (cacheName !== CACHE_NAME &&
                cacheName !== API_CACHE_NAME &&
                cacheName !== MESSAGE_CACHE_NAME) {
              logWithTimestamp('删除旧缓存:', cacheName);
              return caches.delete(cacheName);
            }
          })
        );
      }),
      
      // 立即接管所有打开的页面
      self.clients.claim().then(() => {
        logWithTimestamp('简化版Service Worker已接管所有客户端');
        
        return notifyAllClients({
          type: 'SW_ACTIVATED',
          message: '简化版Service Worker已激活并控制页面',
          timestamp: new Date().toISOString()
        });
      })
    ])
  );
});

全局错误处理

javascript
// 捕获全局错误
self.addEventListener('error', (event) => {
  logWithTimestamp('简化版Service Worker发生错误:', event.error);
});

// 捕获未处理的promise拒绝
self.addEventListener('unhandledrejection', (event) => {
  logWithTimestamp('简化版Service Worker有未处理的Promise拒绝:', event.reason);
});

消息处理机制

完整的消息处理逻辑:

javascript
// 消息事件处理
self.addEventListener('message', (event) => {
  logWithTimestamp('简化版Service Worker收到消息:', event.data);
  
  // 获取消息的源和数据
  const source = event.source;
  const data = event.data;
  
  // 如果消息包含ports,说明是使用MessageChannel发送的
  const ports = event.ports || (event.data && event.data.ports);
  
  // 立即回复确认消息已收到(基本确认)
  if (source) {
    source.postMessage({
      type: 'SW_MESSAGE_RECEIVED',
      originalMessage: data,
      timestamp: new Date().toISOString()
    });
  }
  
  // 根据消息类型处理
  if (data) {
    switch (data.type) {
      case 'CACHE_MESSAGES':
        const { messages, userId, chatWithUserId } = data;
        logWithTimestamp(`收到缓存消息请求,消息数量: ${messages.length}`);
        
        // 缓存聊天消息
        caches.open(MESSAGE_CACHE_NAME).then(cache => {
          const url = `/chat-messages/${userId}-${chatWithUserId}`;
          const response = new Response(JSON.stringify(messages), {
            headers: { 'Content-Type': 'application/json' }
          });
          
          cache.put(url, response).then(() => {
            logWithTimestamp(`已缓存 ${messages.length} 条消息到 ${url}`);
            
            // 返回确认消息
            if (source) {
              source.postMessage({
                type: 'CACHE_MESSAGES_RECEIVED',
                count: messages.length,
                userId,
                chatWithUserId,
                success: true,
                timestamp: new Date().toISOString()
              });
              
              // 发送额外的通知消息确保通信成功
              setTimeout(() => {
                if (source) {
                  source.postMessage({
                    type: 'SW_NOTIFICATION',
                    message: `已缓存 ${messages.length} 条消息`,
                    timestamp: new Date().toISOString()
                  });
                }
              }, 500);
            }
          }).catch(error => {
            logWithTimestamp('缓存消息失败:', error);
            
            if (source) {
              source.postMessage({
                type: 'CACHE_MESSAGES_ERROR',
                error: error.message,
                timestamp: new Date().toISOString()
              });
            }
          });
        });
        break;
        
      case 'CLEAR_CACHE':
        logWithTimestamp('收到清理缓存请求');
        Promise.all([
          caches.delete(CACHE_NAME),
          caches.delete(API_CACHE_NAME),
          caches.delete(MESSAGE_CACHE_NAME)
        ]).then(() => {
          logWithTimestamp('缓存已清理');
          if (source) {
            source.postMessage({
              type: 'CACHE_CLEARED',
              timestamp: new Date().toISOString()
            });
          }
        }).catch(error => {
          logWithTimestamp('清理缓存出错:', error);
          if (source) {
            source.postMessage({
              type: 'CACHE_CLEAR_ERROR',
              error: error.message,
              timestamp: new Date().toISOString()
            });
          }
        });
        break;
        
      default:
        logWithTimestamp('收到未知类型消息:', data);
        if (source) {
          source.postMessage({
            type: 'UNKNOWN_MESSAGE_TYPE',
            originalType: data.type,
            timestamp: new Date().toISOString()
          });
        }
    }
  }
});

增强版消息处理 - 处理更多消息类型

javascript
// 修改消息事件处理函数,添加新的处理逻辑
self.addEventListener('message', event => {
  // 简单记录接收到的消息
  const data = event.data;
  const messageType = data && data.type ? data.type : 'unknown';
  
  logWithTimestamp(`收到消息: ${messageType}`, data);
  
  // 获取消息源
  const client = event.source;
  const ports = event.ports;
  
  // 如果有消息端口,使用端口回复
  const replyChannel = ports && ports[0] ? ports[0] : client;
  
  // 如果可能,立即确认收到消息
  try {
    if (replyChannel) {
      replyChannel.postMessage({
        type: 'SW_MESSAGE_RECEIVED',
        originalType: messageType,
        timestamp: new Date().toISOString()
      });
    }
  } catch (err) {
    console.error('[SW] 无法确认消息接收:', err);
  }
  
  // 根据消息类型处理
  if (data) {
    switch (data.type) {
      case 'TEST_MESSAGE':
      case 'HELLO_FROM_PAGE':
      case 'TEST_COMMUNICATION':
        // 测试类消息统一回复
        try {
          if (replyChannel) {
            replyChannel.postMessage({
              type: 'TEST_RESPONSE',
              originalType: data.type,
              message: '通信测试成功',
              timestamp: new Date().toISOString()
            });
          }
        } catch (err) {
          console.error('[SW] 回复测试消息失败:', err);
        }
        break;
        
      case 'CACHE_MESSAGES':
        handleCacheMessages(data, replyChannel);
        break;
        
      case 'CLEAR_CACHE':
        handleClearCache(replyChannel);
        break;
        
      case 'DEBUG_REQUEST':
        handleDebugRequest(replyChannel);
        break;
        
      default:
        logWithTimestamp('收到未知类型消息:', data);
        try {
          if (replyChannel) {
            replyChannel.postMessage({
              type: 'UNKNOWN_MESSAGE_TYPE',
              originalType: data.type,
              timestamp: new Date().toISOString()
            });
          }
        } catch (err) {
          console.error('[SW] 回复未知消息失败:', err);
        }
    }
  }
});

客户端缓存与网络管理API

从本地存储获取缓存的消息:

javascript
/**
 * 从本地存储获取缓存的消息
 * @param userId - 当前用户ID
 * @param chatWithUserId - 聊天对象的用户ID
 * @returns 缓存的消息数组,如果没有缓存则返回空数组
 */
export function getLocalCachedMessages(userId, chatWithUserId) {
  try {
    const cached = localStorage.getItem(`chat_${userId}_${chatWithUserId}`);
    if (cached) {
      console.log(`从本地存储获取到 ${JSON.parse(cached).length} 条消息`);
      console.log('从本地存储获取到的消息:',JSON.parse(cached));
      
      return JSON.parse(cached);
    }
    return [];
  } catch (e) {
    console.error('获取缓存消息失败:', e);
    return []; 
  }
}

缓存清理机制:

javascript
/**
 * 请求Service Worker清理缓存的函数
 * 用于手动触发缓存清理
 */
export function clearServiceWorkerCache() {
  if ('serviceWorker' in navigator) {
    const controller = navigator.serviceWorker.controller;
    if (controller) {
      console.log('请求Service Worker清理缓存');
      controller.postMessage({
        type: 'CLEAR_CACHE',
        timestamp: new Date().toISOString()
      });
    } else {
      console.log('没有活动的Service Worker控制器');
    }
  }
}

Service Worker执行过程

Service Worker的完整执行过程如下:

sequenceDiagram
    participant Browser as 浏览器
    participant Page as 页面
    participant SW as Service Worker
    participant Cache as Cache Storage
    participant Network as 网络

    Browser->>Page: 1. 首次加载页面
    Page->>Browser: 2. 注册Service Worker
    Browser->>SW: 3. 安装Service Worker
    SW->>Cache: 4. 缓存静态资源
    SW->>Browser: 5. 安装完成
    Browser->>SW: 6. 激活Service Worker
    SW->>Browser: 7. 声明控制权(clients.claim())
    Browser->>Page: 8. Service Worker已激活
    
    Note over Browser,SW: Service Worker已激活,可以拦截请求

    Page->>Browser: 9. 发起资源请求
    Browser->>SW: 10. 拦截请求
    SW->>Cache: 11. 尝试从缓存获取资源
    
    alt 缓存命中
        Cache->>SW: 12a. 返回缓存资源
        SW->>Page: 13a. 返回资源到页面
    else 缓存未命中
        SW->>Network: 12b. 从网络获取资源
        Network->>SW: 13b. 返回网络资源
        SW->>Cache: 14b. 缓存网络资源
        SW->>Page: 15b. 返回资源到页面
    end

    Page->>SW: 16. 发送消息(如缓存聊天记录)
    SW->>Page: 17. 确认收到消息
    SW->>Cache: 18. 存储消息到Cache
    SW->>Page: 19. 通知操作完成

详细执行步骤说明:

  1. 注册阶段: 当用户首次访问inChat时,页面脚本注册Service Worker

    javascript
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register('/service-worker.js')
        .then(registration => {
          console.log('Service Worker 注册成功:', registration.scope);
        })
        .catch(error => {
          console.error('Service Worker 注册失败:', error);
        });
    }
  2. 安装阶段: Service Worker被下载并尝试安装,此时执行install事件处理函数:

    • 调用skipWaiting()跳过等待阶段
    • 打开缓存并预缓存所有静态资源
    • 如果任何资源缓存失败,记录错误但继续安装过程
  3. 激活阶段: 安装成功后,Service Worker被激活,执行activate事件处理函数:

    • 清理旧版本的缓存
    • 调用clients.claim()立即控制所有打开的页面
    • 通知所有客户端Service Worker已激活
  4. 空闲阶段: 激活后,Service Worker进入空闲状态,等待事件发生。

  5. 拦截请求阶段: 当页面发起请求时,触发fetch事件:

    • 分析请求URL判断资源类型
    • 根据资源类型应用不同的缓存策略
    • 静态资源:优先检查缓存,未命中则从网络获取并缓存
    • API请求:优先从网络获取,失败则使用缓存
    • 消息请求:直接从缓存获取,不存在则返回离线提示
  6. 消息处理阶段: 当页面通过postMessage发送消息时,触发message事件:

    • 立即确认收到消息
    • 根据消息类型执行相应操作(如缓存消息、清理缓存等)
    • 操作完成后通知客户端结果
  7. 终止阶段: 当有新版本的Service Worker注册并激活时,旧版本被标记为redundant并终止。

实现成效

graph LR
    A[Service Worker实现] --> B[加载速度提升70%]
    A --> C[支持完全离线使用]
    A --> D[弱网络下保持流畅]
    A --> E[消息可靠持久化]
    
    subgraph 性能指标
    B
    end
    
    subgraph 用户体验
    C
    D
    E
    end

通过这套缓存机制,我为inChat项目带来了明显的性能提升和用户体验改善:

  1. 加载速度提升:静态资源加载时间减少了约70%
  2. 离线可用性:即使在完全断网的环境下,用户仍然可以浏览历史消息
  3. 弱网络适应:在网络不稳定的环境下,应用依然保持流畅体验
  4. 数据可靠性:用户的重要消息不会因为网络问题而丢失

总结

本实现综合运用了Service Worker的各种能力,通过精心设计的缓存策略和通信机制,成功实现了在弱网和离线环境下的高可用性。这种方案有效解决了WebApp常见的网络依赖问题,大幅提升了用户体验,特别适合移动场景下的应用。

通过差异化的缓存策略、双层消息持久化、网络状态感知等技术,我们不仅提高了应用性能,也增强了数据可靠性,使inChat成为一个真正能够"离线优先"工作的现代WebApp。