吴锴的博客

Life? Don't talk to me about life!

0%

如何使用 Service Worker 来缓存图片

大家应该都听过 service worker 这个概念,那么它究竟是什么呢?我们看下 MDN 上的解释:

Service workers essentially act as proxy servers that sit between web applications, the browser, and the network (when available). They are intended, among other things, to enable the creation of effective offline experiences, intercept network requests and take appropriate action based on whether the network is available, and update assets residing on the server. They will also allow access to push notifications and background sync APIs.

Service worker 在 web 应用、浏览器和网络之间扮演代理服务器的角色。可以用于创建有效的离线体验,劫持网络请求等。

在这篇文章中我创建了一个示例项目来展示 service worker 的基本用法及如何缓存图片。可以在这个项目 service-worker-demo 中看到文中的代码。

Service Worker 使用基础

首先使用 Vite 来创建一个新项目,运行 yarn create vite 后选择 vanilla 即可。

public 目录下创建一个名为 sw.js 的 service worker 文件:

1
2
// sw.js
console.log('This message is from service worker file.');

开启开发模式 yarn dev 后,可以在类似 http://localhost:5173/sw.js 的位置看到这个文件。

Service woker 在生效前要经历注册(register)的过程,在 main.js 文件中加入以下代码来注册 service worker:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if ('serviceWorker' in navigator) {
window.addEventListener('load', function () {
navigator.serviceWorker.register('/sw.js').then(
function (registration) {
// Registration was successful
console.log('ServiceWorker registration successful');
},
function (err) {
// Registration failed
console.log('ServiceWorker registration failed: ', err);
}
);
});
}

启动项目并打开页面,会在控制台看到以下内容:

控制台输出

第一条输出来自 sw.js 文件,表明这个 service worker 已经运行。第二条输出来自 main.js 文件中对 service worker 注册的过程,表明成功注册了。

打开 Application 标签页可以看到 service worker 状态是 activated and is running

service worker 状态

如果这时候刷新页面,会发现控制台中少了一条输出:

控制台输出

因为现在是已注册的状态了,所以这个文件不会再次运行。

如果你点击 Unregister 按钮,则会取消这个 service worker 文件的注册。之后刷新页面就会重新经历注册流程。

Unregister 按钮

有一点需要注意的是 serivce worker 文件的位置决定了它的作用域,比如上面代码中的 sw.js 可以控制整个 domain 的 fetch 事件。假如 service worker 文件在 /example/sw.js,那么它只能看见 URL 以 /example/ 开头的页面(例如 /example/page1/, /example/page2/)上的 fetch 事件。

如何进行图片的缓存

我们已经知道了如何让一个极简的 service worker 文件正常运行,接下来看看如何进行图片的缓存。

先在 html 中加入几张图片,任意放几张图片文件放到 public 目录下:

1
2
3
4
5
6
7
<div id="app">
<img src="/image1.png" />
<img src="/image2.png" />
<img src="/image3.png" />
<img src="/image4.png" />
<img src="/image5.png" />
</div>

此时在 Network 中查看图片请求,Size 栏展示的是图片的大小。当然如果没有勾选 Disable cache 的情况下,这里也可能显示 (memory cache) 或者 (disk cache)

Network 请求状态

接下来修改 service worker 文件 sw.js,加入以下内容:

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
const cacheName = 'cache-name-v1';

self.addEventListener('install', (event) => {
event.waitUntil(caches.open(cacheName));
});

self.addEventListener('fetch', async (event) => {
if (event.request.destination === 'image') {
// Open the cache
event.respondWith(
caches.open(cacheName).then((cache) => {
// Respond with the image from the cache or from the network
return cache.match(event.request).then((cachedResponse) => {
return (
cachedResponse ||
fetch(event.request.url).then((fetchedResponse) => {
// Add the network response to the cache for future visits.
// Note: we need to make a copy of the response to save it in
// the cache and use the original as the request response.
cache.put(event.request, fetchedResponse.clone());

return fetchedResponse;
})
);
});
})
);
}
});

看一下这段代码的效果,为了能让更新后的 service worker 文件生效,需要重新打开页面。现在 Size 栏展示的是 (ServiceWorker),表示图片已由 service worker 进行了缓存。

Network 请求状态

接下来解释下这段代码做了什么。

我们一直在说 service worker 可以用来缓存图片,这个缓存是放在哪里呢?这就先需要了解 Cache API,Cache 接口提供了对 Request / Respone 对象的持久化存储机制。你也可以在 service worker 以外来使用它,打开控制台:

Cache API
1
2
3
4
5
6
7
const cacheName = 'cache-name-v1';

// ...

caches.open(cacheName).then((cache) => {

})

这里就是先打开了 caches storage,然后依次对缓存内容进行操作。

1
2
3
4
5
6
self.addEventListener('fetch', async (event) => {
if (event.request.destination === 'image') {
event.respondWith(
// ...
)
})

这里监听了网络请求事件,在事件处理函数中我们就可以处理请求和响应了。先判断出这是一个图片请求:event.request.destination === 'image',然后用 event.respondWith() 来返回响应内容。

下面这一段包含在 event.respondWith() 之内,是在决定要要返回什么。先在缓存中检查请求是否已有响应缓存,如果已经有了就直接返回,否则就执行 fetch 并将这次的响应放入缓存中。

1
2
3
4
5
6
7
8
9
10
11
12
caches.open(cacheName).then((cache) => {
return cache.match(event.request).then((cachedResponse) => {
return (
cachedResponse ||
fetch(event.request.url).then((fetchedResponse) => {
cache.put(event.request, fetchedResponse.clone());

return fetchedResponse;
})
);
});
})

上面看到在 Network 中查看图片请求都显示来自 service worker,可以从 Application -> Cache Storage 里查看具体的缓存内容。

Cache Storage

为什么要用 service worker 缓存

你可能会问用 service worker 缓存有什么好处?浏览器中不是已经有了 memory cache 和 disk cache 了吗?

一个使用场景是离线化应用,尝试对 sw.js 文件做一点小改动,注释掉这个对类型的判断:

1
2
3
if (event.request.destination === 'image') {

}

重新打开这个页面后会发现所有的资源都会走 service worker 了,这时即使你断开网络也是可以正常展示的。

离线状态

利用 service worker 也可以允许开发人员更精细地控制哪些资源被缓存,以及在何时更新缓存。

另一些优化

既然我们利用 service worker 可以劫持所有图片的请求,那么还可以做些其他的优化,比如将图片修改为更加合适的格式。我们知道 webp 是一种比 png 更新的图片格式,一般会有更小的体积。某些图片服务可能提供这样的功能,在这样的 url 上(http://www.example.com/1.png)添加一个后缀就(`http://www.example.com/1.png?format=webp)得到了对应格式的图片。

尝试下实现这个功能,先给用到的图片生成对应的 webp 格式,放在同一位置,比如 image1.png 生成 image1.webp。

定义一个函数来做 url 的替换,仅将后缀做个替换:

1
2
3
function getNewUrl(url) {
return url.replace(/.png$/, '.webp');
}

然后改写 fetch 事件的监听函数:

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
const cacheName = 'cache-name-v2';

self.addEventListener('fetch', async (event) => {
if (event.request.destination === 'image') {
event.respondWith(
caches.open(cacheName).then((cache) => {
// 构建一个新的请求
const newRequest = new Request(
getNewUrl(event.request.url),
event.request
);

return cache.match(event.request).then((cachedResponse) => {
return (
cachedResponse ||
// 通过新的请求来获取资源
fetch(newRequest).then((fetchedResponse) => {
cache.put(event.request, fetchedResponse.clone());
return fetchedResponse;
})
);
});
})
);
}
});

注意这里我们将 cacheName 修改为了 cache-name-v2,因为之前版本的 service worker 已经将一份 image1.png 的缓存放在了名为 cache-name-v1 的缓存中,代码检查到图片缓存请求已存在也就不会发出新的请求。新建一个 cache key 来保存新的缓存。

看下改动的效果:

Cache Storage

可以看到虽然图片名称仍然是 image1.png,但是 Content-Type 已经改变了。这里 webp 格式几乎比 png 格式小了一半体积,当然这主要是因为我的 png 图片没有经过压缩处理。真实的场景中,由 png 格式转换成 webp 不会有这么大的体积优化。

关于 service worker 还有一点需要了解,因为 service worker 可以任意修改网络请求,所以处于安全原因需要在 HTTPS 环境中才能启用。

参考资料

如果我的文章对你有帮助,欢迎打赏支持!

欢迎关注我的其它发布渠道