作为一名前端开发者,我深知现代Web应用面临的最大挑战之一就是网络不稳定性。想象一下:用户在地铁里打开你的应用,突然进入隧道失去信号,页面直接变成空白——这种体验有多糟糕?Service Worker技术正是为了解决这个问题而生的。
Service Worker本质上是一个运行在浏览器后台的JavaScript线程,它能拦截和处理网络请求,实现精细的缓存控制。与传统的浏览器缓存不同,它提供了完全程序化的控制能力,让我们可以创建真正可靠的离线体验。我在多个生产项目中实践过这项技术,今天就把最实用的经验分享给你。
重要提示:Service Worker必须在HTTPS环境下运行(localhost除外),这是出于安全考虑的设计限制。
Service Worker有几个关键特性需要理解:
在实际项目中,我通常会把Service Worker的缓存分为两类:
这种分类管理能显著提高缓存效率。下面我们来看具体实现。
一个典型的离线应用目录结构如下:
code复制offline-app/
├── index.html
├── app.js # 主应用逻辑
├── sw.js # Service Worker脚本
├── styles.css
├── manifest.json
└── assets/
├── icon-192.png
└── fallback-image.png
注册Service Worker的代码要放在主JavaScript文件中:
javascript复制// app.js
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
const registration = await navigator.serviceWorker.register('/sw.js', {
scope: '/'
});
console.log('注册成功:', registration);
} catch (error) {
console.error('注册失败:', error);
}
});
}
我在实际项目中发现几个常见问题:
下面是sw.js的基础框架:
javascript复制// 缓存名称和版本
const CACHE_NAME = 'app-v1';
const PRE_CACHE_URLS = [
'/',
'/styles.css',
'/app.js'
];
// 安装阶段 - 预缓存关键资源
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(PRE_CACHE_URLS))
.then(() => self.skipWaiting())
);
});
// 激活阶段 - 清理旧缓存
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(name => {
if (name !== CACHE_NAME) {
return caches.delete(name);
}
})
);
}).then(() => self.clients.claim())
);
});
// 拦截请求
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => response || fetch(event.request))
);
});
这个基础版本已经能实现离线访问,但还不够智能。接下来我们深入优化。
根据资源类型,我们需要不同的缓存策略:
javascript复制async function cacheFirst(request) {
const cached = await caches.match(request);
if (cached) return cached;
try {
const response = await fetch(request);
const cache = await caches.open(CACHE_NAME);
cache.put(request, response.clone());
return response;
} catch (error) {
// 对于图片返回fallback
if (request.url.match(/\.(png|jpg)$/)) {
return caches.match('/assets/fallback-image.png');
}
throw error;
}
}
javascript复制async function networkFirst(request) {
try {
const response = await fetch(request);
const cache = await caches.open('api-cache');
cache.put(request, response.clone());
return response;
} catch (error) {
const cached = await caches.match(request);
if (cached) return cached;
return new Response(JSON.stringify({error: '离线数据不可用'}), {
headers: {'Content-Type': 'application/json'}
});
}
}
javascript复制async function staleWhileRevalidate(request) {
const cache = await caches.open(CACHE_NAME);
const cached = await cache.match(request);
const fetchPromise = fetch(request).then(response => {
cache.put(request, response.clone());
return response;
});
return cached || fetchPromise;
}
随着应用运行,缓存会不断增长,需要合理管理:
javascript复制class CacheManager {
constructor() {
this.maxSize = 50 * 1024 * 1024; // 50MB
}
async cleanOldCaches() {
const cache = await caches.open(CACHE_NAME);
const keys = await cache.keys();
let totalSize = 0;
// 计算当前缓存大小
for (const request of keys) {
const response = await cache.match(request);
const size = response.headers.get('content-length');
totalSize += parseInt(size || '0');
}
// 如果超过限制,清理最旧的缓存
if (totalSize > this.maxSize) {
const sorted = keys.sort((a, b) => {
return new Date(a.headers.get('date')) -
new Date(b.headers.get('date'));
});
for (const request of sorted) {
await cache.delete(request);
// 重新计算大小...
if (totalSize <= this.maxSize) break;
}
}
}
}
问题1:缓存不更新
问题2:旧版本缓存残留
app-v2问题3:内存泄漏
javascript复制// 在install阶段预加载
const PRE_CACHE_URLS = [
'/critical.css',
'/main.js',
'/homepage-layout.html'
];
javascript复制// 主线程中动态加载
window.addEventListener('load', () => {
import('./non-critical.js');
});
javascript复制function getStrategy(request) {
const url = new URL(request.url);
if (url.pathname.startsWith('/api/')) {
return networkFirst;
}
if (url.pathname.endsWith('.html')) {
return staleWhileRevalidate;
}
return cacheFirst;
}
Application > Service Workers:
Network面板:
Cache Storage:
网络节流测试:
离线到在线切换:
版本更新测试:
javascript复制// 主线程中注册同步
navigator.serviceWorker.ready.then(registration => {
registration.sync.register('sync-data');
});
// Service Worker中处理
self.addEventListener('sync', event => {
if (event.tag === 'sync-data') {
event.waitUntil(syncPendingData());
}
});
async function syncPendingData() {
const pending = await getPendingData();
for (const item of pending) {
await fetch('/api/save', {
method: 'POST',
body: JSON.stringify(item)
});
}
}
javascript复制// 注册推送
Notification.requestPermission().then(permission => {
if (permission === 'granted') {
subscribeToPush();
}
});
// Service Worker处理推送
self.addEventListener('push', event => {
const data = event.data.json();
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.message,
icon: '/assets/icon.png'
})
);
});
HTTPS强制要求:
缓存头设置:
Cache-Control: no-store渐进式增强:
监控与统计:
javascript复制// 监控示例
self.addEventListener('fetch', event => {
const start = Date.now();
event.respondWith(
handleRequest(event.request).then(response => {
const duration = Date.now() - start;
reportMetrics({
url: event.request.url,
duration,
cached: !!response.fromCache
});
return response;
})
);
});
通过以上方案,我们可以在项目中实现完善的离线功能。记住几个关键点:
Service Worker的学习曲线确实有点陡峭,但一旦掌握,你就能打造出媲美原生应用的Web体验。我在实际项目中应用这些技术后,用户留存率提升了30%,特别是在网络条件较差的地区效果尤为明显。