ServiceWorker 离线及缓存策略

原文

www.lishuaishuai.com/pwa/1093.ht…javascript

前言

若是你追求极致的Web体验,你必定在站点中使用过 PWA(Progressive Web App),也必定面临过在编写Service Worker代码时的犹豫不决,由于Service Worker过重要了,一旦注册在用户的浏览器,全站的请求都会被 Service Worker 控制,一不留神,小问题也成了大问题了。接下来看如何用Service Worker处理离线问题。css

什么时候进行缓存

On install - as a dependency

ServiceWorker 提供一个 install 事件。可使用该事件作一些准备,即处理其余事件以前必须完成的操做。 在进行这些操做时,任何之前版本的 ServiceWorker 仍在运行和提供页面,所以在此处进行的操做必定不能干扰它们。

适合于: CSS、图像、字体、JS、模板等,基本上囊括了能够视为网站“版本”的静态内容的任何对象。html

若是未能提取上述对象,将使网站彻底没法运行,对应的本机应用会将这些对象包含在初始下载中。java

self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open('mysite-static-v3').then(function(cache) {
      return cache.addAll([
        '/css/whatever-v3.css',
        '/css/imgs/sprites-v6.png',
        '/css/fonts/whatever-v8.woff',
        '/js/all-min-v4.js'
        // etc
      ]);
    })
  );
});
复制代码

event.waitUntil 选取一个 promise 以定义安装时长和安装是否成功。 若是 promise reject,则安装被视为失败,并舍弃这个 ServiceWorker (若是一个较旧的版本正在运行,它将保持不变)。caches.opencache.addAll 将返回 promise。若是其中有任一资源获取失败,则 cache.addAll 执行rejectweb

On install - not as a dependency

与上述类似,但若是缓存失败,既不会延迟安装也不会致使安装失败。json

适合于: 不是即刻须要的大型资源,如用于游戏较高级别的资源。promise

self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open('mygame-core-v1').then(function(cache) {
      cache.addAll(
        // levels 11-20
      );
      return cache.addAll(
        // core assets & levels 1-10
      );
    })
  );
});
复制代码

咱们不会将级别 11-20 的 cache.addAll promise 传递回 event.waitUntil,所以,即便它失败,游戏在离线状态下仍然可用。固然,必须考虑到可能缺乏这些资源的状况,而且若是缺乏,则从新尝试缓存它们。浏览器

当级别 1-10 进行下载时,ServiceWorker 可能会终止,意味着它们将不会被缓存。 未来,咱们计划添加一个后台下载 API 以处理此类状况和较大文件下载,如电影。缓存

On activate

适合于: 清理和迁移。服务器

在新的 ServiceWorker 已安装而且未使用之前版本的状况下,新 ServiceWorker 将激活,而且将得到一个 activate 事件。 因为旧版本退出,此时很是适合处理 IndexedDB 中的架构迁移和删除未使用的缓存。

self.addEventListener('activate', function(event) {
  event.waitUntil(
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        cacheNames.filter(function(cacheName) {
          // Return true if you want to remove this cache,
          // but remember that caches are shared across
          // the whole origin
        }).map(function(cacheName) {
          return caches.delete(cacheName);
        })
      );
    })
  );
});
复制代码

在激活期间,fetch 等其余事件会放置在一个队列中,所以长时间激活可能会阻止页面加载。 尽量让您的激活简洁,仅针对旧版本处于活动状态时没法执行的操做使用它。

On user interaction

适合于: 若是整个网站没法离线工做,您能够容许用户选择他们须要离线可用的内容。 例如,YouTube 上的某个视频、维基百科上的某篇文章、Flickr 上的某个特定图库。

为用户提供一个“Read later”或“Save for offline”按钮。在点击该按钮后,从网络获取您须要的内容并将其置于缓存中。

document.querySelector('.cache-article').addEventListener('click', function(event) {
  event.preventDefault();

  var id = this.dataset.articleId;
  caches.open('mysite-article-' + id).then(function(cache) {
    fetch('/get-article-urls?id=' + id).then(function(response) {
      // /get-article-urls returns a JSON-encoded array of
      // resource URLs that a given article depends on
      return response.json();
    }).then(function(urls) {
      cache.addAll(urls);
    });
  });
});
复制代码

caches API可经过页面以及service workers获取,这意味着您不须要经过service workers向缓存添加内容。

On network response

适合于: 频繁更新诸如用户收件箱或文章内容等资源。 同时适用于不重要的资源,如头像,但须要谨慎处理。

若是请求的资源与缓存中的任何资源均不匹配,则从网络中获取,将其发送到页面同时添加到缓存中。

若是您针对一系列网址执行此操做,如头像,那么您须要谨慎,不要使源的存储变得臃肿,若是用户须要回收磁盘空间,您不会想成为主要候选对象。请确保将缓存中再也不须要的项目删除。

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.open('mysite-dynamic').then(function(cache) {
      return cache.match(event.request).then(function (response) {
        return response || fetch(event.request).then(function(response) {
          cache.put(event.request, response.clone());
          return response;
        });
      });
    })
  );
});
复制代码

为留出充足的内存使用空间,每次您只能读取一个响应/请求的正文。 在上面的代码中,.clone() 用于建立可单独读取的额外副本。

Stale-while-revalidate

适合于: 频繁更新最新版本并不是必需的资源。 头像属于此类别。

若是有可用的缓存版本,则使用该版本,但下次会获取更新。

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.open('mysite-dynamic').then(function(cache) {
      return cache.match(event.request).then(function(response) {
        var fetchPromise = fetch(event.request).then(function(networkResponse) {
          cache.put(event.request, networkResponse.clone());
          return networkResponse;
        })
        return response || fetchPromise;
      })
    })
  );
});
复制代码

这与 HTTP 的 stale-while-revalidate 很是类似。

On push message

Push API 是基于 ServiceWorker 构建的另外一个功能。 该 API 容许唤醒 ServiceWorker 以响应来自操做系统消息传递服务的消息。即便用户没有为您的网站打开标签,也会如此,仅唤醒 ServiceWorker。 您从页面请求执行此操做的权限,用户将收到提示。

适合于: 与通知相关的内容,如聊天消息、突发新闻或电子邮件。 同时可用于频繁更改受益于当即同步的内容,如待办事项更新或日历更改。

常见的最终结果是出现一个通知,在点按该通知时,打开/聚焦一个相关页面,但在进行此操做前必定要先更新缓存。

很明显,用户在收到推送通知是处于在线状态,可是,当他们最终与通知交互时可能已经离线,所以,容许离线访问此内容很是重要。Twitter 本机应用在大多数状况下都是很是好的离线优先例子,但在这点上却有点问题。

若是没有网络链接,Twitter 没法提供与推送消息相关的内容。 不过,点按通知会移除通知,从而使用户获取的信息将比点按通知前少。 不要这样作!

在显示通知以前,如下代码将更新缓存:

self.addEventListener('push', function(event) {
  if (event.data.text() == 'new-email') {
    event.waitUntil(
      caches.open('mysite-dynamic').then(function(cache) {
        return fetch('/inbox.json').then(function(response) {
          cache.put('/inbox.json', response.clone());
          return response.json();
        });
      }).then(function(emails) {
        registration.showNotification("New email", {
          body: "From " + emails[0].from.name
          tag: "new-email"
        });
      })
    );
  }
});

self.addEventListener('notificationclick', function(event) {
  if (event.notification.tag == 'new-email') {
    // Assume that all of the resources needed to render
    // /inbox/ have previously been cached, e.g. as part
    // of the install handler.
    new WindowClient('/inbox/');
  }
});
复制代码

On background-sync

后台同步是基于 ServiceWorker 构建的另外一个功能。它容许您一次性或按(很是具备启发性的)间隔请求后台数据同步。 即便用户没有为您的网站打开标签,也会如此,仅唤醒 ServiceWorker。您从页面请求执行此操做的权限,用户将收到提示。

适合于: 非紧急更新,特别那些按期进行的更新,每次更新都发送一个推送通知会显得太频繁,如社交时间表或新闻文章。

self.addEventListener('sync', function(event) {
  if (event.id == 'update-leaderboard') {
    event.waitUntil(
      caches.open('mygame-dynamic').then(function(cache) {
        return cache.add('/leaderboard.json');
      })
    );
  }
});
复制代码

缓存持久化

为您的源提供特定量的可用空间以执行它须要的操做。该可用空间可在全部源存储之间共享。 LocalStorage、IndexedDB、Filesystem,固然还有 Caches。

您获取的空间容量未指定,其因设备和存储条件而异。 您能够经过如下代码了解您已得到多少空间容量:

navigator.storageQuota.queryInfo("temporary").then(function(info) {
  console.log(info.quota);
  // Result: <quota in bytes>
  console.log(info.usage);
  // Result: <used data in bytes>
});
复制代码

不过,与全部浏览器存储同样,若是设备出现存储压力,浏览器将随时舍弃这些空间。 遗憾的是,浏览器没法区分您想要不惜任何代价保留的电影和您不太关心的游戏之间有什么不一样。

为解决此问题,建议使用 API requestPersistent

// From a page:
navigator.storage.requestPersistent().then(function(granted) {
  if (granted) {
    // Hurrah, your data is here to stay!
  }
});
复制代码

固然,用户必须授予权限。让用户参与此流程很是重要,由于如今咱们能够预期用户会控制删除。若是用户的设备出现存储压力,并且清除不重要的数据没能解决问题,那么用户须要凭判断力决定保留哪些项目以及移除哪些项目。

为实现此目的,须要操做系统将“持久化”源等同于其存储使用空间细分中的本机应用,而不是做为单个项目报告给浏览器。

缓存策略

不管您缓存多少内容 ServiceWorker 都不会使用缓存,除非您指示它在什么时候使用缓存以及如何使用。 如下是用于处理请求的几个模式:

Cache only

适合于: 您认为属于该“版本”网站静态内容的任何资源。您应在安装事件中缓存这些资源,以便您能够依靠它们。

self.addEventListener('fetch', function(event) {
  // If a match isn't found in the cache, the response
  // will look like a connection error
  event.respondWith(caches.match(event.request));
});
复制代码

Network Only

适合于: 没有相应离线资源的对象,如 analytics pings、non-GET 请求。

self.addEventListener('fetch', function(event) {
  event.respondWith(fetch(event.request));
  // or simply don't call event.respondWith, which
  // will result in default browser behaviour
});
复制代码

Cache First

适合于: 若是您以离线优先的方式进行构建,这将是您处理大多数请求的方式。 根据传入请求而定,其余模式会有例外。

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request).then(function(response) {
      return response || fetch(event.request);
    })
  );
});
复制代码

其针对缓存中的资源为您提供“仅缓存”行为,而对于未缓存的资源则提供“仅网络”行为(其包含全部 non-GET 请求,由于它们没法缓存)。

Cache & network race

适合于: 小型资源,可用于改善磁盘访问缓慢的设备的性能。

在硬盘较旧、具备病毒扫描程序且互联网链接很快这几种情形相结合的状况下,从网络获取资源比访问磁盘更快。不过,若是在用户设备上具备相关内容时访问网络会浪费流量,请记住这一点。

// Promise.race is no good to us because it rejects if
// a promise rejects before fulfilling.Let's make a proper
// race function:
function promiseAny(promises) {
  return new Promise((resolve, reject) => {
    // make sure promises are all promises
    promises = promises.map(p => Promise.resolve(p));
    // resolve this promise as soon as one resolves
    promises.forEach(p => p.then(resolve));
    // reject if all promises reject
    promises.reduce((a, b) => a.catch(() => b))
      .catch(() => reject(Error("All failed")));
  });
};

self.addEventListener('fetch', function(event) {
  event.respondWith(
    promiseAny([
      caches.match(event.request),
      fetch(event.request)
    ])
  );
});
复制代码

Network First

适合于: 快速修复(在该“版本”的网站外部)频繁更新的资源。 例如,文章、头像、社交媒体时间表、游戏排行榜。

这意味着您为在线用户提供最新内容,但离线用户会得到较旧的缓存版本。 若是网络请求成功,您可能须要更新缓存条目。

不过,此方法存在缺陷。若是用户的网络时断时续或很慢,他们只有在网络出现故障后才能得到已存在于设备上的彻底可接受的内容。这须要花很长的时间,而且会致使使人失望的用户体验。 请查看下一个模式,缓存而后访问网络,以得到更好的解决方案。

self.addEventListener('fetch', function(event) {
  event.respondWith(
    fetch(event.request).catch(function() {
      return caches.match(event.request);
    })
  );
});
复制代码

Cache then network

适合于: 频繁更新的内容。例如,文章、社交媒体时间表、游戏排行榜。

这须要页面进行两次请求,一次是请求缓存,另外一次是请求访问网络。 该想法是首先显示缓存的数据,而后在网络数据到达时更新页面。

有时候,当新数据(例如,游戏排行榜)到达时,您能够只替换当前数据,可是具备较大的内容时将致使数据中断。从根本上讲,不要使用户正在读取或交互的内容“消失”。

Twitter 在旧内容上添加新内容,并调整滚动位置,以便用户不会感受到间断。 这是可能的,由于 Twitter 一般会保持使内容最具线性特性的顺序。 我为 trained-to-thrill 复制了此模式,以尽快获取屏幕上的内容,但当它出现时仍会显示最新内容。

页面中的代码:

var networkDataReceived = false;

startSpinner();

// fetch fresh data
var networkUpdate = fetch('/data.json').then(function(response) {
  return response.json();
}).then(function(data) {
  networkDataReceived = true;
  updatePage();
});

// fetch cached data
caches.match('/data.json').then(function(response) {
  if (!response) throw Error("No data");
  return response.json();
}).then(function(data) {
  // don't overwrite newer network data
  if (!networkDataReceived) {
    updatePage(data);
  }
}).catch(function() {
  // we didn't get cached data, the network is our last hope:
  return networkUpdate;
}).catch(showErrorMessage).then(stopSpinner);
复制代码

ServiceWorker 中的代码:

咱们始终访问网络并随时更新缓存。

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.open('mysite-dynamic').then(function(cache) {
      return fetch(event.request).then(function(response) {
        cache.put(event.request, response.clone());
        return response;
      });
    })
  );
});
复制代码

Generic fallback

若是您未能从缓存和/或网络提供一些资源,您可能须要提供一个常规回退。

适合于: 次要图像,如头像、失败的 POST 请求、“Unavailable while offline”页面。

self.addEventListener('fetch', function(event) {
  event.respondWith(
    // Try the cache
    caches.match(event.request).then(function(response) {
      // Fall back to network
      return response || fetch(event.request);
    }).catch(function() {
      // If both fail, show a generic fallback:
      return caches.match('/offline.html');
      // However, in reality you'd have many different
      // fallbacks, depending on URL & headers.
      // Eg, a fallback silhouette image for avatars.
    })
  );
});
复制代码

若是您的页面正在发布电子邮件,您的 ServiceWorker 可能回退以在 IDB 的发件箱中存储电子邮件并进行响应,让用户知道发送失败,但数据已成功保存。

ServiceWorker-side templating

适合于: 没法缓存其服务器响应的页面。

在服务器上渲染页面可提升速度,但这意味着会包括在缓存中没有意义的状态数据,例如,“Logged in as…”。若是您的页面由 ServiceWorker 控制,您可能会转而选择请求 JSON 数据和一个模板,并进行渲染。

importScripts('templating-engine.js');

self.addEventListener('fetch', function(event) {
  var requestURL = new URL(event.request);

  event.respondWith(
    Promise.all([
      caches.match('/article-template.html').then(function(response) {
        return response.text();
      }),
      caches.match(requestURL.path + '.json').then(function(response) {
        return response.json();
      })
    ]).then(function(responses) {
      var template = responses[0];
      var data = responses[1];

      return new Response(renderTemplate(template, data), {
        headers: {
          'Content-Type': 'text/html'
        }
      });
    })
  );
});
复制代码

总结

没必要选择上述的某一个方法,能够根据请求网址使用其中的多个方法。

参考文献

developers.google.com/web/fundame… developers.google.com/web/tools/w…

相关文章
相关标签/搜索