【转载】你的首个 Progressive Web App

编辑推荐: 掘金是一个高质量的技术社区,从 CSS 到 Vue.js,性能优化到开源类库,让你不错过前端开发的每一个技术干货。 点击链接查看最新前端内容,或到各大应用市场搜索「 掘金」下载APP,技术干货尽在掌握中。

本文转载于google.com的《你的首个 Progressive Web App》一文,如需转载,请注明出处:https://developers.google.com/web/fundamentals/getting-started/codelabs/your-first-pwapp/

Progressive Web Apps 是结合了 Web 和 原生应用中最好功能的一种体验。对于首次访问的用户它是非常有利的, 用户可以直接在浏览器中进行访问,不需要安装应用。随着时间的推移当用户渐渐地和应用建立了联系,它将变得越来越强大。它能够快速地加载,即使在比较糟糕的网络环境下,能够推送相关消息, 也可以像原生应用那样添加至主屏,能够有全屏浏览的体验。

什么是 Progressive Web App?

Progressive Web Apps 是:

  • 渐进增强 - 能够让每一位用户使用,无论用户使用什么浏览器,因为它是始终以渐进增强为原则。
  • 响应式用户界面 - 适应任何环境:桌面电脑,智能手机,笔记本电脑,或者其他设备。
  • 不依赖网络连接 - 通过 service workers 可以在离线或者网速极差的环境下工作。
  • 类原生应用 - 有像原生应用般的交互和导航给用户原生应用般的体验,因为它是建立在 app shell model 上的。
  • 持续更新 - 受益于 service worker 的更新进程,应用能够始终保持更新。
  • 安全 - 通过 HTTPS 来提供服务来防止网络窥探,保证内容不被篡改。
  • 可发现 - 得益于 W3C manifests 元数据和 service worker 的登记,让搜索引擎能够找到 web 应用。
  • 再次访问 - 通过消息推送等特性让用户再次访问变得容易。
  • 可安装 - 允许用户保留对他们有用的应用在主屏幕上,不需要通过应用商店。
  • 可连接性 - 通过 URL 可以轻松分享应用,不用复杂的安装即可运行。

这引导指南将会引导你完成你自己的 Progressive Web App,包括设计时需要考虑的因素,也包括实现细节,以确保你的应用程序符合 Progressive Web App 的关键原则。

我们将要做什么?

你将会学到

  • 如何使用 "app shell" 的方法来设计和构建应用程序。
  • 如何让你的应用程序能够离线工作。
  • 如何存储数据以在离线时使用。

你需要

HTML,CSS 和 JavaScript 的基本知识 这份引导指南的重点是 Progressive Web Apps。其中有些概念的只是简单的解释 而有些则是只提供示例代码(例如 CSS 和其他不相关的 Javascipt ),你只需复制和粘贴即可。

设置

下载示例代码

你可以下载本 progressive web app 引导指南需要的所有代码

将下载好的zip文件进行解压缩。这将会解压缩一个名为(your-first-pwapp-master)的根文件夹。这文件夹包含了这指南你所需要的资源。

名为 step-NN 的文件夹则包含了这指南每个步骤的完整的代码。你可以把他当成参考。

安装及校验网络服务器

你可以选择其他的网络服务器,但在这个指南我们将会使用Web Server for Chrome。如果你还没有安装,你可以到 Chrome 网上应用店下载。

安装完毕后,从书签栏中选择Apps的捷径:

接下来点击Web Server的图标

你将会看到以下的窗口,这让你配置你的本地网络服务器:

点击 choose folder 的按钮,然后选择名为 work 的文件夹。这会把目录和文件都以HTTP的方式展示出来。URL地址可以在窗口里的 Web Server URL(s) 找到。

在选项中,选择"Automatically show index.html" 的选择框:

然后在 "Web Server: STARTED" 的按钮拉去左边,在拉去右边,以将本地网络服务器关闭并重启。

现在你可以使用游览器来访问那个文件夹(点击窗口内的Web Server URL下的链接即可)。你将会看到以下的画面:

很明显的,这个应用程序并没有什么功能。现在只有一个加载图标在那里转动,这只是来确保你的网络服务器能正常操作。在接下来的步骤,我们将会添加更多东西。

基于应用外壳的架构

什么是应用外壳(App Shell)

App Shell是应用的用户界面所需的最基本的 HTML、CSS 和 JavaScript,也是一个用来确保应用有好多性能的组件。它的首次加载将会非常快,加载后立刻被缓存下来。这意味着应用的外壳不需要每次使用时都被下载,而是只加载需要的数据。

应用外壳的结构分为应用的核心基础组件和承载数据的 UI。所有的 UI 和基础组件都使用一个 service worker 缓存在本地,因此在后续的加载中 Progressive Web App 仅需要加载需要的数据,而不是加载所有的内容。

换句话说,应用的壳相当于那些发布到应用商店的原生应用中打包的代码。它是让你的应用能够运行的核心组件,只是没有包含数据。

为什么使用基于应用外壳的结构?

使用基于应用外壳的结构允许你专注于速度,给你的 Progressive Web App 和原生应用相似的属性:快速的加载和灵活的更新,所有这些都不需要用到应用商店。

设计应用外壳

第一步是设计核心组件

问问自己:

  • 需要立刻显示什么在屏幕上?
  • 我们的应用需要那些关键的 UI 组件?
  • 应用外壳需要那些资源?比如图片,JavaScript,样式表等等。

我们将要创建一个天气应用作为我们的第一个 Progressive Web App 。它的核心组件包括:

在设计一个更加复杂的应用时,内容不需要在首次全部加载,可以在之后按需加载,然后缓存下来供下次使用。比如,我们能够延迟加载添加城市的对话框,直到完成对首屏的渲染且有一些空闲的时间。

实现应用外壳

任何项目都可以有多种起步方式,通常我们推荐使用 Web Starter Kit。但是,这里为了保持我们的项目 足够简单并专注于 Progressive Web Apps,我们提供了你所需的全部资源。

为应用外壳编写 HTML 代码

为了保证我们的起步代码尽可能清晰,我们将会开始于一个新的 index.html 文件并添加在 构建应用外壳中谈论过的核心组件的代码

请记住,核心组件包括:

  • 包含标题的头部,以及头部的 添加/刷新 按钮
  • 放置天气预报卡片的容器
  • 天气预报卡片的模板
  • 一个用来添加城市的对话框
  • 一个加载指示器

<!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>

需要注意的是,在默认情况下加载指示器是显示出来的。这是为了保证 用户能在页面加载后立刻看到加载器,给用户一个清晰的指示,表明页面正在加载。

为了节省你的时间,我们已经已创建了 stylesheet。

添加关键的 JavaScript 启动代码

现在我们的 UI 已经准备好了,是时候来添加一些代码让它工作起来了。像搭建应用外壳的时候那样,注意 考虑哪些代码是为了保持用户体验必须提供的,哪些可以延后加载。

在启动代码中,我们将包括(你可以在(scripts/app.js)的文件夹中找到):

  • 一个 app 对象包含一些和应用效果的关键信息。
  • 为头部的按钮(add/refresh)和添加城市的对话框中的按钮(add/cancel)添加事件监听 函数。
  • 一个添加或者更新天气预报卡片的方法(app.updateForecastCard)。
  • 一个从 Firebase 公开的天气 API 上获取数据的方法(app.getForecast)。
  • 一个迭代当前所有卡片并调用 app.getForecast 获取最新天气预报数据的方法 (app.updateForecasts)。
  • 一些假数据 (fakeForecast) 让你能够快速地测试渲染效果。

测试

现在,你已经添加了核心的 HTML、CSS 和 JavaScript,是时候测试一下应用了。这个时候它能做的可能还不多,但要确保在控制台没有报错信息。

为了看看假的天气信息的渲染效果,从index.html中取消注释以下的代码:

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

接下来,从 app.js中取消注释以下的代码:

// app.updateForecastCard(initialWeatherForecast);

刷新你的应用程序,你将会看到一个比较整齐漂亮的天气预报的卡片:

尝试并确保他能正常运作之后,将 app.updateForecastCard 清除。

从快速的首次加载开始

Progressive Web Apps 应该能够快速启动并且立即可用。目前,我们的天气应用能够快速启动,但是还不能使用,因为还没有数据。我们能够发起一个 AJAX 请求来获取数据,但是额外的请求会让初次加载时间变长。取而代之的方法是,在初次加载时提供真实的数据。

插入天气预报信息

在本实例中,我们将会静态地插入天气预报信息,但是在一个投入生产环境的应用中,最新的天气预报数据会由服务器根据用户的 IP 位置信息插入。

这代码已经包括了所需的资料,那就是我们在前个步骤所用的 initialWeatherForecast

区分首次运行

但我们如何知道什么时候该展示这些信息,那些数据需要存入缓存供下次使用?当用户下次使用的时候,他们所在城市可能已经发生了变动,所以我们需要加载目前所在城市的信息,而不是之前的城市。

用户首选项(比如用户订阅的城市列表),这类数据应该使用 IndexedDB 或者其他快速的存储方式存放在本地。 为了尽可能简化,这里我们使用 localStorage 进行存储,在生产环境下这并不是理想的选择,因为它是阻塞型同步的存储机制,在某些设备上可能很缓慢。

首先,让我们添加用来存储用户首选项的代码。从代码中寻找以下的TODO注解:

// TODO add saveSelectedCities function here

然后将以下的代码粘贴在TODO注解的下一行。

//  将城市裂变存入 localStorage.
app.saveSelectedCities = function() {
    var selectedCities = JSON.stringify(app.selectedCities);
    localStorage.selectedCities = selectedCities;
};

接下来,添加一些启动代码来检查用户是否已经订阅了某些城市,并渲染它们,或者使用插入的天气数据来渲染。从代码中寻找以下的TODO注解:

// TODO add startup code here

然后将以下的代码粘贴在TODO注解的下一行。

/****************************************************************************   
 *
 * 用来启动应用的代码
 *
 * 注意: 为了简化入门指南, 我们使用了 localStorage。
 *   localStorage 是一个同步的 API,有严重的性能问题。它不应该被用于生产环节的应用中!
 *   应该考虑使用, IDB (https://www.npmjs.com/package/idb) 或者
 *   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 {
  app.updateForecastCard(initialWeatherForecast);
  app.selectedCities = [
    {key: initialWeatherForecast.key, label: initialWeatherForecast.label}
  ];
  app.saveSelectedCities();
}

储存已被选择的城市

现在,你需要修改"add city"按钮的功能。这将会把已被选择的城市储存进local storage。

更新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);
  });

测试

  • 在首次允许时,你的应用应该立刻向用户展示 initialWeatherForecast 中的天气数据。
  • 添加一个新城市确保会展示两个卡片。
  • 刷新浏览器并验证应用是否加载了天气预报并展示了最新的信息。

使用 Service Workers 来预缓存应用外壳

Progressive Web Apps 是快速且可安装的,这意味着它能在在线、离线、断断续续或者缓慢的网络环境下使用。为了实现这个目标,我们需要使用一个 service worker 来缓存应用外壳,以保证它能始终迅速可用且可靠。

如果你对 service workers 不熟悉,你可以通过阅读 介绍 Service Workers 来了解关于它能做什么,它的生命周期是如何工作的等等知识。

service workers 提供的是一种应该被理解为渐进增强的特性,这些特性仅仅作用于支持service workers 的浏览器。比如,使用 service workers 你可以缓存应用外壳和你的应用所需的数据,所以这些数据在离线的环境下依然可以获得。如果浏览器不支持 service workers ,支持离线的 代码没有工作,用户也能得到一个基本的用户体验。使用特性检测来渐渐增强有一些小的开销,它不会在老旧的不支持 service workers 的浏览器中产生破坏性影响。

注册 service worker

为了让应用离线工作,要做的第一件事是注册一个 service worker,一段允许在后台运行的脚本,不需要 用户打开 web 页面,也不需要其他交互。

这只需要简单两步:

  • 创建一个 JavaScript 文件作为 service worker
  • 告诉浏览器注册这个 JavaScript 文件为 service worker

第一步,在你的应用根目录下创建一个空文件叫做 service-worker.js 。这个 service-worker.js 文件必须放在跟目录,因为 service workers 的作用范围是根据其在目录结构中的位置决定的。

接下来,我们需要检查浏览器是否支持 service workers,如果支持,就注册 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 被激活后,它应该打开缓存对象并将应用外壳需要的资源存储进去。将下面这些代码加入你的 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.addAll() 并传入一个 url 列表,然后加载这些资源并将响应添加至缓存。不幸的是 cache.addAll() 是原子操作,如果某个文件缓存失败了,那么整个缓存就会失败!

好的。让我们开始熟悉如何使用DevTools并学习如何使用DevTools来调试service workers。在刷新你的网页前,开启DevTools,从 Application 的面板中打开 Service Worker 的窗格。它应该是这样的:

当你看到这样的空白页,这意味着当前打开的页面没有已经被注册的Service Worker。

现在,重新加载页面。Service Worker的窗格应该是这样的:

当你看到这样的信息,这意味着页面有个Service Worker正在运行。

现在让我们来示范你在使用Service Worker时可能会遇到的问题。为了演示, 我们将把service-worker.js里的install 的事件监听器的下面添加在activate 的事件监听器。

self.addEvent