Caching Files With Service Worker

浏览器的缓存是把双刃剑,使用得当的话,可以加快页面的加载速度,减少向服务器的请求次数,进而减少带宽和服务器压力。但是如果使用不得当的话,会造成新功能或者问题修复未能正常生效等问题,用户体验有所下降。常见的缓存方式包括强制缓存和协商缓存,分别用在不同情境下,通过相关的 Headers 控制(感兴趣的可以看之前 HTTP Headers 文章中关于缓存的介绍),也是大多数网站的首要技术选择。那么还有什么其他缓存相关的技术可以使用吗?让我们来关注一下Service Worker

快速入门

Service Worker旨在通过代码精确控制缓存文件和 HTTP 请求,是已经被废弃掉的AppCache技术的替代方案。Service Worker有相关的生命周期概念,如下所示:

实际案例

我们来看一下语雀是如何使用Service Worker来缓存相关内容的。

注册Service Worker

ServiceWorkerContainernavigator下,所以先判断是否支持Service Worker,不支持的话另做处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 变量assetsresourceBase,并用importScripts引入新的逻辑处理

1
2
3
4
5
6
7
8
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

处理缓存

主要逻辑如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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(() => {})
)
);
});

这里实现了如下逻辑:

  1. 在 install 阶段调用cache.addAll()self.assets内的文件缓存
  2. 在 active 阶段将新旧的self.assets进行对比,并将失效的缓存删除掉: self.assets.includes(t.url) || e.delete(t)
  3. 注册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 来做缓存控制而言,拥有更细粒度的控制过程,并且可以做相应的错误和降级处理。但是依然需要注意缓存带来的时效性问题,否则得不偿失。

参考链接

Comments