PWA 学习笔记之 Service Workers Cache Control

View this thread on: d.buzz | hive.blog | peakd.com | ecency.com
·@semlinker·
0.000 HBD
PWA 学习笔记之 Service Workers Cache Control
通过[PWA 学习笔记之 Service Workers](https://steemit.com/cn-programming/@semlinker/pwa-service-workers)这篇文章,我们了解了如何使用、调试、更新 Service  Worker 及 Service Worker 生命周期相关知识。接下来本文将介绍如何利用 fetch API、CacheStorage API 及 Service Worker 实现缓存控制。

### fetch 事件及自定义响应

在你使用缓存前,你必须能够拦截网络请求。强大的 Service Worker 为我们提供了这种能力,在 Service Worker 作用域内的每个网络请求,将会触发 fetch 事件:

```javascript
self.addEventListener('fetch', (event) => {
  event.respondWith(fetch(event.request));
});
```

以上代码我们只是实现简单的请求代理,实际上并没有多大用途,其实我们是可以自定义响应对象,即通过 fetch API 定义的 Response 接口,创建响应对象,使用的语法如下:

```javascript
let myResponse = new Response(body, init);
```

 **参数说明**:

- body(可选)—— 定义 response 中 body 内容,支持以下数据类型:
  - Blob
  - BufferSource
  - FormData
  - URLSearchParams
  - USVString
- init(可选)—— 用于配置响应对象,支持以下属性:
  - status:响应的状态码,如 200
  - statusText:与状态码关联的状态信息,如 OK
  - headers:响应头部对象

**使用示例**:

```javascript
var myBlob = new Blob();
var init = { "status" : 200 , "statusText" : "Awesome!" };
var myResponse = new Response(myBlob,init);
```

光写不练假把式,我们立马实践一下,手动更新 service-worker.js 文件:

```javascript
self.addEventListener('install', (event) => {
    self.skipWaiting();
    console.log('Update service worker installed', event);
});

self.addEventListener('activate', (event) => {
    console.log('Update service worker activated', event);
});

// 以下是新增的内容
self.addEventListener('fetch', (event) => {
    event.respondWith(new Response('My name is semlinker!'));
});
```

更新完保存文件,然后在浏览器中访问 index.html 文件,此时你会在页面中看到以下内容:

```
My name is semlinker!
```

### CacheStorage API

缓存是我们的好朋友,利用缓存我们甚至可以在离线状态下,使得 App 仍然可以使用。接下来我们继续更新 service-worker.js 文件,在 install 事件处理函数中,我们利用 CacheStorage API 对特定的资源进行缓存:

```javascript
self.addEventListener('install', (event) => {
    self.skipWaiting();
    if ('caches' in self) {
        event.waitUntil(
            caches.open('v1').then((cache) => {
                return cache.addAll([
                    './index.html',
                    './script.js',
                    './sw-in-action.png'
                ]);
            })
        );
    }
    console.log('Update service worker installed', event);
});

// 注释掉自定义响应对象
self.addEventListener('fetch', (event) => {
    // event.respondWith(new Response('My name is semlinker!'));
});
```

在 install 事件处理函数中,我们先判断当前浏览器是否支持 CacheStorage API,若当前浏览器支持该 API 的话,我们也可以通过 `window.caches` 访问 CacheStorage 对象。然后我们使用 `event.waitUntil` 方法让 Service  Worker 处于 installing 阶段,直到设定的任务完成。这里需要注意的是,event.waitUntil 方法的参数类型必须为 Promise。

最后,我们调用 `caches.open('v1')` 方法来获取 v1 版本的缓存对象(若不存在,则会自动创建),进而通过调用缓存对象的 addAll 方法,缓存对应资源。

为了能够更直观的感受缓存特性,我们新增了一张图片资源。这时,我们需要注释掉 fetch 事件处理器中,自定义响应的代码。除此之外,我们也需要更新一下 index.html 文件,新增以下 img 标签:

```html
 <!-- <img src="sw-in-action.png"> -->
```

万事俱备只欠东风,接下来重新刷新一下浏览器,不出所料的话,你将会看到一张图片。难道这样就结束了?当然不是,我们还没检查已设置的资源,是否被成功缓存。还是老样子,打开开发者工具,切换到 Application Tab 页,不过这次我们打开的是 Cache 菜单下的 CacheStorage 子菜单,具体内容如下图所示:

![sw-cache-v1.jpg](https://steemitimages.com/DQmQDgeFw2e5DVoK29v2y7t2F5jeL9zPcaghR2a2F8cMVDh/sw-cache-v1.jpg)

通过上图,可以发现我们设置的资源已经被成功缓存了。那么,接下来的事情就是应该如何利用缓存。不知道小伙伴们,是否还记得[PWA 学习笔记之 CacheStorage API](https://steemit.com/cn-programming/@semlinker/pwa-cachestorage-api)这篇文章中介绍过的 match 方法,**该方法返回一个 Promise 对象,用于判断给定的请求对象是否已被缓存。若匹配则返回已缓存的对象**。下面,我来继续更新 service-worker.js 文件,这里我们只需更新 fetch 事件处理器:

```javascript
self.addEventListener('fetch', (event) => {
    event.respondWith(
        caches.match(event.request)
    );
});
```

重新保存 service-worker.js 文件,在成功激活新的 Service Worker 后,我们先切换到 Network Tab 页,然后再次刷新一下浏览器。具体的内容如下图所示:

![resource-from-sw.png](https://steemitimages.com/DQmZ1BxVxdZqS1EAdCPK7fuUz1ey5ZwNwKAi8bruNDRawxi/resource-from-sw.png)

从图中,我们可以看出当刷新当前页面时,Service Worker 直接为我们返回了之前已缓存的资源。另外,感兴趣的小伙伴,你也可以把网络状态设置为 Offline,或者进入 Application Tab 页,把 Cache Storage 中 v1 的缓存删掉,然后再刷新当前页面,看一下页面的显示情况。

其实 fetch 事件处理器的代码存在一个问题,假如我们哪天更新了 index.html 文件,比如再次新增一个 img 标签,它引用了新的图片资源,但并没有把新的图片资源保存到缓存对象中,这时会出现什么问题呢?我们立马来验证一下,具体流程如下:

- 复制已有的 sw-in-action.png 文件,重命名为 sw-in-action-1.png;
- 更新 index.html 文件,添加新的 img 标签并设置其 src 地址(引用刚创建的 sw-in-action-1.png 文件)。

保存文件,刷新当前页面,再次观察 Network Tab 页的内容,具体如下图所示:

![resource-request-fail.png](https://steemitimages.com/DQmdYavQJHrv3ENtxiWERKV5oP6oTu84GQ2b45igYwwNSfc/resource-request-fail.png)

图中我们发现已缓存的资源,能够正常返回,但由于我们没有把 sw-in-action-1.png 文件,添加到缓存中,使得该资源的请求,缓存匹配失败,最终导致 sw-in-action-1.png 该文件无法在页面中正常显示 。

那么如何解决这个问题呢?最简单的方案就是把 sw-in-action-1.png 文件添加到 cache.addAll 方法的数组中。这样虽然可以解决问题,但有没有更好的方案呢?**理想的情况下,我们优先从缓存中获取对应的资源,若缓存中不存在对应的资源,且当前网络是可用的情况下,我们可以在无法获取缓存的时候,发起网络请求,获取对应的资源**。按照以上的分析思路,我们再来更新一下 service-worker.js 文件,具体如下:

```javascript
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((response) => {
    // 若缓存命中,则直接返回缓存对象,否则通过fetch API从网络获取
      return response || fetch(event.request);
     })
  );
});
```

使用上面的代码,我们解决了刚才遇到的问题。那么上述的代码,还可以优化么?答案是可以的,我们可以把从网络获取的资源,添加到缓存对象中,以保证离线状态下,仍可以使用。要实现该功能,还是利用我们之前学过的 CacheStorage API,具体实现代码如下:

```javascript
self.addEventListener('fetch', (event) => {
    event.respondWith(
        caches.match(event.request).then((response) => {
            return response || fetch(event.request).then((response) => {
                return caches.open('v1').then((cache) => {
                    cache.put(event.request, response.clone());
                    return response;
                });
            });
        })
    );
});
```

通过这篇文章我们把之前所学的知识,如 fetch API、CacheStorage API 及 Service Worker 等知识综合应用起来,简单实现了缓存控制。后面的文章我们将介绍不同的缓存策略及如何更新缓存。刚开始学习 PWA,以上内容有误之处,请小伙伴们多多指教。

### 参考资源

- [MDN - Response](https://developer.mozilla.org/zh-CN/docs/Web/API/Response/Response)
👍 ,