浅谈Service Worker 缓存策略

缓存策略

写过 Service Worker的都知道,做预缓存资源(precaching),来来去去的无非是以下的套路:

  • 注册Service Worker脚本
  • 在install监听事件里,将需要缓存的资源列表逐一请求,并缓存至 caches
  • 取得控制权后,触发activate事件后,清除过期资源
1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/sw.js').then(function(registration) {
// Registration was successful
console.log('ServiceWorker registration successful with scope: ', registration.scope);
}, function(err) {
// registration failed :(
console.log('ServiceWorker registration failed: ', err);
});
});
}
</script>
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
var CACHE_NAME = 'my-site-cache-v1';
var urlsToCache = [
'/',
'/styles/main.css',
'/script/main.js'
];

self.addEventListener('install', function(event) {
// Perform install steps
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
console.log('Opened cache');
return cache.addAll(urlsToCache);
})
);
});
self.addEventListener('activate', function(event) {
var cacheWhitelist = ['pages-cache-v1', 'blog-posts-cache-v1'];
event.waitUntil(
caches.keys().then(function(cacheNames) {
return Promise.all(
cacheNames.map(function(cacheName) {
if (cacheWhitelist.indexOf(cacheName) === -1) {
return caches.delete(cacheName);
}
})
);
})
);
});

可以看出预缓存资源这块逻辑并不复杂,而重点在于fetch 事件,根据 Google 的离线指南,fetch 事件能写的缓存策略简直是花样多多。如果再对CSS、图像、字体、JS、模板进行差异化缓存,可想而知 fetch 事件的复杂。

Workbox 简介

当然这类文章往往说到这里的套路是,作者在前文指出种种困难后,为解决问题,隆重介绍了一个新的JS库,安利一波好处,诸如起到多大的优化效果、代码是如何的简洁等等。没错,接下来我要安利Google 的Workbox,且听我接下来慢慢介绍。

Workbox 继承了sw-precache 和 sw-toolbox库。它是一个生成SW 脚本、预缓存、路由控制、运行时缓存的库与工具合集。Workbox 还集成了background sync、Google 分析,给web app 提供了静态资源、接口请求结果的离线支持。

常用的3大模块

  • 预缓存 precaching (类比sw-precache

  • 路由控制 routing(类比sw-toolbox

  • 运行时缓存策略 strategies (类比sw-toolbox的5个Handlers

这里说个历史,sw-precache、sw-toolbox库是GoogleChrome 团队在 Workbox之前推出的,反响一般,所以在2017年已经停止维护。

precaching

核心Api 是 workbox.precaching.precacheAndRoute()workbox.precaching.addRoute(),作用是预存一些上线后改动概率很小的文件,处理逻辑跟文章开头说的预缓存资源一样,但这里是根据revision或文件名更新资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Revisioned files added via a glob
workbox.precaching.precache([
'/styles/example-1.abcd.css',
'/styles/example-2.1234.css',
'/scripts/example-1.abcd.js',
'/scripts/example-2.1234.js',
]);

// Precache entries from workbox-build or somewhere else
workbox.precaching.precache([
{
url: '/index.html',
revision: 'abcd',
}, {
url: '/about.html',
revision: '1234',
}
]);

// Add Precache Route
workbox.precaching.addRoute();

你会看到有这两种写法,他们的区别在于:如果静态资源文件名含有文件 hash 值,则最佳实践;如果资源文件名不含有文件 hash 值,就需要添加一个版本属性。

所以基本无法手动维护缓存列表,所以一般写成空数组形式,加以配置文件 workbox-cli-config.js,配置文件指定查找文件的位置 (globDirectory)、预缓存哪些文件 (globPatterns)、忽略哪些文件 (globIgnores),利用workbox-cli生成最终 sw脚本,或者输出manifest

1
workbox.precaching.precacheAndRoute([]);

strategies

Workbox 介绍了几种缓存策略,workbox strategies,即常用的五种缓存策略

  • CacheFirst: 缓存优先

  • CacheOnly: 仅缓存

  • NetworkFirst: 网络优先

  • NetworkOnly: 仅网络

  • StaleWhileRevaidate: 取缓存,再走网络更新缓存

这里不解释五种策略的作用,因为搞懂前文所提到的谷歌开发者文档离线指南,就可以明白workbox 提供缓存策略的原理以及如何使用。

如果这五种缓存策略均不满足缓存需求,workbox 还提供了自定义策略,其实相当于最原始的 fetch 事件控制缓存策略。

1
workbox.routing.registerRoute(matchCb, handlerCb);

Workbox in Webpack

vue-cli 3.0内置了 workbox,一般喜欢 vue cli一把梭的同学,自然不怕这些 webpack的配置,开箱即用。

但workbox 的配置其实并没有那么复杂,了解 workbox-cli的,很容易上手。重点理解 workbox 的流程。

文档参考:Workbox webpack Plugins

经验之谈

不要给 sw.js 设置不同的名字

一般针对静态资源,根据文件hash给它们一个唯一的命名,例如 index.[hash].js。因为这些文件不常修改,再配以长时间的强缓存,能够大大降低访问它们的耗时。

可惜针对 SW,这种做法并不合适。我们假设一个项目首页 index.html,底下包含了一段用于注册 service-worker.v1.js。
为了提升速度或者离线可用,这个 service-worker.v1.js 会把 index.html 缓存起来。
某次升级更新之后,现在 index.html 需要配上 service-worker.v2.js 使用了,所以源码中底下的中修改了注册的地址。
但我们发现,用户访问站点时由于旧版 service-worker.v1.js 的作用,从缓存中取出的 index.html 引用的依然是 v1,并不是我们升级后引用 v2。
之所以出现这种情况,是因为把 v1 升级为 v2 依赖于 index.html 引用地址的变化,但它本身却被缓存了起来。一旦到达这种窘境,除非用户手动清除缓存,卸载 v1,否则我们无能为力。

所以 service-worker.js 必须使用相同的名字,不能在文件名上加上任何会改变的因素。

不要给 sw.js 设置缓存

理由和第一点类似,也是为了防止在浏览器需要请求新版本的 SW 时,因为缓存的干扰而无法实现。毕竟我们不能要求用户去清除缓存。因此给sw.js设置 Cache-control: no-store 或者 no-cache 是比较安全的。

workbox cdn

示例的workbox cdn地址是谷歌家的,可以迁移至自家公司的 cdn

静态资源在 CDN 上

cdn上的静态资源是跨域的,路由匹配的时候,正则表达式必须与URL的开头匹配

1
2
3
4
5
workbox.routing.registerRoute(
new RegExp('https://xxx.com.*/styles/.*\.css'),
handlerCb
);
`

由于是跨域,fetch事件跨域会报错,所以缓存策略绝不能用 cachefirst 或者 cacheonly,要用staleWhileRevalidate

Service Worker降级

做个服务器开关,出了问题,unregister 脚本

一份合理的缓存策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
workbox.precaching([
// 基本不会改的东西
]);
workbox.routing.registerRoute(
new RegExp('.*\.html'),
workbox.strategies.networkFirst()
);

workbox.routing.registerRoute(
new RegExp('.*\.(?:js|css)'),
workbox.strategies.cacheFirst()
);

workbox.routing.registerRoute(
new RegExp('https://your\.cdn\.com/'),
workbox.strategies.staleWhileRevalidate()
);