Service Worker实现智能缓存与离线访问
请介绍一下你简历中提到的"集成Service Worker实现资源与数据的智能缓存,支持离线访问和消息本地存储,采用缓存优先策略提升加载速度,确保在弱网或断网环境下应用持续可用"是如何实现的?
设计思路
我采用了分层缓存策略,针对不同类型的资源使用不同的缓存机制:
// 定义三个独立的缓存空间
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[返回离线消息提示]
核心工具函数
首先实现一些核心的工具函数,用于日志记录和客户端通信:
// 调试日志函数
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. 智能缓存策略
我根据不同请求类型实现了差异化的缓存策略:
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);
})
);
});
实现缓存操作的核心工具函数:
// 从缓存中获取响应
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. 消息本地持久化
离线消息是最具挑战性的部分,我实现了双层保障机制:
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. 网络感知与自适应
为了提供良好的离线体验,我实现了网络状态的实时监控:
/**
* 检查网络连接状态
* @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生命周期管理
安装事件 - 缓存静态资源
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();
});
})
);
});
激活事件 - 清理旧缓存并控制所有客户端
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()
});
})
])
);
});
全局错误处理
// 捕获全局错误
self.addEventListener('error', (event) => {
logWithTimestamp('简化版Service Worker发生错误:', event.error);
});
// 捕获未处理的promise拒绝
self.addEventListener('unhandledrejection', (event) => {
logWithTimestamp('简化版Service Worker有未处理的Promise拒绝:', event.reason);
});
消息处理机制
完整的消息处理逻辑:
// 消息事件处理
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()
});
}
}
}
});
增强版消息处理 - 处理更多消息类型
// 修改消息事件处理函数,添加新的处理逻辑
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
从本地存储获取缓存的消息:
/**
* 从本地存储获取缓存的消息
* @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 [];
}
}
缓存清理机制:
/**
* 请求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. 通知操作完成
详细执行步骤说明:
注册阶段: 当用户首次访问inChat时,页面脚本注册Service Worker
javascriptif ('serviceWorker' in navigator) { navigator.serviceWorker.register('/service-worker.js') .then(registration => { console.log('Service Worker 注册成功:', registration.scope); }) .catch(error => { console.error('Service Worker 注册失败:', error); }); }
安装阶段: Service Worker被下载并尝试安装,此时执行
install
事件处理函数:- 调用
skipWaiting()
跳过等待阶段 - 打开缓存并预缓存所有静态资源
- 如果任何资源缓存失败,记录错误但继续安装过程
- 调用
激活阶段: 安装成功后,Service Worker被激活,执行
activate
事件处理函数:- 清理旧版本的缓存
- 调用
clients.claim()
立即控制所有打开的页面 - 通知所有客户端Service Worker已激活
空闲阶段: 激活后,Service Worker进入空闲状态,等待事件发生。
拦截请求阶段: 当页面发起请求时,触发
fetch
事件:- 分析请求URL判断资源类型
- 根据资源类型应用不同的缓存策略
- 静态资源:优先检查缓存,未命中则从网络获取并缓存
- API请求:优先从网络获取,失败则使用缓存
- 消息请求:直接从缓存获取,不存在则返回离线提示
消息处理阶段: 当页面通过
postMessage
发送消息时,触发message
事件:- 立即确认收到消息
- 根据消息类型执行相应操作(如缓存消息、清理缓存等)
- 操作完成后通知客户端结果
终止阶段: 当有新版本的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项目带来了明显的性能提升和用户体验改善:
- 加载速度提升:静态资源加载时间减少了约70%
- 离线可用性:即使在完全断网的环境下,用户仍然可以浏览历史消息
- 弱网络适应:在网络不稳定的环境下,应用依然保持流畅体验
- 数据可靠性:用户的重要消息不会因为网络问题而丢失
总结
本实现综合运用了Service Worker的各种能力,通过精心设计的缓存策略和通信机制,成功实现了在弱网和离线环境下的高可用性。这种方案有效解决了WebApp常见的网络依赖问题,大幅提升了用户体验,特别适合移动场景下的应用。
通过差异化的缓存策略、双层消息持久化、网络状态感知等技术,我们不仅提高了应用性能,也增强了数据可靠性,使inChat成为一个真正能够"离线优先"工作的现代WebApp。