Service Worker 离线访问实现详解
离线访问是 Service Worker 最核心的功能之一,让 Web 应用在无网络环境下仍能正常工作。
核心原理
Service Worker 作为网络代理,拦截所有 HTTP 请求,根据缓存策略决定从缓存返回还是请求网络。
实现步骤
1. 注册 Service Worker
javascript// main.js if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/sw.js') .then(registration => { console.log('SW registered:', registration.scope); }) .catch(error => { console.log('SW registration failed:', error); }); }); }
2. 预缓存核心资源
javascript// sw.js const CACHE_NAME = 'offline-cache-v1'; const urlsToCache = [ '/', '/index.html', '/styles.css', '/app.js', '/icons/icon-192x192.png', '/offline.html' // 离线 fallback 页面 ]; self.addEventListener('install', event => { event.waitUntil( caches.open(CACHE_NAME) .then(cache => { console.log('Cache opened'); return cache.addAll(urlsToCache); }) .catch(err => console.error('Cache failed:', err)) ); self.skipWaiting(); });
3. 拦截请求并返回缓存
javascriptself.addEventListener('fetch', event => { event.respondWith( caches.match(event.request) .then(response => { // 缓存命中,直接返回 if (response) { return response; } // 缓存未命中,请求网络 return fetch(event.request) .then(networkResponse => { // 动态缓存新资源 if (!networkResponse || networkResponse.status !== 200) { return networkResponse; } const responseToCache = networkResponse.clone(); caches.open(CACHE_NAME).then(cache => { cache.put(event.request, responseToCache); }); return networkResponse; }) .catch(() => { // 网络失败,返回离线页面 if (event.request.mode === 'navigate') { return caches.match('/offline.html'); } }); }) ); });
4. 清理旧缓存
javascriptself.addEventListener('activate', event => { event.waitUntil( caches.keys().then(cacheNames => { return Promise.all( cacheNames .filter(name => name !== CACHE_NAME) .map(name => caches.delete(name)) ); }) ); self.clients.claim(); });
离线页面设计
html<!-- offline.html --> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>离线模式</title> <style> body { display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; font-family: Arial, sans-serif; background: #f5f5f5; } .offline-container { text-align: center; padding: 40px; } .offline-icon { font-size: 64px; margin-bottom: 20px; } h1 { color: #333; } p { color: #666; } button { padding: 10px 20px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; } </style> </head> <body> <div class="offline-container"> <div class="offline-icon">📡</div> <h1>您当前处于离线状态</h1> <p>请检查网络连接后重试</p> <button onclick="location.reload()">重新加载</button> </div> </body> </html>
高级离线策略
1. 网络优先 + 缓存回退
javascriptself.addEventListener('fetch', event => { event.respondWith( fetch(event.request) .then(response => { // 更新缓存 const clone = response.clone(); caches.open(CACHE_NAME).then(cache => { cache.put(event.request, clone); }); return response; }) .catch(() => caches.match(event.request)) ); });
2. 缓存优先 + 后台更新
javascriptself.addEventListener('fetch', event => { event.respondWith( caches.match(event.request).then(response => { const fetchPromise = fetch(event.request).then(networkResponse => { caches.open(CACHE_NAME).then(cache => { cache.put(event.request, networkResponse.clone()); }); return networkResponse; }); return response || fetchPromise; }) ); });
检测网络状态
javascript// 主线程中检测 window.addEventListener('online', () => { console.log('网络已连接'); }); window.addEventListener('offline', () => { console.log('网络已断开'); }); // 检查当前状态 if (navigator.onLine) { console.log('在线'); } else { console.log('离线'); }
最佳实践
- 核心资源优先:确保 HTML、CSS、JS 等核心资源被缓存
- 优雅降级:网络失败时提供友好的离线页面
- 缓存更新:定期更新缓存,避免用户长期看到旧版本
- 缓存清理:及时清理过期缓存,避免存储溢出
- 测试验证:使用 Chrome DevTools 的 Network 面板模拟离线环境