第三章 缓存

3.1 HTTP 缓存基础

浏览器发起请求时,服务器返回的资源附带了一些 HTTP 响应头,告知浏览器这个资源是什么类型的,要缓存多长时间,是否压缩过等等。

使用 HTTP 缓存意味着要依赖服务器来告知何时缓存资源以及资源何时过期。如果内容具有相关性,任何更新都可能导致服务器发送的到期日期变得很容易不同步,并影响网站。

3.2 Service Worker 缓存基础

Service Worker 缓存无需由服务器来告知浏览器资源需要缓存多久,可以全权掌握。

3.2.1 在 Service Worker 安装过程中预缓存

1
2
3
4
5
6
7
8
9
10
var cacheName = 'helloWorld' // 缓存的名称
self.addEventListener('install', event = { // 进入 Service Worker 的安装事件
event.waitUntil(
caches.open(cacheName) // 使用指定的换名名称来打开缓存
.then(cache => cache.addAll([ // 把 JavaScript 和图片文件添加到缓存中
'/js/script.js',
'/images/hello.png'
]))
)
})

一旦缓存开启,就可以开始把资源添加进去。接下来,调用了 cache.addAll() 并传入文件数组。event.waitUntil() 方法使用了 JavaScript 的 Promise 来知晓安装所需的时间以及是否安装成功。

如果所有的文件都成功缓存了,那么 Service Worker 便会安装完成。如果有任何文件下载失败了,安装过程也随之失败。

1
2
3
4
5
6
7
8
9
10
11
self.addEventListener('fetch', function() { // 添加 fetch 事件的事件监听器
event.respondWith(
caches.match(event.request) // 检查传入的请求 URL 是否匹配当前缓存中存在的任何内容
.then(function(response) {
if (response) { // 如果有 response 并且不是未定义的或空的,就将它返回
return response
}
return fetch(event.request) // 否则,只是如往常一样继续,通过网络获取预期的资源
})
)
})

3.2.2 拦截并缓存

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
var cacheName = 'helloWorld'

self.addEventListener('fetch', function() { // 添加 fetch 事件的事件监听器
event.respondWith(
caches.match(event.request) // 检查传入的请求 URL 是否匹配当前缓存中存在的任何内容
.then(function(response) {
if (response) { // 如果有 response 并且不是未定义的或空的,就将它返回
return response
}

var requestToCache = event.request.clone() // 复制请求。请求是一个流,只能使用一次

return fetch(requestToCache)
.then(function(response) {
if (!response || response.status !== 200) { // 如果请求失败或者服务器响应了错误代码,则立即返回错误消息
return response
}

var responseToCache = response.clone()

caches.open(cacheName)
.then(function(cache) {
cache.put(requestToCache, responseToCache) // 将响应添加到缓存中
})

return response
})
})
)
})

首先,要检查请求的资源是否存在于缓存中。如果存在于缓存中,可以就此返回缓存并不再继续执行代码。

如果缓存中没有请求资源,就按原计划发起网络请求。在代码进一步执行之前,需要复制请求,请求是一个流,只能使用一次。因为之前已经通过缓存使用了一次请求,接下来发起 HTTP 请求还要再使用一次,所以需要在此时复制请求。然后检查 HTTP 响应,确保服务器返回的是成功响应并且没有任何问题。

如果成功响应,会再次复制响应。因为想要浏览器和缓存都能够使用响应,所以需要复制它,这样就有了两个流。

最后,在代码中使用这个响应并将其添加至缓存中,以便下次再使用它。

在上面的示例中,每次返回成功的 HTTP 响应时,都能够动态地向缓存中添加资源。如果想要缓存资源但不太确定它们可能更改的频率或确切地来自哪里,这种方案可能会适合。

3.2.3 整合所有代码

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
var cacheName = 'latestNews-v1'

// 在安装过程中缓存已知的资源
self.addEventListener('install', event => {
event.waitUntil(
caches.open(cacheName)
.then(cache => cache.addAll([ // 在安装期间打开缓存并存储一组资源进行缓存
'./js/main.js',
'./js/article.js',
'./images/newspaper.svg',
'./css/site.css',
'./data/latest.json',
'./data/data-1.json',
'./article.html',
'./index.html'
]))
)
})

// 缓存任何获取的新资源
self.addEventListener('fetch', event => { // 监听 fetch 事件
event.respondWith(
caches.match(event.request, { ignoreSearch: true }) // 忽略任何查询字符串参数,这样便不会造成任何缓存未命中
.then(function (response) {
if (response) return response // 如果发现了成功的匹配,就在此刻返回缓存并不再继续执行
var requestToCache = event.request.clone()

return fetch(requestToCache) // 如果没在缓存中找到任何内容,则发起请求
.then(function (response) {
if (!response || response.status !== 200) {
return response
}

var requestToCache = response.clone()
caches.open(cacheName) // 存储在缓存中,这样便不需要再次发起请求
.then(function (cache) {
cache.put(requestToCache, requestToCache)
})
})
})
)
})

如上代码是安装期间的预缓存和获取资源时进行缓存的组合应用。该 Web 应用使用了应用外壳架构,意味着可以利用 Service Worker 缓存来只请求填充页面所需的数据。你已经成功存储了外壳的资源,所以剩下的就是来自服务器的动态新闻内容。

3.3 缓存前后的性能对比

WebPagetest.org,略。

3.4 深入 Service Worker 缓存

3.4.1 对文件进行版本控制

理念是每次更改文件时创建一个全新的文件名,以确保浏览器可以获取最新的内容。

3.4.2 处理额外的查询参数

  • ignoreSearch,忽略请求参数和缓存请求中 URL 的查询部分
  • ignoreMethod,忽略请求参数的方法
  • ignoreVary,忽略已缓存响应中的 vary 响应头

3.4.3 需要多少内存

略。

3.4.4 将缓存提升到一个新的高度:Workbox

Workbox,是一个由谷歌团队编写的辅助库,帮助快速创建 Service Worker,涵盖了最常见的网络策略。

1
2
3
4
5
6
7
8
// sw.js
importScripts('workbox-sw.prod.v1.1.0.js') // 加载 Workbox 库
const workboxSW = new self.WorkboxSW()

workboxSW.router.registerRoute( // 开始缓存匹配'/css'路径的任何请求
'https://test.org/css/(.*)',
workboxSW.strategies.cacheFirst()
)

更多用法详见 Workbox 文档。