PWA 学习笔记之 Service Workers Cache Control

in #cn-programming7 years ago (edited)

通过PWA 学习笔记之 Service Workers这篇文章,我们了解了如何使用、调试、更新 Service Worker 及 Service Worker 生命周期相关知识。接下来本文将介绍如何利用 fetch API、CacheStorage API 及 Service Worker 实现缓存控制。

fetch 事件及自定义响应

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

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

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

let myResponse = new Response(body, init);

参数说明

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

使用示例

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

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

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 对特定的资源进行缓存:

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 comment removed:  <img src="sw-in-action.png"> )

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

sw-cache-v1.jpg

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

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

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

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

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

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

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

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

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,以上内容有误之处,请小伙伴们多多指教。

参考资源