PWA官方文档翻译之二Your First Progressive Web App

1.介绍

  • PWA结合了最好的web体验和最好的app体验,它对第一次使用某个app的用户来说是非常有用的,因为不需要安装用户仅在浏览器中访问就可以,并且随着用户在PWA上的操作越来越多,交互越来越频繁,PWA会变得越来越强大,即便在不稳定的网络下,它也可以快速加载,并且可以向用户发送通知,以及在主屏幕创建一个图标,并且可以全屏使用,提供沉浸式的体验。
什么是PWA
  • 渐进式的,每个人都可以使用PWA,无论你使用什么浏览器,因为PWA的最终是想渐进式的增强你的用户体验。
  • 多平台,PWA适用于个人PC,平板式设备,智能手机,甚至我们不知道的下一种设备。
  • 独立的网络连接,增强式的服务使PWA可以在无线环境下或网络及其不稳定的环境下工作。
  • 类本地应用,因为PWA就是按照本地app来设计的,所以你会觉着你在使用一个本地app。
  • 保持最新,serive worker使得应用总是保持在最新版本的状态。
  • 安全,PWA使用https进行通信加密,防止了被第三方获取数据以及数据被篡改。
  • 寻找方式非常简单,通过W3Cmanifests缓存的数据和serive worker的登记,PWA可以非常容易的搜索引擎里找到打开。
  • 可复用性,通过PWA推送的通知,用户可以再次访问PWA。
  • 可留存性,允许用户将PWA在桌面上创建图标,并且不必到应用商店去下载搜索下载应用。
  • 易分享,通过URL就可以将PWA分享出去,不需要复杂的安装。
下面的代码实例将会和你一起创一个PWA,包括PWA创建的设计及规范以及注意事项,来确保你的PWA符合应用标准。
我们将要做什么

在这个代码实例中,你将学会使用PWA技术去建立一个天气APP,你将学到:

  • 怎么用"app shell"去设计和开发一个PWA。
  • 怎么让你的app可以离线工作。
  • 如何存储数据以便在离线时也可以使用。
环境和要求
  • 谷歌浏览器版本52或更高
  • Web server for chrome或其他的网络服务器。
  • 实例代码
  • 代码编辑器
  • html,css,javascript以及调试工具的基本知识。

2.开始开发

下载源码,你可以下载本项目的代码通过 项目代码

解压你下载压缩文件,将会解压出来一个(your-first-pwapp-master)文件夹,这个文件夹包含了所有这个项目所需要的资源文件。
而名为step-NN的文件夹包含了这个项目所需要的步骤,你可以把它当做参考。

安装web server for chrome。

你可以通过chrome应用商店安装web server for chrome(具体方式是不可描述的—译者语)。
安装完成后,点击


9efdf0d1258b78e4.png

在chrome应用商店会出现这个图标


icon.png

点击它,你会看到下面的对话框,来配置你的本地web服务器。
home.png

点击CHOOSE FOLDER按钮,选择工作文件夹,就是刚才解压出来的文件夹,这样可以让你在调试中,直观地看出url来观察PWA的运行。

在选项中勾上Automatically show index.html,如图所示。


然后通过Web Server:STARTED来开启服务。

homescreen.png

现在你就可以看到打开的第一个画面,使用浏览器访问工作文件夹就可以(点击高亮的web server url)
显然这个页面上什么也没有,它只是这个app的小骨架,接下来我们将会给这个app添加UI和各种功能。

3.开发你的APP Shell

什么是app shell
intro.png

app的shell包含了构建一个PWA所需要的最基本的html,css,javascript文件,并且是确保app有良好的性能的必要组件之一,它的第一次加载非常的快速,并且第一次加载后就能被缓存下来,这意味着在app shell第一次加载完成后,用户再打开app后,app shell将从本地缓存中加载,这是非常快速的。
app shell架构将app的基础架构和ui分离,所有的基础架构和ui都将在本地缓存,这样在后续加载的时候,PWA只需要检索必要的数据,并不需要再次加载所有数据。
换句话说,app shell相当于那些被存于应用商店从来没有被打开过的app,其中没有数据,一旦打开这个app就会记录数据并且使用。

为何要使用app shell架构?

使用app shell架构,可以使你专注于速度,并且赋予PWA近似于本地app的属性,热加载和定期更新,但是不需要应用商店。

开发 app shell

第一步是设计核心组件
问问自己?

  • 什么是需要立刻呈现在屏幕上的
  • 这个app需要什么重要的ui组件
  • app shell需要什么js,css以及图片等
    我们将开发一个天气app作为我们的第一个progress web app,关键的组件包括:
  • 头部的标题,添加以及刷新按钮
  • 天气预报的卡片式容器
  • 卡片式模板
  • 添加城市时的对话框
  • 加载时的动画效果
wet.png

当你在设计更加复杂的应用时,第一次加载时可以不必加载不需要的资源,例如我们第一次可以不加载添加城市弹出的对话框,只有用户在发起点击时开始加载。

4.开始实现你的APP Shell

你的项目可以通过多种项目开始,我们通常使用Web Starter Kit,但是在这个例子里,为了让你专注于PWA的开发,我们为你提供可了所有的资源。

创建app shell的html部分

现在我们将添加app shell 架构的核心部分,组件包括:

  • 头部的标题,添加以及刷新按钮
  • 天气预报的卡片式容器
  • 卡片式模板
  • 添加城市时的对话框
  • 加载时的动画效果
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Weather PWA</title>
  <link rel="stylesheet" type="text/css" href="styles/inline.css">
</head>
<body>
  <header class="header">
    <h1 class="header__title">Weather PWA</h1>
    <button id="butRefresh" class="headerButton"></button>
    <button id="butAdd" class="headerButton"></button>
  </header>

  <main class="main">
    <div class="card cardTemplate weather-forecast" hidden>
    . . .
    </div>
  </main>

  <div class="dialog-container">
  . . .
  </div>

  <div class="loader">
    <svg viewBox="0 0 32 32" width="32" height="32">
      <circle id="spinner" cx="16" cy="16" r="14" fill="none"></circle>
    </svg>
  </div>

  <!-- Insert link to app.js here -->
</body>
</html>

这是工作目录的index.html文件(这只是整体项目的一部分,它已经存在了,不需要再赋值。)

注意!默认情况下,加载动画是存在的,确保用户在加载页面时可以立即看到加载器,让用户清楚地明白内容正在被加载。
为了节省时间,我们已经创建了样式表文件供你使用。

开始主要的javascript内容

现在主要的ui已经成功的构建起来了(上文所提到的样式表文件),看起来应该添加一些代码让他开始工作了,就像前文所说,哪些文件应该在首次运行的时候就应该被加载,而哪些可以延时加载。
在你的工作目录里,打开,script/app.js文件

  • 一个包含整个PWA的关键信息的app对象。
  • 添加,刷新,取消城市的监听函数(add/refresh,add/cancel)。
  • 添加或者更新天气预报的方法(app.updateForecastCard)。
  • 一个更新所有卡片数据的方法(app.getForecast,app.updateForecasts)。
  • 一个从Firebase公开的天气API上获取数据的方法(app.updateForecasts)。
  • 一些作为实例的假数据(fakeForecast)。
测试一下

现在你已经完成了完整的html,css和javascript,是时候去测试这个app了。
如果你想看看假数据是怎么被渲染的,可以在index.html中取消以下代码的注释。

<!--<script src="scripts/app.js" async></script>-->

然后从app.js中取消一下代码的注释

// app.updateForecastCard(initialWeatherForecast);

然后刷新下你的应用,你将会看到下面这个漂亮的卡片。

ex.png

当你验证其工作正常后,可以app.updateForecastCard的假数据清除,我们仅仅是想确保每个组件都可以正常工作。

5.从第一次快速加载开始

Progressive Web Apps 应该快速启动并且立即可以使用,在当前的状态下,我们的天气app启动的非常快速,但是它还是不能够使用,因为没有数据,这时我们可以创建一个ajax请求来获取数据,但是额外的请求就会使加载时间变长,我们想一个办法,在初次加载的时候,就给用户提供真实的数据。

加入天气预报数据

在这个代码实例中,我们模拟服务器直接将数据注入javascript,但是在用户的设备上运行的时候,最新的天气预报数据将根据用户的ip来确定位置以便注入。
代码已经包括了我们要注入的数据,就是我们上一步所使用的方法(initialWeatherForecast)。

如何区分是不是首次运行

但是我们不知道什么时候展示这些信息,将数据版存到本地以供下次使用么?如果用户下次使用,城市发生了变更该如何,我们需要的是加载本城市的信息,而不是之前的城市。
用户的第一选项如已经订阅的城市列表应该使用IndexedDB或者其他快速的存储方式存储到本地,但是为了简化代码实例,我们使用了localstorage的方法来存储数据,但是这在实际运行中并不是理想的环境,因为它是阻塞型同步机制,在某些设备上可能会很慢。
下面让我们来添加存储用户订阅城市的代码,找到以下注释

// TODO add saveSelectedCities function here

然后将下列代码复制到该注释下,如下

//  TODO add saveSelectedCities function here
app.saveSelectedCities = function() {
    var selectedCities = JSON.stringify(app.selectedCities);
    localStorage.selectedCities = selectedCities;
};

接下来我们需要添加一些代码检查用户是否已经添加了某城市,并且渲染这些城市的数据,找到以下注释。

// TODO add startup code here

然后在注释下添加这些代码

/************************************************************************
   *
   * Code required to start the app
   *
   * NOTE: To simplify this codelab, we've used localStorage.
   *   localStorage is a synchronous API and has serious performance
   *   implications. It should not be used in production applications!
   *   Instead, check out IDB (https://www.npmjs.com/package/idb) or
   *   SimpleDB (https://gist.github.com/inexorabletash/c8069c042b734519680c)
   ************************************************************************/

  app.selectedCities = localStorage.selectedCities;
  if (app.selectedCities) {
    app.selectedCities = JSON.parse(app.selectedCities);
    app.selectedCities.forEach(function(city) {
      app.getForecast(city.key, city.label);
    });
  } else {
    /* The user is using the app for the first time, or the user has not
     * saved any cities, so show the user some fake data. A real app in this
     * scenario could guess the user's location via IP lookup and then inject
     * that data into the page.
     */
    app.updateForecastCard(initialWeatherForecast);
    app.selectedCities = [
      {key: initialWeatherForecast.key, label: initialWeatherForecast.label}
    ];
    app.saveSelectedCities();
  }

该代码用来检查该城市是否保存在订阅列表中,如果有,渲染出该城市的卡片,如果没有则渲染假数据,并保存到默认卡片中。

保存所添加的数据

最后,你需要添加 ’添加城市‘ 按钮,将所要的添加的城市保存到本地。
更新 butAddCity 里的代码如下

document.getElementById('butAddCity').addEventListener('click', function() {
    // Add the newly selected city
    var select = document.getElementById('selectCityToAdd');
    var selected = select.options[select.selectedIndex];
    var key = selected.value;
    var label = selected.textContent;
    if (!app.selectedCities) {
      app.selectedCities = [];
    }
    app.getForecast(key, label);
    app.selectedCities.push({key: key, label: label});
    app.saveSelectedCities();
    app.toggleAddDialog(false);
  });

如果该app存在将会初始化app.selectedCities,并且执行app.selectedCities.push() 和app.saveSelectedCities()。

测试
  • 当第一次运行时,应用立刻向用户展示,initialWeatherForecast 中的天气数据。
  • 添加一个新的城市后确保会展示两个卡片。
  • 刷新浏览器来确保加载了最新的数据。

6使用serive worker来缓存app shell

Progressive Web Apps是非常快速并且可以加载在本地的,这意味着它可以在在线,离线,以及不稳定的网络情况下使用,为了实现这个目标,我们使用了一个serive worker(PWA服务)来缓存app shell,来确保始终保持可用状态并且机器可靠。
如果你对service worker不熟悉,你可以通过阅读service worker 来了解它可以做什么并且它的生命周期是怎么工作的等等,如果你完成看了这个代码实例,一定要查看 Debugging Service Workers code lab 来深入了解。
service workers提供了一种奖金是增强的特征,这些特性仅仅作用于支持service workers的浏览器,比如,使用service workers你可以缓存app shell和你的应用所需要的数据,所以这些数据即便在离线的环境下也可以工作。如果浏览器不支持service workers,支持离线的代码并没有工作,用户也能得到一个基本的用户体验,并且检测你所使用的浏览器的时候会花费近本可以忽略不计的性能,对你所使用的浏览器也没有任何影响。

注册service worker

为了让应用可以离线工作,要做的第一件事情就是注册一个service worker,这是一段在后台运行的脚本程序,并不要用户去打开它,也不需要任何的操作。
这只需要两步

  1. 创建一个js文件来运行service worker。
  2. 声明这个js文件是service worker。
    第一步,在跟目录下创建一个空文件叫做service-worker.js。这个文件必须放在根目录。因为service worker的作用域范围是跟它所在的位置来决定的。
    然后,需要检查浏览器是不是支持service worker,如果支持会注册service worker,将下面的代码添加至app.js中。
if('serviceWorker' in navigator) {  
    navigator.serviceWorker  
        .register('/service-worker.js')  
        .then(function() { console.log('Service Worker Registered'); });  
}
缓存站点的资源

当service worker被注册以后,用户首次访问页面的时候,一个install事件函数就会被触发。在这个事件的回调函数中,我们能够缓存所有的应用需要用到的资源。
当service worker被激活后,他应该打开缓存的对象,并且将其所需要i的资源存储进去。将下面的代码加入到service-worker.js(你可以在your-first-pwapp-master/work中找到) :

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对象,并且定义一个cache的名称,这样我们就可以给cache文件迭代本本,或者将数据分离,以至于我们能够轻松地升级数据而不影响其他数据。
一旦cache数据被打开,我们可以调用 cache.addAll() 并且往其中传入一个url列表,然后加载这些资源。但是,一旦 cache.addAll() 操作失败,那么整个cache加载都会失败(原文是cache.addAll() is atomic,我的理解是单线程的,就是说一旦其中一个缓存失败,那么其他都会无法继续--译者)。
ok,让我们来熟悉控制台并学习怎么调试service workers。在你刷新页面之前,打开控制台,找到Appliction,并且打开Service worker的选项。如图

dev.png

如果你看到的是上图这样的页面的话,说明你打口的页面没有已经注册的Service worker
你需要重新加载页面,Service worker的选项应该如下图所示。

dev2.png

当你看到控制台如上图显示的话,这意味着service worker已经正在工作了。

现在让我们开始展示你在使用service worker可能遇到的问题,为了演示问题所在,请在service-worker.js的install事件下面添加一个activate监听器。

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

在service-worker开始运行的时候,activate监听事件就会被激活。
打开控制台,然后重新刷新下网页,然后找到Application选项,找到service workers选项,在已经被激活的service workers上,点击inspect(如果你没找到点击show all,然后每个服务都有一个start,点击start就有inspect了-译者语),理论上说,控制开会出现[ServiceWorker] Activate这样的信息,但是并没有出现,现在你回到service worker选项,你会发现一个新service worker正处于等待的状态(包括activate监听事件也在这个状态)。

listen.png

一般来说,只要页面的还有一个tab的话,service worker会一直工作,所以你可以关闭然后再重新打开页面或者点击skipWaiting按钮,但是有一个更加简单的办法可以让你不必这么麻烦的操作,启用update on reload(在service worker下第一行第二个选项--译者语),当你启用update on reload这项服务后,页面在每次刷新后都会强制更新。

现在开启update on reload并且确定新service worker已经被激活了。

注意:你可能会看到service worker出现一个错误,忽略这个错误就行,它是安全的(如下图)。
error.png

以上就是控制台关于调试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();
});

一旦你的app shell变化,上述的代码会确保你的service worker缓存也可以跟着更新。
最后,让我们更新一下app shell所要求的文件列表,这需要你的app所用的所有文件,包括图片,js文件,css文件等,在你的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其实还不能工作,我们已经缓存了app shell的组件,但是我们仍然需要从本地缓存中加载他们。

从缓存中加载app shell

service worker可以收到我们从PWA中发起的请求,并且响应,这意味着我们可以怎样处理这些请求,并且什么样的请求可以被缓存下来。
例如:

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

让我们来更新app shell,将下面的代码加入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()返回给页面。

测试吧

现在你的app已经可以在离线模式下使用了!让我们来试一试吧!
首先你要刷新下你的页面,然后点击Application面板找到cache storage,并且展开该部分,你应该在左边会看到你的app shell缓存的名称。当你惦记你的app shell缓存,你将会看到所有已经被缓存的资源。

test1.png

现在,让我们开始离线测试。回到控制台的service worker选项,启动offline的复选框,你将会看到network选项边上有一个黄色的警告图标,这表示你处于离线状态。

test2.png

然后刷新页面,你就会发现你的页面也可以正常的去操作。

test3.png

而下一步就是修改app本身的和service worker的逻辑,让天气对象的数据可以被保存下来,并且可以在app处于离线状态的时候,将最新的缓存数据显示出来。
TIPS:如果你要清除所有保存的数据的话(localstorage,indexDB以及缓存的文件),并且删除任何service worker,你可以控制台的Application选项上点击Clear storage清除数据。

当心边界条件问题

之前提到过,这段代码一定不要用在生产环境下,因为有许多的边界条件问题。

  • 缓存依赖于每次修改内容后缓存键的改变
    例如,缓存的方法要求你在每次内容变化之后更新键值,否则,缓存不会变化,并且重新提供旧的内容,所以确保你的项目中键值在每次内容更新后都会变化。

  • 每次修改后缓存的资源都会被重新下载
    另一个问题就是,当一个文件被修改后,整个缓存也要被重新的下载。这就意味着你即使有一个简单的拼写错误,也会让整个缓存重新的下载,这很影响效率。

  • 浏览器的缓存可能会阻止service worker缓存的更新
    还有一个重要的问题,第一次访问的时候,请求的资源是直接经过htpps加密的,这个时候可能不会返回缓存的资源,除此之外,浏览器可能返回旧的缓存资源,这就导致了service worker不会被更新。

  • 在生产环境中采取cache-first的策略
    我们的app使用了优先缓存的策略,这导致了所有的后续请求,都会从缓存中返回而不会去请求网络。cache-first的策略很容易实现,但是也会为将来带来诸多问题。一旦主页和注册的service worker被缓存下来,去修改service worker的配置非常困难(因为service worker的配置依赖于它的位置),你就会发现你的app很难进行迭代升级。

我应该如何避免这些问题呢。

我们应该如何避免这些问题呢?比如使用一个叫sw-precache的库,他可以帮助你精密地控制资源的生命周期,能够确保请求直接访问网络,并且帮你处理所有棘手的问题。

实时调试service worker

调试service worker是一件有挑战性的东西,当你涉及到缓存之后,你想要缓存进行更新,但是实际上它没有进行更新,事情就会变得像一场噩梦一样。在service worker典型的生命周期和你的代码之间,你很快就会受挫。但是幸运的是,有一些工具可以帮助你处理这些事情。

重新开始

TIPS:如果你要清除所有保存的数据的话(localstorage,indexDB以及缓存的文件),并且删除任何service worker,你可以控制台的Application选项上点击Clear storage清除数据。(上面有了,只是又出现了,只好跟着又翻译一遍--译者语)

一些其他的问题
  • 一旦service worker被注销掉,它会一直保留直到浏览器被关闭。
  • 如果你打开了多个窗口,除非你对其进行了刷新,使用了新的service worker,新的service worker才会开始工作,
  • 注销一个service worker不会清空缓存,所以如果缓存的键值没有进行修改的话,你可能获得的还是旧的数据。
  • 如果一个service worker已经存在,除非你使用immediate control的方式或者刷新页面,否则新注册的service worker不会立即接替控制。

7.使用Service Worker来缓存应用数据

选择一个正确的 caching strategy 很重要,它取决你在应用中使用的数据类型。比如像天气信息,股票信息这种对实时性要求很高的数据,应该经常被刷新,但是例如用户的头像或者文字的内容应该以较低的频率来刷新一样。
cache-first-then-network这个策略是一个理想的选择(钦点的(๑• . •๑)--译者语),这个方法拿到数据非常的快速,然后返回新的数据,与现请求网络再去缓存相比,用户不需要等很长时间就可以拿到数据。
cache-first-then-network需要我们发起两个异步请求,一个请求缓存,一个请求网络。我们应用中的网络请求不能被修改,但是我们要修改一下service-worker.js的缓存请求代码。
一般情况下,请求应该立即返回缓存的数据,提供app能够使用的最新的数据,然后当网络请求返回后存到缓存中以供下次调用。(就是说刷新一下,拿到的是上次网络请求返回的数据,而后台这边又会发起一个请求拿到目前最新的数据保存到缓存中,一定程度上解决了网络请求慢的问题--译者语)

拦截网络请求之后使用缓存来响应

我们需要修改service worker来拦截对天气API的请求,然后把其请求该API的结果存储下来,以便我们以后调用,那么在 cache-first-then-network的策略下,我们希望请求返回给我们的是最新的数据,如果不是,那么也没事,因为我们已经把数据存在了缓存里,直接调用就行了。
在service worker里,我们添加一个dataCacheName变量,以至于我们可以从app shell中将应用数据分离出来。当你的app shell更新了,其缓存消失了,但是你的数据还在,不会受到影响,并可以随时调用。记住,若是将来你的数据格式改变了,你需要一种能让app shell和应用数据保持同步的办法。
将下面的代码加入到service-worker.js中
var dataCacheName = 'weatherData-v1';
接下来,我们需要更新activate的回调,以防删除appshell的缓存之后,应用数据也会被删除。
if (key !== cacheName && key !== dataCacheName) {
(原文中就是这样的,不过我认为应该是少了一个括号--译者语)
最后,来修改fetch事件的回调,添加代码来将请求数据API和其他的请求分开来。

self.addEventListener('fetch', function(e) {
  console.log('[Service Worker] Fetch', e.request.url);
  var dataUrl = 'https://query.yahooapis.com/v1/public/yql';
  if (e.request.url.indexOf(dataUrl) > -1) {
    /*
     * When the request URL contains dataUrl, the app is asking for fresh
     * weather data. In this case, the service worker always goes to the
     * network and then caches the response. This is called the "Cache then
     * network" strategy:
     * https://jakearchibald.com/2014/offline-cookbook/#cache-then-network
     */
    e.respondWith(
      caches.open(dataCacheName).then(function(cache) {
        return fetch(e.request).then(function(response){
          cache.put(e.request.url, response.clone());
          return response;
        });
      })
    );
  } else {
    /*
     * The app is asking for app shell files. In this scenario the app uses the
     * "Cache, falling back to the network" offline strategy:
     * https://jakearchibald.com/2014/offline-cookbook/#cache-falling-back-to-network
     */
    e.respondWith(
      caches.match(e.request).then(function(response) {
        return response || fetch(e.request);
      })
    );
  }
});

上面的代码对请求进行拦截,判断请求的URL是否为天气API,如果是的话,我们就使用fetch发起新的请求,一旦有响应返回,就会将其存入到缓存,然后把请求返回给原请求。
其实应用到现在还不能正式运行呢(啊,我去--译者语),我们虽然已经实现了从app shell拿取缓存,但是即使我们缓存了数据,依然需要通过网络来发起请求。(第一次需要通过网络,但是在这之后,你每次查看天气,它不光会缓存目前的,还会缓存未来几个小时的,也就是说未来几个小时即使你没有网络也能看天气,哇,牛!--译者语)

发起网络请求

之前提到过,app需要启动两个异步请求,一个访问缓存,一个访问网络。可以访问最新的缓存亦可以访问网络,这就是渐进式增强的一个很好的例子,因为缓存可能不是在所有的浏览器都可以使用,若不能使用的话,网络请求仍然可以很好地使用。

为了实现网络请求,我们需要做
  • 检查window全局对象是否有caches对象
  • 向缓存发起请求
  • 如果服务器的请求没有返回任何结果,需要使用本地缓存。
  • 向服务器发起请求。
  • 保存在数据本地以便调用。
  • 热更新。
从缓存抓取数据

接下来我们要检查是否存在caches这个对象并且拿到最新的缓存数据,找到TODO add cache logic here comment 中,它在app.getForecast()方法中,加入下面的代码

 if ('caches' in window) {
      /*
       * Check if the service worker has already cached this city's weather
       * data. If the service worker has the data, then display the cached
       * data while the app fetches the latest data.
       */
      caches.match(url).then(function(response) {
        if (response) {
          response.json().then(function updateFromCache(json) {
            var results = json.query.results;
            results.key = key;
            results.label = label;
            results.created = json.query.created;
            app.updateForecastCard(results);
          });
        }
      });
    }

这时应用会做出两个请求,一个是XHR到天气API,一个是缓存请求,若缓存中有数据,其返回非常快,然后更新卡片的数据,这都是当天气API的请求未返回的情况下,一旦天气数据从服务端返回,就会更新卡片的信息。

注意!我们知道网络请求和缓存请求都更新了天气数据,但是app怎么知道哪一个是最新的呢?下面的代码就解决了这个问题,将它添加到app.updateForecastCard:里。

 var cardLastUpdatedElem = card.querySelector('.card-last-updated');
    var cardLastUpdated = cardLastUpdatedElem.textContent;
    if (cardLastUpdated) {
      cardLastUpdated = new Date(cardLastUpdated);
      // Bail if the card has more recent data then the data
      if (dataLastUpdated.getTime() < cardLastUpdated.getTime()) {
        return;
      }
    }

每次卡片数据更新后,app存储的时间戳卡片的数据中,app只要判断时间戳是不是已经存在就行了。

试一试吧

现在这个app已经可以实现完整的离线功能了,保存几个城市,然后点击刷新按钮来刷新数据,然后在离线的环境下刷新新app再试试。
之后去cache storage页面(控制台的application下),展开观察,在左边,你应该可以看到你的app shell缓存的名称,当你点击它,你会看到所有已经被缓存的资源。

fin.png

8.如何在原生应用集成PWA

9.上线和庆祝。。。

未完待续。。。。

推荐阅读更多精彩内容