6.使用service workers预缓存应用壳

PWA必须快和可安装,这意味着它们可以在联网状态,断网状态和慢速网络状态都可以工作。为了达成这个,我们需要通过service worker技术来缓存我们的应用壳,那样这些应用就总是使用起来快速和可信赖。

如果你还对service workers不熟悉,你可以通过阅读Introduction To Service Workers获取关于它们可以做什么,它们的生命周期是怎样的以及更多基础知识。一旦你完成了这个实验,确保自己去阅读了Debugging Service Workers code lab,来获取更多的深入关于service workers是如何工作的知识。

通过service workers提供的特性,需要考虑渐进增强,并且当且仅当浏览器支持时才被添加到应用中。例如,有了service workers你可以对你的应用缓存应用壳和数据,以便在无网络状况下仍然可以被访问。当service workders不被支持时,离线代码不被调用,用户仍然可以获得一个基本的体验。使用特性检测来渐进增强几乎没有开销且应用不会在哪些不支持该特性的老旧浏览器上崩溃。

记住:service worker功能只在那些通过HTTPS访问的页面上起作用(http://localhost和等效的其他类似本地服务也可以工作)。想了解这个规定背后的逻辑,请查阅Chromium team的Prefer Secure Origins For Powerful New Features文章。

service worker可用的情况下注册它

让你的应用可以在离线下使用的第一步就是注册一个service worker,一段可以后台运行且不需要打来一个网页或做任何交互的脚本。

这需要两个步骤:

  • 告诉浏览器注册一个JavaScript文件作为service worker;
  • 创建一个包含service worker代码的JavaScript文件;

首先,我们需要检查浏览器是否支持service workers,如果支持,就注册service worker。添加如下代码到app.js(在// TODO add service worker code here注释下面):

  if ('serviceWorker' in navigator) {
    navigator.serviceWorker
             .register('./service-worker.js')
             .then(function() { console.log('Service Worker Registered'); });
  }

缓存站点资源

当用户第一次访问时,service worker就会注册,然后一个install事件就会被触发。在这个事件处理器里,我们将缓存哪些对应用来说必要的资源。

以下的代码不能够在正式生产时使用,它只覆盖了最基本的使用情况,而且很容易让你陷入一个你的应用壳不会更新的状态。请确保复习以下区域关于讨论怎么在实现的时候规避这个陷阱。

当service worker被启动了,它将打开caches对象然后通过其获取必要的资源来加载应用壳。在你的应用的根目录(它应该是your-first-pwapp-master/work文件夹)下创建一个service-worker.js文件。这个文件需要在你的应用的根目录下,因为service workers的作用域是通过确定该文件所在的目录来定义的。添加如下代码到你的service-worker.js文件中:

var cacheName = 'weatherPWA-step-6-1';
var filesToCache = [];

self.addEventListener('install', function(e) {
  console.log('[ServiceWorker] Install');
  e.waitUntil(
    caches.open(cacheName).then(function(cache) {
      console.log('[ServiceWorker] Caching app shell');
      return cache.addAll(filesToCache);
    })
  );
});

首先,我们需要用caches.open()方法且提供一个缓存名字来打开缓存。提供一个缓存名字允许我们版本化我们的文件,或者将数据从应用壳分离以便我们能够轻松的更新一个而不会影响到其他。

一旦缓存打开,我们可以调用cache.addAll()方法,该方法可传入一个URLs列表,然后就会从服务器端获取并将响应添加到缓存中。不幸的是,cache.addAll()方法是脆弱的,如果任何一个文件失败,整个缓存步骤将失败!

好吧,让我们开始熟悉怎样使用DevTools来理解和debug service workers。在重载你的页面之前,打开DevTools,在Application面板去到Service Worker栏,像下面这样。

当你看到一个空白的页面像这样,意味着当前打开的页面并没有任何注册的service workers。

现在,刷新你的页面,Service Workder栏将像下面这个样子。

当你看到这样的信息,它意味着当前页面有一个service worker在跑着。

好了,现在我们将演示在开发service workers中可能遇到的一个知识点。为了演示,让我们在你的service-worker.js文件中在install事件监听器下添加一个activate事件监听器。

self.addEventListener('activate', function(e) {
  console.log('[ServiceWorker] Activate');
});

这个activate事件是当service worker开始工作的时候触发的。

打开DevTools的console控制台并重载你的页面,在Application面板切换到Service Worker栏并在激活的service worker上点击inspect。你将期望看到[ServiceWorker] Activate信息输出到console控制台,但是并没有发生。查阅你的Service Worker栏,你可以看到新的service worker(包括激活的事件监听器)正处于“waiting”的状态。

基本来说,旧的service worker将持续控制这个页面直到打开一个新的tab页。所以,你可以关闭或者重新打开这个页面,或者按下skipWaiting按钮,但是一个长久的解决方案是去在DevTools上的Service Worker栏下,选中Update on Reload选择框。当这个选择框选中后,这个service worker将在每次页面刷新重载时强制更新。

现在选中Update on Reload选择框并刷新页面,以确认新的service worker已经激活了。

注意:你也许会在Application面板的Service Worker栏看到和下面相似的报错,忽略这个错误是安全的。

这就是目前关于在DevTools中检查和调试service workers的全部内容。我们将在后续展示一些更多的技巧。让我们回到你的app打造中。

让我们在activate事件监听器中扩展一些更新缓存的逻辑。更新你的代码如下:

self.addEventListener('activate', function(e) {
  console.log('[ServiceWorker] Activate');
  e.waitUntil(
    caches.keys().then(function(keyList) {
      return Promise.all(keyList.map(function(key) {
        if (key !== cacheName) {
          console.log('[ServiceWorker] Removing old cache', key);
          return caches.delete(key);
        }
      }));
    })
  );
  return self.clients.claim();
});

这些代码确保你的service worker在任何应用壳文件改变时,更新它的缓存。为了让这些代码工作,你需要在你的servicer worker文件上的顶部更新你的cacheName变量。

最后一条语句解决了一个极端情况,想了解更多可以阅读下面的引用。

当app已完成,self.clients.claim()解决了一个极端情况,这便是app可能不会返回最新的数据。你可以通过将这条语句注释并做一下步骤来复现这个极端情况:1)初次加载app以看到最初始的纽约城数据为准;2)点击app上的刷新按钮;3)使应用离线;4)重载app。你将期望看到更新的纽约城数据,但是你实际看到的是初始数据。这种情况发生,是应为service worker并没有激活。self.clients.claim()方法本质上帮助你更快的激活service worker.

最后,让我们更新应用壳所必须的文件列表。在这个数组中,我们将包含所有我们app所需要的文件,包括图片,JavaScript,样式,等等。在service-worker.js文件的顶部,用如下代码替换var filesToCache=[]:

var filesToCache = [
  '/',
  '/index.html',
  '/scripts/app.js',
  '/styles/inline.css',
  '/images/clear.png',
  '/images/cloudy-scattered-showers.png',
  '/images/cloudy.png',
  '/images/fog.png',
  '/images/ic_add_white_24px.svg',
  '/images/ic_refresh_white_24px.svg',
  '/images/partly-cloudy.png',
  '/images/rain.png',
  '/images/scattered-showers.png',
  '/images/sleet.png',
  '/images/snow.png',
  '/images/thunderstorm.png',
  '/images/wind.png'
];

确保包含所有的文件名,举个例子,我们的app是从index.html服务的,但是也可能通过/被请求到,因为当一个根目录被请求时我们的服务器也将发送index.html。你可以在fetch方法中处理这个,但是它将需要特殊处理,这或将带来些许复杂性。

我们的app目前还不能在离线条件下工作。我们缓存了我们的应用壳组件,但是我们仍然需要在我们的本地缓存中加载它们。

从缓存中获取应用壳

Service worker提供了从我们的PWA应用中拦截请求并处理的能力。这意味着我们可以决定我们想怎么处理请求和是否从我们的缓存的响应来作为下一个请求的响应。

举个例子:

self.addEventListener('fetch', function(event) {
  // Do something interesting with the fetch here
});

让我们从缓存中获取应用壳。添加如下代码到你的service-worker.js文件的底部:

self.addEventListener('fetch', function(e) {
  console.log('[ServiceWorker] Fetch', e.request.url);
  e.respondWith(
    caches.match(e.request).then(function(response) {
      return response || fetch(e.request);
    })
  );
});

caches.match()方法将对fetch事件触发的网页请求在缓存中匹配,看是否在缓存中存在当前的这个请求,它将要么返回缓存的版本,要么使用fetch方法去网上获取一份。然后这个响应将通过e.respondWith()方法传回到网页中。

如果你没有在console控制台看到[ServiceWorker]的打印,并确保你已经改变了cacheName变量且你正在通过在Application面板下的Service Worker栏查看的是正确的service worker且在正在运行中的service worker上点击了inspect按钮。如果这还不能工作,请看为调试service workers而写的Tips区域。

测试一下

你的app已经可以离线使用了。让我们来试一下吧。

重载你的页面,然后去到DevTools上的Application面板里的Cache Storage栏。点击Cache Storage展开然后你将在你左手侧看到你的应用壳名字缓存罗列在这里。当你点击你的应用壳缓存,你将看到当前缓存的所有资源。

现在,让我们测试离线模式。回到DevTools的Service Worker栏并选中Offline选择框。当你选中后,你可以看到在Network tab栏有一个晓得黄色警示。这意味着你已经处于离线。

重载你的页面然后...成功了!差不多吧,至少。注意它怎么加载的初始(伪造)的天气数据。

查看在app.getForecast()方法中的else语句来理解为什么app能够加载伪造的数据。

接下来的步骤是修改app的service worker的逻辑让其能缓存天气数据,并在离线时,返回最近的数据。

提示:使用在Application tab下的Clear storage栏来更新清除所有保存的数据(localStorage, indexDB)并移除所有的service workers。

留意一些小问题

如前面提到的,这些代码不能够在生产环境下使用,因为有许多没有处理的一些小问题。

缓存依赖每次变更的缓存键值

例如这个缓存方法需要你在每次内容更改时,去手动更新缓存key,否则,缓存将不会更新,且旧的内容会被使用到。所以请确保在每次你工作的项目更改时,同事更新缓存键值。

每次更新都需要所有的资源被下载下来

另外一个负面情况是每次文件更改,那么全部的缓存都将过时并且需要重新下载。这意味着修复一个简单的单个字母的拼写错误都将使得整个缓存失效并且需要重新全部下载。非常的不高效。

浏览器缓存可能阻止service worker的缓存不更新

这里有另外一个非常重要的警告。在service worker在安装过程中,一个HTTPS的请求直接到网络上去而不是从浏览器的缓存中返回一个响应至关重要。如果不是的话,浏览器将返回旧的缓存版本,结果就是导致service worker缓存永不更新了!

留意在生产环境缓存优先的策略

我们的应用采用一个缓存优先的策略,这将导致任何缓存的内容的返回都不必去预先请求网络。一个缓存优先的策略是很容易实现的,同时它也可以导致一些未来的挑战。一旦这个域名下的页面和service worker注册内容已经缓存,它将导致去更改service worker的配置(因为service worker的配置是由它被定义的地方决定的)极其困难,并且你将发现你自己部署的网站将极其难更新!

我要怎么避免这些小问题呢?

因此,我们要怎样去避免这些问题呢?使用一个库像sw-precache, 其将提供对过期的东东更好的控制,将确保请求直接去到网络并帮你处理那些困难的工作。

给测试实时的service workers的提示

调试service workers会非常的具有挑战性,当其纳入了缓存,当你期望缓存更新时,但是缓存并不更新,事情将变得比噩梦更可怕。在你的代码中出现典型的service worker生命周期bug,你可能立刻变得沮丧。但是请不要。这里还是有一些工具你可以使用的,让你的生活更轻松。

重新开始

在一些情况下,你可能发现你自己加载的是缓存数据或者它们根本如你所愿的更新。为了清除所有保存的数据(localStorage, indexDB数据,缓存的文件)和移除所有的service workers,使用在Application tab下的 Clear storage栏。

其他一些建议:

  • 一旦一个service worker被解除注册,它可能仍然存在列表中直到打开它的浏览器窗口被关闭;
  • 如果你的app在多个窗口中打开,新的service worker将不会起作用知道他们都被加载了且更新到了最新的service worker;
  • 解除注册一个service worker不会去清除缓存,因此如果缓存名没有更新,有可能你将继续拿到旧数据;
  • 如果一个service worker存在且一个新的service worker被注册了,新的service worker将不会起作用知道页面重载,除非你采取immediate control

results matching ""

    No results matching ""