浏览器的缓存是把双刃剑,使用得当的话,可以加快页面的加载速度,减少向服务器的请求次数,进而减少带宽和服务器压力。但是如果使用不得当的话,会造成新功能或者问题修复未能正常生效等问题,用户体验有所下降。常见的缓存方式包括强制缓存和协商缓存,分别用在不同情境下,通过相关的Headers控制(感兴趣的可以看之前HTTP Headers文章中关于缓存的介绍),也是大多数网站的首要技术选择。那么还有什么其他缓存相关的技术可以使用吗?让我们来关注一下Service Worker。
快速入门
Service Worker旨在通过代码精确控制缓存文件和HTTP请求,是已经被废弃掉的AppCache技术的替代方案。Service Worker有相关的生命周期概念,如下所示:
实际案例
我们来看一下语雀是如何使用Service Worker来缓存相关内容的。
注册Service Worker
ServiceWorkerContainer在navigator下,所以先判断是否支持Service Worker,不支持的话另做处理
if ('serviceWorker' in navigator) {
// Register a service worker hosted at the root of the
// site using the default scope.
navigator.serviceWorker.register('/serviceworker.js').then(function(registration) {
console.log('Service worker registration succeeded:', registration);
}, /*catch*/ function(error) {
console.log('Service worker registration failed:', error);
});
} else {
console.log('Service workers are not supported.');
}
ServiceWorkerGlobalScope逻辑处理
在Worker内不存在全局变量window取而代之的是self,这里注册了两个Scope变量assets和resourceBase,并用importScripts引入新的逻辑处理
self.assets = [
'https://gw.alipayobjects.com/os/chair-script/skylark/common.9795d8b0.chunk.css',
'https://gw.alipayobjects.com/os/lib/??react/16.13.1/umd/react.production.min.js,react-dom/16.13.1/umd/react-dom.production.min.js,react-dom/16.13.1/umd/react-dom-server.browser.production.min.js,moment/2.24.0/min/moment.min.js',
];
self.resourceBase = 'https://gw.alipayobjects.com/os/chair-script/skylark/';
importScripts(
'https://gw.alipayobjects.com/os/chair-script/skylark/serviceworker.d050b459.js'
);
值得注意的是语雀把react,react-dom,moment等不常变更的依赖放在了Service Worker内,实现了另一种形式的app vendor chunk
处理缓存
主要逻辑如下所示:
self.addEventListener("install", (e => {
Array.isArray(self.assets) && e.waitUntil(caches.open("v1").then((e => {
e.addAll(self.assets)
})))
})), self.addEventListener("activate", (e => {
Array.isArray(self.assets) && caches.open("v1").then((e => {
e.keys().then((t => {
t.forEach((t => {
self.assets.includes(t.url) || e.delete(t)
}))
}))
}))
}));
const r = [self.resourceBase, "https://at.alicdn.com/t/", "https://gw.alipayobjects.com/os/"];
self.addEventListener("fetch", (e => {
r.some((t => e.request.url.startsWith(t))) && e.respondWith(caches.match(e.request).then((t =>
t && 200 === t.status ? t : fetch(e.request).then((t => {
if (200 !== t.status) return t;
const r = t.clone();
return caches.open("v1").then((t => {
t.put(e.request, r)
})), t
})).catch((() => {})))))
}))
这里实现了如下逻辑:
- 在install阶段调用
cache.addAll()将self.assets内的文件缓存 - 在active阶段将新旧的
self.assets进行对比,并将失效的缓存删除掉:self.assets.includes(t.url) || e.delete(t) - 注册
fetch事件监听,如果请求url在[self.resourceBase, "https://at.alicdn.com/t/", "https://gw.alipayobjects.com/os/"]内则从缓存内拿相应的文件
可以看出语雀使用Service Worker进行缓存,整体的逻辑简单明了,并没有什么复杂高深的内容在里面。
其它应用
我们从语雀处理缓存逻辑时的fetch事件监听可以看出来,fetch可以做的不止从缓存内拿相应的文件这么简单,我们可以完全控制整个fetch过程。那么可以做的事情就比较多了:
1. 缓存一个离线的html,当检测到无网络时,展示相应的html内容,提升用户体验
2. 对请求不到资源的情况做错误处理,展示相应的内容
3. 将所有图片换成我的支付宝付款码🙈
总结
Service Worker做缓存还是挺有用的,相比较Cache-Control之类的Headers来做缓存控制而言,拥有更细粒度的控制过程,并且可以做相应的错误和降级处理。但是依然需要注意缓存带来的时效性问题,否则得不偿失。