Service Worker离线缓存实战踩坑总结
这次移动端项目真让人头疼
最近做了个移动端项目,用户量还行,但加载速度一直被吐槽。用户反馈最多的就是打开慢、网络不好时直接白屏。之前用过一些缓存方案,但都不够彻底。后来想了个办法,干脆上Service Worker,搞离线缓存,这样至少能保证基础页面能访问。
开始动手才发现问题一堆
想法挺好,但真正实现的时候发现不是那么回事。首先是兼容性问题,iOS Safari对SW的支持一直有问题,Android的Chrome倒是没问题。还有就是调试太麻烦,不像普通JS那样容易看到错误信息。
先说注册部分吧,这个看似简单其实挺讲究的:
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('SW registered: ', registration);
})
.catch(registrationError => {
console.log('SW registration failed: ', registrationError);
});
});
}
这里有个坑,SW文件必须跟主域名同源,不能跨域。而且路径层级也有讲究,注册位置决定了它的作用域范围。
缓存策略折腾得我够呛
缓存这块是最花时间的。一开始我用的是Network First策略,想着网络好的时候优先加载最新资源,不好时再用缓存。结果发现用户体验反而不好,因为每次都要等网络请求超时才显示缓存,用户感觉很慢。
后来改成Cache First,关键资源先从缓存拿:
self.addEventListener('fetch', event => {
if (event.request.url.includes('/api/')) {
// API请求用网络优先
event.respondWith(
fetch(event.request).catch(() => {
return caches.match(event.request);
})
);
} else if (event.request.destination === 'document') {
// 页面用缓存优先
event.respondWith(
caches.match(event.request).then(response => {
return response || fetch(event.request);
})
);
}
});
这里踩了一个坑:缓存更新的问题。用户访问旧版本缓存后,即使服务器更新了,用户可能还是看不到新内容。最后用了版本控制的方式:
const CACHE_NAME = 'app-v1.2.3';
const urlsToCache = [
'/',
'/styles/main.css',
'/scripts/bundle.js',
'/images/logo.png'
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(urlsToCache))
);
});
// 激活时清理旧缓存
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheName !== CACHE_NAME) {
return caches.delete(cacheName);
}
})
);
})
);
});
iOS的坑真是深不见底
Safari对Service Worker的支持真的是一言难尽。最开始在iOS上完全不起作用,查了好多资料才发现是Safari在隐私模式下不支持SW。而且iOS 11.3以下版本压根就不支持SW,这个得提前判断。
我还专门写了兼容性检测:
function isSWSupported() {
return 'serviceWorker' in navigator &&
!isIOS() &&
!window.isSecureContext;
}
function isIOS() {
return /iPad|iPhone|iPod/.test(navigator.userAgent) &&
!window.MSStream;
}
不过说实话,这部分处理得还不够完美,iOS低版本还是有兼容问题,但影响用户不多,就没深入解决了。
动态缓存的处理也费了不少心思
静态资源还好说,动态数据的缓存比较麻烦。比如用户个人信息、列表数据这些,不能缓存太久,但也不能每次都去请求。
我的解决方案是给API响应加个时间戳,缓存超过5分钟就失效:
self.addEventListener('fetch', event => {
const { request } = event;
if (request.url.includes('/api/user') ||
request.url.includes('/api/list')) {
event.respondWith(
caches.match(request).then(response => {
const now = Date.now();
if (response) {
const cachedTime = response.headers.get('x-cached-time');
if (cachedTime && (now - parseInt(cachedTime)) < 5 * 60 * 1000) {
return response;
}
}
return fetch(request).then(networkResponse => {
const clonedResponse = networkResponse.clone();
caches.open('api-cache').then(cache => {
const modifiedResponse = new Response(clonedResponse.body, {
status: networkResponse.status,
statusText: networkResponse.statusText,
headers: {
...networkResponse.headers,
'x-cached-time': now.toString()
}
});
cache.put(request, modifiedResponse);
});
return networkResponse;
});
})
);
}
});
这块代码写得有点复杂,但效果还行。唯一的不足是缓存大小控制不够精细,长时间使用可能会占用过多存储空间。
最后的效果还算满意
上线后确实有改善,特别是网络不好的情况下,用户至少能看到页面框架,体验好了不少。Lighthouse评分也从60多提升到80多,算是个不小的提升。
不过也有一些问题没完全解决,比如缓存更新机制还不够智能,有时候需要用户手动刷新才能看到最新内容。还有就是iOS上的兼容性问题,虽然影响面不大,但确实存在。
回头看还是有些遗憾
如果重来做的话,我会更早考虑SW的集成,而不是后面加上去。前期规划好缓存策略,后期维护会容易很多。还有就是要做好降级处理,毕竟不是所有设备都支持SW。
总体来说这次尝试是有价值的,虽然过程曲折,但确实提升了用户体验。SW这个技术还是很不错的,只是需要花时间去踩坑。
结语
以上是我这次移动端项目用Service Worker的完整经验,踩过的坑、走过的弯路都分享出来了。如果你也在做类似的功能,希望能帮你少走些弯路。这个方案肯定还有优化空间,有更好的实现方式欢迎评论区交流。

暂无评论