PWA书籍

第1部分: 定义 PWA

2015年,国际电信联盟预估到15年年末全球上网人口将到达32亿,也就是说全球将近一半的人口都在上网。 想象一下每秒钟的上网人数,32亿。大约32000个足球场才装得下这么多人!这几乎是一个大到没法理解的数字。当这些人上网时,他们使用的设备不尽相同、他们的网速也各不相同、甚至同一个的网速也会变化。做为 Web 开发者,试图知足全部这些不一样的场景彷佛使人望而生畏!这正是 PWA 出现的契机。它们赋予了开发者能够构建速度更快、富有弹性而且更吸引人的网站的能力,这些网站可以被全球数十亿人访问。在本书的第1部分中,咱们将直接深刻地定义到底什么才是 PWA 。javascript

在第1章中,你将了解渐进式网络应用 ( Progressive Web Apps ),即咱们所熟知的 PWA,以及 PWA 带来的好处。而后,咱们一块儿来看看已经利用 PWA 的能力来改善用户浏览体验的企业。咱们还会详细分析一个真实世界中 PWA ,并了解下像 Twitter 和 Flipkart 这样的公司是如何创建本身的 PWA 的。PWA 的关键组成部分是 Service Worker, 咱们将深刻介绍此主题,以及了解它在 Web 浏览器中加载时所经历的生命周期。css

在第2章中,首先介绍了构建 PWA 时能够使用的不一样架构方法,以及如何最佳地组织你的代码。咱们将研究两种不一样的方法,“汲取功能”或“应用外壳架构” - 这两种方法均可以知足项目的须要。PWA 最棒的一点就是你无需重写已存在的 Web 应用便能开始使用 PWA 的功能,只要你以为这些功能会使用户受益并提高他们的体验, 就能够添加它们。最后,本章会以剖析一个现有的 PWA 来结尾,该 PWA 是 Twitter 团队开发的 Twitter Lite ( 精简版 Twitter ) 。html

在第1部分结束之际,你应该对 PWA 是什么,以及它们能带给用户的好处有一个清晰的认知。第1部分将为本书的下一部分奠基基础,在下一部分中咱们将直接进入编码环节,从头开始构建一个 PWA 。前端

1.1 PWA 有什么优点?

在咱们开始探索为何 PWA 对于当今 Web 世界是个重大飞跃以前,值得回忆下自 Web 问世以来的历程。这要追溯到1990年的圣诞节,Tim Berners-Lee 爵士和他在 CERN 的团队建立了工做网络所需的全部工具,他们建立了 HTTP、HTML 和 WorldWideWeb (全世界第一个网页浏览器)。WorldWideWeb 只能运行由超连接的简单纯文本组成的网页。事实上,这些第一代的网页仍然在线,而且能够浏览!java

回到如今,咱们所浏览的网页与最初的网页并无太大的不一样。固然,如今咱们有了像 CSS 和 Javacript 这样的功能,但网页的核心依旧是使用 HTML、HTTP 以及一些其余构建模块来构建的,这些都是 Tim Berners-Lee 及他的团队在多年前所建立的。这些辉煌的构建模块意味着 Web 已经可以以惊人的速度增加。然而,咱们用来访问网页的设备数量也在不断增加。不管你的用户是在旅途中仍是坐在书桌前,他们都无时无刻不在获取信息。咱们对于 Web 的指望从未如此之高。git

虽然咱们的移动设备变得愈发强大,但咱们的移动网络并非总能知足需求。若是你使用智能手机,你就会知道移动链接是有多么的脆弱。2G、3G 或 4G 这些链接自己都很不错,可是它们时常会失去链接,或者网速变得不好。若是你的业务是跟网络相关的,那这就是你须要去解决的问题。github

从历史上来讲,原生应用 (下载到手机的) 已经可以提供更好的总体用户体验,你只要下载好原生应用,它便会当即加载。即便没有网络链接,也并不是是彻底不可用的: 你的设备上已经存储了供用户使用的绝大部分资源。原生应用具有提供有弹性、吸引人的体验的能力,同时也意味它的数量已经呈爆炸式增加。目前在苹果和 Google 的应用商店中,已经有超过400万的原生应用!web

从历史上来讲,Web 没法提供原生应用所具有的这些强大功能,好比离线能力,瞬时加载和更高的可靠性。这也正是 PWA 成为 Web 颠覆者的契机。主要的浏览器厂商一直在努力改进构建 Web 的方式, 并建立了一组新功能以使 Web 开发者可以建立快速、可靠和吸引人的网站。PWA 应该具有如下特色:编程

  • 响应式
  • 独立于网络链接
  • 相似原生应用的交互体验
  • 始终保持更新
  • 安全
  • 可发现
  • 可重连
  • 可安装
  • 可连接

做为 Web 开发者,这是咱们传统构建网站方式的一种转变。这意味着咱们能够开始构建能够应对不断变化的网络条件或无网络链接的网站。这还意味着咱们能够创建更吸引人的网站来为咱们的用户提供一流的浏览体验。json

读到这,你可能会想,这太疯狂了!那些不支持这些新功能的老浏览器怎么办? PWA 最棒的一点就是它们真的是“渐进式”的。若是你构建一个 PWA,即便在一个不支持的老旧浏览器上运行,它仍然能够做为一个普通的网站来运行。驱动 PWA 的技术就是这样设计的,只有在支持这些新功能的浏览器中才会加强体验。若是用户的设备支持,那么他们将得到全部额外的好处和更多的改进功能。不管怎样,这对你和你的用户来讲都是共赢!

1.1.1 基础

那么 PWA 究竟是由什么组成的呢?咱们一直将它们做为一组功能和原理来讨论,但真正使某个网站成为 “PWA” 的究竟是什么呢?最最简单的 PWA 其实只是普通的网站。它们是由咱们这些 Web 开发者所熟悉和喜欢的技术所建立的,即 HTML、CSS 和 JavaScript 。然而, PWA 却更进一步,它为用户提供了加强的体验。我很是喜欢 Google Chrome 团队的开发人员 Alex Russell 的描述方式:

“这些应用没有经过应用商店进行打包和部署,它们只是汲取了所须要的原生功能的网站而已。”

PWA 会指向一个清单 (manifest) 文件,其中包含网站相关的信息,包括图标,背景屏幕,颜色和默认方向。(在第5章中,你将学习到如何使用清单文件来使你的网站更加吸引人)

PWA 使用了叫作 Service Workers 的重要新功能,它能够令你深刻网络请求并构建更好的 Web 体验。随着本章的深刻,咱们将进一步了解它们以及它们带给浏览器的改进。PWA 还容许你将其“添加”到设备的主屏幕上。它会像原生应用那样,经过点击图标即可让你轻松访问一个 Web 应用。(咱们将在第5章中深刻讨论)

PWA 还能够离线工做。使用 Service Workers,你能够选择性地缓存部分网站以提供离线体验。若是你如今在没有网络链接的状况下浏览网站,那么对于绝大多数网站,你看到的应该是相似于下面图1.1所示的样子。

Figure 1.1

图1.1 做为用户,离线页面可能会很是使人沮丧,尤为是迫切须要获取这些信息时!

有了 Service Workers,咱们的用户无需再面对恐怖的“无网络链接”屏幕了。使用 Service Workers,你能够拦截并缓存任何来自你网站的网络请求。不管你是为移动设备,桌面设备仍是平板设备构建网站, 均可以在有网络链接或没有网络链接的状况下控制如何响应请求。(咱们将在第3章中深刻了解缓存,并在第8章中构建一个离线网页。)

简而言之,PWA 不只仅是一组很是棒的新功能,它们其实是咱们构建更好的网站的一种方式。PWA 正在迅速成为一套最佳实践。构建 PWA 所采起的步骤将有利于访问你网站的任何人,不管他们选择使用何种设备。

一旦你解锁了开始构建 PWA 所需的基本构建块,你会很快发现,比较高级的例子并无看上去那么高级。对于不知情的外行人来讲,这本书可能看起来无足轻重,可是一旦你进入构建 PWA 的节奏后,你会发现一切都是如此的简单!

1.1.2 构建 PWA 的业务场景

做为一名开发者,我固然知道当一项新技术或一系列功能出现时,是有多么的使人兴奋。但为你的网站发掘并引进最新最好的库或框架的强烈欲望每每会掩盖其为企业带来的价值。不管你是否相信,PWA 能实际上为咱们的用户带来真正的价值,并使网站更具吸引力,更有弹性,甚至更快。

PWA 最棒的一点是能够一步步地来加强现有的 Web 应用。咱们在本书中学习的技术集合能够应用于任何现有的网站,甚至是你正在构建的新的 Web 应用。不管你选择何种技术栈来开发网站,PWA 都将与你的解决方案紧密结合在一块儿,由于它只是简单地基于 HTML、CSS 和 JavaScript 。这简直太棒了!

如今你对 PWA 已经有了基本的了解,让咱们先暂时停下脚步,想象一下用 PWA 来构建的各类可能性。假设你的在线业务是报纸,人们经过它来了解更多关于当地的新闻。若是你知道有人常常访问你的网站并浏览多个页面,为何不提早缓存这些页面,这样他们就能够彻底离线地浏览新闻?或者想象下,你的 Web 应用服务于一个慈善机构,志愿者们在这个网络链接不稳定或压根无网络链接的区域进行工做。PWA 的功能将容许你构建一个离线应用,使他们在没有网络链接的现场也能收集信息。一旦他们回到办公室或有网络链接的区域,数据就能够同步到服务器。对于 Web 开发者来讲,PWA 是个完全的颠覆者,而且我我的对它们将带给 Web 的功能感到兴奋不已。

在本章的前面,我提到了你能够将 PWA “添加” 到设备的主屏幕上。一旦添加后,它便会出如今你的主屏幕上并能够经过点击图标来访问你的网站。能够把它当作台式机的快捷方式,以使你轻松访问网站。

2015年,印度最大的电商网站 Flipkart 开始构建 Flipkart Lite,它是 Web 和 Flipkart 原生应用完美结合的 PWA 。若是你在浏览器中打开 flipkart.com,你会明白为何这个网站是如此成功。就用户体验来讲是使人印象深入的,网站的速度很快,能够离线工做,而且用起来令人愉悦。经过将它的网站构建成 PWA,Flipkart 可以显示“添加到主屏幕” 操做栏。

不管你是否相信,经过“添加到主屏幕”图标到达的用户实际上在网站上购买的可能性高达70%!! (参见图 1.2)

Figure 1.2

图1.2 添加到主屏幕功能是从新与用户接触的好方法。

任何进入苹果或 Google 应用商店的新原生应用可能看起来就像沙滩上的一粒沙。截至2016年6月,在这些商店中始终保持将近200万个应用。若是你开发了一个原生应用,那么它很容易就被应用商店中的海量应用所掩盖。然而,因为 PWA 只是汲取了丰富功能的网站,所以能够经过搜索引擎轻松发现。人们能够很天然地经过社交媒体连接或浏览网页发现 PWA。构建 PWA 可让你接触比单独使用原生应用更多的人,由于它们是为任何可以运行浏览器的平台而构建的!

PWA 另外一个很棒的点是它们是用 Web 开发者所熟悉和喜好的技术所构建的。CSS、JavaScript 和 HTML 都是构建 PWA 的基石。我我的在一家小型创业公司工做,我知道编写一个能够在多个平台 (iOS、Android 和网站) 上运行的应用是多么的昂贵。有了 PWA,你只须要一个了解 Web 语言的开发团队便可。它使得招聘更容易,并且确定便宜得多!这并非说你不该该构建原生应用,由于不一样的用户会有不一样的需求,但只要你想的话,你能够专一于为网络上的用户营造一个至关好的体验并使他们留下来。

当涉及到 Web 的构建时,用户能够轻松访问你网站的一部分,而无需先下载庞大的文件。使用正确的缓存技术的 PWA 能够保存用户数据并当即为用户提供功能。随着世界各地愈来愈多的用户开始上网,为下一个十亿人构建网站从未如此重要。PWA 经过构建快速、精简的 Web 应用来帮助你实现此目标。

若是你在当今的网络上阅读过一些软件开发文章的话,经常会有围绕“原生 vs. Web”的争论。哪一个更好?各自的优点与劣势是什么? 原生应用自己是很是好的,但事实是 PWA 不只仅是将原生的功能引入 Web 。它们解决了企业面临的真正问题,旨在为用户创造一个名副其实的可发现、快速和有吸引力的体验。

1.2 Service Workers: PWA 的关键

正如我以前所提到的,释放 PWA 力量的关键在于 Service Workers 。就其核心来讲,Service Workers 只是后台运行的 worker 脚本。它们是用 JavaScript 编写的,只需短短几行代码,它们即可使开发者可以拦截网络请求,处理推送消息并执行许多其余任务。

最棒的一点是,若是用户的浏览器不支持 Service Workers 的话,它们只是简单地回退,你的网站还做为普通的网站。正是因为这一点,它们被描述为“完美的渐进加强”。渐进加强术语是指你能够先建立能在任何地方运行的体验,而后为支持更高级功能的设备加强体验。

1.2.1 理解 Service Workers

Service Worker 是如何...工做的呢?那么为了尽量地简单易懂,我真的很想解释下 Google 的 Jeff Posnick 是如何描述他们的:

“将你的网络请求想象成飞机起飞。Service Worker 是路由请求的空中交通管制员。它能够经过网络加载,或甚至经过缓存加载。”

做为“空中交通管制员”,Service Workers 可让你全权控制网站发起的每个请求,这为许多不一样的使用场景开辟了可能性。空中交通管制员可能将飞机重定向到另外一个机场,甚至延迟降落,Service Worker 的行为方式也是如此,它能够重定向你的请求,甚至完全中止。

虽然 Service Workers 是用 JavaScript 编写的,但须要明白它们与你的标准 JavaScript 文件略有不一样,这一点很重要。Service Worker:

  • 运行在它本身的全局脚本上下文中
  • 不绑定到具体的网页
  • 没法修改网页中的元素,由于它没法访问 DOM
  • 只能使用 HTTPS

你无需成为 JavaSript 专家后才能够开始尝试 Service Workers 。它们是事件驱动的,你能够简单地选择想要进入的事件。当你对这些不一样的事件有了基本的了解后,开始使用 Service Workers 要比你想象中简单!

为了更好地解释 Service Workers,咱们来看看下面的图1.3。

Figure 1.3

图1.3 Service Workers 可以拦截进出的 HTTP 请求,从而彻底控制你的网站。

Service Worker 运行在 worker 上下文中,这意味着它没法访问 DOM,它与应用的主要 JavaScript 运行在不一样的线程上,因此它不会被阻塞。它们被设计成是彻底异步的,所以你没法使用诸如同步 XHR 和 localStorage 之类的功能。在上面的图1.3中,你能够看到 Service Worker 处于不一样的线程,而且能够拦截网络请求。记住,Service Worker 就像是“空中交通管制员”,它可让你全权控制网站中全部进出的网络请求。这种能力使它们极其强大,并容许你来决定如何响应请求。

1.2.2 Service Worker 生命周期

在深刻代码示例以前,理解 Service Worker 在其生命周期中经历的不一样阶段很重要。为了更好的进行解释,让咱们想象一下一个已经建好的基础网站,而且该网站使用了 Service Worker 。该网站是一个流行的博客平台,数以百万计的做家天天都在使用它来分享内容。

简单点说,该网站不停地在接收包括图像甚至视频在内的内容请求。为了理解 Service Worker 生命周期是如何工做的,咱们从网站每一天数百万次交互请求中挑选出一个。

图1.4展现了 Service Worker 生命周期,它会在用户访问该网站的博客页面时发生。

Figure 1.4

图1.4 Service Worker 生命周期

让咱们慢慢来理解上面的图1.4,一步步地了解 Service Worker 生命周期是如何工做的。

当用户首次导航至 URL 时,服务器会返回响应的网页。在图1.4中,你能够看到在第1步中,当你调用 register() 函数时, Service Worker 开始下载。在注册过程当中,浏览器会下载、解析并执行 Service Worker (第2步)。若是在此步骤中出现任何错误,register() 返回的 promise 都会执行 reject 操做,而且 Service Worker 会被废弃。

一旦 Service Worker 成功执行了,install 事件就会激活 (第3步)。Service Workers 很棒的一点就是它们是基于事件的,这意味着你能够进入这些事件中的任意一个。咱们将在本书的第3章中使用这些不一样的事件来实现超快速缓存技术。

一旦安装这步完成,Service Worker 便会激活 (第4步) 并控制在其范围内的一切。若是生命周期中的全部事件都成功了,Service Worker 便已准备就绪,随时能够使用了!

Figure 1.5

图1.5 Service Worker 生命周期经历了不一样阶段,这有点像交通灯系统

对我我的而言,我以为记住 Service Worker 生命周期最简单的方法就是把它当成一组交通讯号灯。在注册过程当中,Service Worker 处于红灯状态,由于它还须要下载和解析。接下来,它处于黄灯状态,由于它正在执行,尚未彻底准备好。若是上述全部步骤都成功了,你的 Service Worker 在将处于绿灯状态,随时能够使用。

须要注意的是,当第一次加载页面时,Service Worker 尚未激活,因此它不会处理任何请求。只有当它安装和激活后,才能控制在其范围内的一切。这意味着,只有你刷新页面或者导航到另外一个页面,Service Worker 内的逻辑才会启动。

1.2.3 Service Worker 基础示例

我很确定,到目前为止你一直迫切地想看看代码应该是怎么样的,因此咱们开始吧。

由于 Service Worker 只是运行在后台线程的 JavaScript 文件,因此在 HTML 页面中你能够像引用任何 JavaScript 文件同样来引用它。假设咱们建立了一个 Service Worker 文件,并将其命名为 sw.js 。要注册它,须要在 HTML 页面中使用以下代码。(参见代码清单 1.1)

代码清单 1.1
<html>
  <head>The best web page ever</head> <body> <script>  // 注册 service worker  if ('serviceWorker' in navigator) { ❶  navigator.serviceWorker.register('/sw.js').then(function(registration) { ❷  // 注册成功  console.log('ServiceWorker registration successful with scope: ', registration.scope); ❸  }).catch(function(err) { ❹  // 注册失败 :(  console.log('ServiceWorker registration failed: ', err);  });  }  </script> </body> </html>
  • ❶ 检查当前浏览器是否支持 Service Workers
  • ❷ 若是支持,注册一个叫作 'sw.js' 的 Service Worker 文件
  • ❸ 若是成功则打印到控制台
  • ❹ 若是发生错误,捕获错误并打印到控制台

在 script 标签内,咱们首先检查浏览器其实是否支持 Service Workers 。若是支持,咱们就使用 navigator.serviceWorker.register('/sw.js') 函数注册,该函数又会通知浏览器下载 Service Worker 文件。若是注册成功,它会开始 Service Worker 生命周期的剩余阶段。

在上面的代码示例中,你或许会注意到 JavaScript 代码并无使用回调函数。那是由于 Service Workers 使用 JavaScript 中的 Promises,Promises 以一种十分整洁、可读的方式来处理回调函数。Promise 表示一个操做还未完成,可是期待它未来会完成。这使得异步方法返回的值如同同步方法那样,并使编写的 JavaScript 更整洁,也更容易阅读。Promises 能够作不少事情,但就目前而言,你所须要知道的就是,若是某些函数返回 Promise,你能够在后面附加 .then(),then 里面包含成功的回调、失败的回调,等等。咱们将在后面的章节中更密切地关注 JavaScript 中的 Promises 。

navigator.serviceWorker.register() 函数返回 promise,若是注册成功的话,咱们能够决定如何继续进行。

以前我提到过 Service Workers 是事件驱动的,并且 Service Workers 最强大的功能之一就是容许你经过进入 fetch 事件来监放任何网络请求。当一个资源发起 fetch 事件时,你能够决定如何继续进行。你能够将发出的 HTTP 请求或接收的 HTTP 响应更改为任何内容。这至关简单,但同时却很是强大!

假设你的 Service Worker 文件中的代码片断以下。(参见代码清单1.2)

代码清单 1.2
self.addEventListener('fetch', function(event) { ❶ if (/\.jpg$/.test(event.request.url)) { ❷ event.respondWith(fetch('/images/unicorn.jpg')); ❸ } });
  • ❶ 为 fetch 事件添加事件监听器
  • ❷ 检查传入的 HTTP 请求是不是 JPEG 类型的图片
  • ❸ 尝试获取独角兽的图片并用它做为替代图片来响应请求

在上面的代码中,咱们监听了 fetch 事件,若是 HTTP 请求的是 JPEG 文件,咱们就拦截请求并强制返回一张独角兽图片,而不是原始 URL 请求的图片。上面的代码会为该网站上的每一个 JPEG 图片请求执行一样的操做。虽然独角兽的图片棒极了,可是你可能不想在现实世界的网站上这样作,由于你的用户可能不满意这样的结果!上面的代码示例可让你了解 Service Workers 的能力。只是短短几行代码,咱们在浏览器中建立了一个强大的代理。

1.2.4 安全考虑

为了让 Service Worker 能在网站上运行,须要经过 HTTPS 来提供服务。虽然这让使用它们变得有些困难,但这样作有一个很重要的缘由。还记得将 Service Worker 比喻成空中交通管制员吗?能力越大,责任越大,对于 Service Worker 而言,它们实际上也可能用于恶意的用途。若是有人可以在你的网页上注册一个狡诈的 Service Worker,他们将可以劫持链接并将其重定向到恶意端点。事实上,坏蛋们可能会利用你的 HTTP 请求作任何他们想要的事情!为了不这种状况发送,Service Worker 只能在经过 HTTPS 提供服务的网页上注册。这确保了网页在经过网络的过程当中没有被篡改。

若是你是一个想要构建 PWA 的网站开发者,读到这你可能会有点沮丧,千万别!传统上,为你的网站获取 SSL 证书可能会花费了至关多的钱,但无论你是否相信,实际上如今有许多免费的解决方案能够供 Web 开发者使用。

首先,若是你想要在本身的电脑上测试 Service Workers,你能够经过 localhost 提供的页面来执行此操做。它们已经建立了这个功能,以使开发者在部署应用以前轻松进行本地调试。

若是你准备好将你的 PWA 发布到网上,那么你能够使用一些免费的服务。Let’s Encrypt https://letsencrypt.org/ 是一个新的证书受权,它是免费的、自动的和开放的。你能够使用 Let’s Encrypt 来快速开始让的你网站经过 HTTPS 提供服务。若是你想了解更多关于 Let’s Encrypt 的信息,去它们的“新手入门”页面 https://letsencrypt.org/getting-started/

若是你像我同样,使用 GitHub 进行源代码控制的话,那么你能够使用 GitHub Pages 来测试 Service Worker 。(参见图1.6) 经过 GitHub Pages,你能够直接在你的 GitHub 仓库中托管基础网站,而无需后端。

Figure 1.6

图1.6 GitHub Pages 容许你经过 SSL 直接在 GitHub 仓库中托管网站。

使用 GitHub Pages 的优势是,默认状况下,你的网页是经过 HTTPS 来提供的。当我第一次开始试用 Service Workers 时,GitHub Pages 容许我快速地启动一个网站,并随时验证个人想法。

1.3 性能洞察: Flipkart

在本章的前面,咱们介绍过一个名为 Flipkart 的电子商务公司的案例,Flipkart 决定将它的网站构建成 PWA 。Flipkart 是印度最大的电子商务网站,一个快速,有吸引力的网站对于他们业务的成功相当重要。还值得注意的是,在像印度这样的新兴市场,移动数据包的成本可能至关高,而且移动网络多是不稳定的。出于这些缘由,许多新兴市场中的电子商务公司都须要构建轻便、精简的网页,以知足任何网络上的用户需求。

2015年,Flipkart 采用了仅使用原生应用的策略,并决定暂时关闭它的移动网站。后来公司发现,在原生应用中提供快速和有吸引力的用户体验变得愈发困难。因此 Flipkart 决定从新思考它们的开发方式。经过引入可当即运行的移动 Web 应用,离线工做和从新吸引用户的功能,使其开发人员又回到移动 Web 开发工做之中,这些引入的功能都是 PWA 所提供的。

当他们实现了本身的新的 PWA 后,便看到了立竿见影的效果。不只网站几乎是瞬时加载的,并且当他们离线时还可以继续浏览分类页面,查看之前的搜索结果和产品页面。数据使用量是 Flipkart 的关键指标,最重要的是将 Flipkart Lite 与原生应用进行比较时,Flipkart Lite 的数据量减小了3倍。

构建 PWA 给予了他们更多的好处,由于网站速度很快而且吸引人,结果是他们的用户在网站上的使用时间增长了3倍,以及高达40%的参与度。这些都是想当使人印象深入的改进!若是你想亲自见证效果,请访问 flipkart.com 尽情享受体验。

1.4 总结

  • 在用户体验方面,相比于传统网站,原生应用能够提供更好的体验。
  • Web 正在发展,咱们没有任何理由不去为用户提供快速、有弹性和吸引人的 Web 应用。
  • PWA 可以为你的用户提供更快的、富有弹性的和更吸引人的网站。
  • Service Workers 是解锁浏览器力量的关键。能够把它们当作是可以拦截 HTTP 请求的空中交通管制员。
  • Web 一直都很棒,可是咱们没有理由不去改进它,并向用户提供更多的功能。每一天,咱们都要为用户多创造一些!

    2.1 创建在现有基础之上

    第1章中 Alex Russell 的引言 (关于汲取了所须要的全部原生功能的网站) 完美地总结了 PWA 的特性,并且我首次开始尝试 Service Workers 时也是这种感受。当我真正理解了它们工做的基本概念后,我慢慢地意识到,它们的强大其实远远超乎个人想象,甚至让我脑洞大开。随着我愈来愈多地了解 PWA,并开始尝试每次学习使用一个新功能或“元素”。一般学习新技术就像是在登山,若是你以一步一趋的思惟方式来学习 PWA 相关的知识的话,你将很快掌握 PWA 的艺术。

    我相信,不少读这本书的人都会在他们目前的项目中花费大量的时间和精力。幸运的是,构建一个 PWA 并不须要你从头开始把项目再重作一遍。当我尝试改善现有的应用时,每当我以为一个功能对用户有益并能为他们提供加强的体验时,我就会添加这个新“功能”。我喜欢把每一个 PWA 的新功能都看做是能够升级超级马里奥的新蘑菇!

    若是你认为你现有的 Web 应用能够从 PWA 的功能中受益,我推荐你一个叫作 Lighthouse (https://github.com/GoogleChrome/lighthouse) 的便利工具。它提供 Web 应用相关的有用的性能信息和审核信息。(参见图2.1)

    Figure 2.1

    图2.1 Lighthouse 工具很是适用于衡量 PWA 的审核和生产性能。

    你也能够把它当作命令行界面来使用,或者若是你使用 Google Chrome 浏览器的话,还有方便的 Chrome 插件能够使用。若是你在打开网站时运行它,它会生成与上面图片相似的内容。该工具针对你的网站进行审核,并生成一个有用的功能和性能指标清单,可用于改进你的网站。若是你想使用这个方便的工具并但愿将其运行到你现有的一个网站上,请移步至 github.com/GoogleChrome/lighthouse 以了解更多信息。

    有了 Lighthouse 工具的反馈,你能够每次添加一个新功能,慢慢地提高你网站的总体体验。

    此刻,你可能想了解有哪些功能是能够添加到你现有网站上的! Service Workers 开辟了一个充满可能性的世界,因此,决定从哪里入手是相当重要的。在本书的其他部分,每章都会重点介绍 PWA 的一个新功能,不管你是为了优化现有网站,仍是为了构建一个全新的网站,均可以即学即用。

    2.2 构建 PWA 的前端架构方式

    在开发者之中,一般会讨论是构建原生应用仍是 Web 应用,到底哪一个更好。就我我的而言,我认为你应该根据用户的须要来构建应用。不该该出现 PWA 和原生应用相争的情况,做为开发者,咱们应该不断探索提高用户体验的方法。就像你想的那样,我会对构建 Web 应用有本身的偏好,但不管你是否喜欢,若是你将 PWA 视为一套“最佳”实践的话,你都会构建出更好的网站。假设说,你喜欢用 React 或 Angular 进行开发,你彻底能够继续使用它们,由于构建 PWA 只会加强 Web 应用,并使其速度更快,更具吸引力和更有弹性。

    原生应用开发者长期以来一直可以给他们的用户提供 Web 开发者求之不得的功能,例如离线操做和不管网络链接如何均可以响应的功能。然而,要感谢 PWA 带给 Web 的新功能,咱们能够努力构建更好的网站。许多原生应用都有着良好的架构,做为 Web 开发者,咱们能够从他们的架构方法中进行学习。在下一节中,咱们会来看看构建 PWA 时,在前端代码中能够使用的不一样架构方式。

    2.2.1 应用外壳架构

    当今有不少很是棒的原生应用。就我的而言,我以为 Facebook 的原生应用为用户提供了很是棒的体验。当你离线时它会给你提示,它会缓存你的时间轴,以便你能更快地访问,它还能作到瞬间加载。若是你有一段时间没有访问 Facebook 的原生应用,你仍会在任何动态内容加载以前,当即看到一个空的“UI 外壳”,包括头部和导航条。

    借助 Service Workers 的力量,咱们没有任何理由不为 Web 上的用户提供一样的体验。使用智能的 Service Worker 缓存,你实际上能够缓存你网站的 UI 外壳,以便用户重复访问。这些新功能使咱们可以以不一样的方式来思考和构建网站。

    此刻你可能想知道什么是 “UI 外壳”:它只是用户界面所必需的最小化的 HTML、CSS 和 JavaScript 。它可能会是相似网站头部,底部和导航这样没有任何动态内容的部分。若是咱们能加载并缓存 UI 外壳,咱们就能够在稍后的阶段将动态内容加载到页面中。Google 的 Inbox 就是一个很好的现成例子。咱们来看看下面的图片,以得到更好的理解。

    Figure 2.2

    图2.2 Google 的 Inbox 利用 Service Workers 来缓存 UI 外壳。

    你可能对 Google 的 Inbox 已经很熟悉了。(参见图2.2) 它是一个便利的 Web 应用,它容许你组织和发送邮件。在底层它使用 Service Workers 来缓存,并为用户提供超级快的体验。如你在上图中所见,当你访问该网站的时候,首先它的 UI 外壳会当即呈如今你眼前。这是很是棒的,由于用户得到了即时反馈,这会让他们感到网站速度很是快,即便咱们仍在等待其他部分的动态内容加载。该应用给用户一种感观上的速度快,即便它用来获取内容的时间并不比以前短。用户还会注意到 “loading” 指示符,它表示网站正在发生一些事情,此刻正忙。这比等待一个空白页面加载好久好多了!

    一旦外壳加载完,网站的动态内容就会使用 JavaScript 来获取并加载。

    Figure 2.3

    图2.3 一旦 UI 外壳加载完,就能够获取网站的动态内容,而后添加到页面的剩余部分。

    上面的图2.3展现了 Google 的 Inbox 网站在动态内容加载完后就会将其填充到 Web 应用之中。使用一样的技术,你能够为网站的重复访问提供瞬时加载,还能够缓存应用的 UI 外壳,这样它就能离线工做。这样即便用户当前没有链接,他们也能够看到应用的 UI 外壳。

    在第3章中,你将学习如何利用 Service Workers 来缓存你的内容而且为你的用户提供离线体验。在本书中,咱们会构建一个 PWA,它会使用应用外壳架构,你能够下载并遵循此代码,而后使用此方法来构建你本身的应用。

    性能优点

    使用了应用外壳架构的 Web 应用“瞬时”加载提及来很容易,但这对用户到底意味着什么呢?多快才是“瞬时”?为了更好的从视觉上感觉出使用应用外壳架构的 PWA 加载有多块,我使用了叫作 webpagetest.org 的工具来生成下面的幻灯片。(参见图2.4)

    Figure 2.4

    图2.4 即便在动态内容加载完成以前,应用外壳架构也能够在屏幕上为用户提供有意义的内容。上图显示了使用 Service Workers 进行缓存先后的加载时间。

    我对我构建的名为 Progressive Beer 的 PWA 运行该工具。上图显示了一段时间内 PWA 加载的幻灯片视图。在图像的顶部,你能够看到以秒为单位的时间及其对应的屏幕上的页面显示状况。

    对于首次访问的用户,该网站须要更长时间进行下载,由于是首次获取这些资源。一旦全部的资源下载完成,首次访问的用户大约在4秒后可以在与网站进行充分的互动。

    对于再次访问的用户,激活的 Service Worker 便会进行安装,用户大概会在0.5秒 (500毫秒) 后看到网站的 UI 外壳。尽管动态内容尚未从服务器返回,但 UI 外壳会首先加载。此后,剩余的动态内容会被加载并填充到屏幕之中。此方法最棒的是,即便没有网络链接,用户仍然能够在大约500毫秒后看到网站的 UI 外壳。此刻,咱们能够向他们展现一些有意义的东西,要么通知他们处于离线状态,要么为他们提供缓存的内容。

    记住,每次用户从新访问网站,他们都会体验到这种快速、可靠和有吸引力的加强体验。若是你即将开发一个新的 Web 应用的话,使用应用外壳架构会是一种利用 Service Workers 的高效方式。

    应用外壳架构实战

    在第1章中,咱们经历了 Service Worker 生命周期的各个阶段。起初,它可能没有太多的意义,但随着咱们深刻了解应用外壳架构是如何工做的,它便开始有意义了。记住,使用 Service Worker 你便可以进入它生命周期的各个不一样事件。

    为了更好的理解如何进入这些事件,咱们来看看下面的图2.5。

    Figure 2.5

    图2.5 在 Service Worker 安装过程当中,咱们能够获取资源并为下次访问作好预缓存。

    当用户首次访问网站时,Service Worker 会开始下载并安装自身。在安装阶段,咱们能够进入这个事件并准备缓存 UI 外壳所需的全部资源。也就是说,基础的 HTML 页面和任何可能须要的 CSS 或 JavaScript 。

    此时,咱们能够当即提供网站的“外壳”,由于它已经添加到 Service Worker 缓存之中。这些资源的 HTTP 请求不再须要转到服务器了。一旦用户导航到另外一个页面,他们将当即看到外壳。(参见图2.6)

    Figure 2.6

    图2.6 对于发起的任意 HTTP 请求,咱们能够检查资源是否存在于缓存之中,若是不存在的话,咱们再经过网络来获取它们。

    动态内容被加载到网站中,网站会正常运行。由于你可以进入这些请求的 fetch 事件,因此你能够决定是否要缓存它们。你可能有常常更新的动态内容,所以缓存它们没有意义。可是,你的用户仍然会获得更快、更好的浏览体验。在下章中,咱们会深刻 Service Worker 缓存,在那里你将了解到更多关于这个强大功能的具体内容。

  • 2.3 逐步剖析现有的 PWA

    尽管 PWA 仍是个比较新的概念,但已有一些了不得的 PWA 已经在网络上供天天数百万用户使用。

    在下章中,咱们会深刻代码并向你展现如何开始构建本身的 PWA 。在咱们更进一步以前,为了更好的理解 PWA 的这些功能是如何工做的,剖析现有的 PWA 仍是有必要的。

    在本节中,咱们来看一个我我的很是喜欢的 PWA 。Twitter 的手机网站就是 PWA,它为使用移动设备的用户提供了加强体验。若是你使用 Twitter,那这是在旅途中查看推文的好方法。(参见图2.7)

    Figure 2.7

    图2.7 Twitter 的手机网站就是 PWA,它采用了应用外壳架构

    若是在移动设备上导航到 twitter.com , 会重定向到 mobile.twitter.com 并展示一个不一样的网站。Twitter 将它们的 PWA 命名为 “Twitter Lite”,由于它占用的存储空间不到1MB,并声称能够节省多达70%的数据,同时加快30%的速度。

    就我的而言,我以为它很是棒,它应该还能够 PC 端使用!相对于原生版本,我实际上更喜欢 PWA 版本。你仍然能够在 PC 端上经过直接导航到mobile.twitter.com 来访问 Web 应用。

    2.3.1 前端架构

    在底层,Twitter Lite 是使用应用外壳架构构建的。这意味着它使用简单的 HTML 页面做为网站的 UI 外壳,而页面的主要内容是使用 JavaScript 动态注入的。若是用户的浏览器支持 Service Workers,那么 UI 外壳所需的全部资源都会在 Service Worker 安装阶段被缓存。

    Figure 2.8

    图2.8 应用外壳架构当即赋予屏幕有意义的内容。左边的图片是用户首先看到的,一旦数据加载完成,就像右边图片那样呈现出数据。

    对于重复访客,这意味着外壳会瞬间加载并可以在没有任何延迟的状况下赋予屏幕有意义的内容。(参见图2.8) 对于不支持 Service Workers 的浏览器,此方法仍将以相同的方式工做,他们只是没有缓存 UI 外壳的资源, 而且会失去超快性能的附加奖励。Web 应用还针对使用响应式网页设计的不一样屏幕尺寸进行了优化。

    缓存

    Service Worker 缓存是一个强大的功能,它赋予咱们这些 Web 开发者使用编程方式来缓存所需资源的能力。你可以拦截 HTTP 请求和响应,并根据你的须要调整它们。这个强大的功能是解锁更好的 Web 应用的关键。使用 Service Worker 容许你进入任何网络请求并彻底由你来决定想要如何响应。

    使用 Service Worker 缓存能够轻松完成快速、有弹性的 PWA 。Twitter Lite 很快,真的很是快。若是你在页面之间进行浏览,你会以为这个 Web 应用很是好用,已缓存的页面几乎都是瞬时加载。做为用户,这种体验是我对于每个网站的指望!

    在底层,Twitter Lite 使用了一个叫作 Service Worker Toolbox 的库。这个库很方便,它包含一些使用 Service Workers 进行尝试并验证过的缓存技术。该工具箱为你提供了一些基本辅助方法,以便你开始建立本身的 Service Workers,并使你避免编写重复代码。在第3章中,咱们将深刻缓存,但目前咱们仍是先来看下使用 Service Worker Toolbox 的缓存示例。

    代码清单 2.1
    toolbox.router.get("/emoji/v2/svg/:icon", function(event) { ❶ return caches.open('twemoji').then(function(response) { ❷ return response.match(event.request).then(function(response) { ❸ return response || fetch(event.request) ❹ }) }).catch(function() { return fetch(event.request) ❺ }) }, { origin: /abs.*\.twimg\.com$/ ❻ })
    • ❶ 拦截路径为 '/emoji/v2/svg/:icon' 的任意请求
    • ❷ 打开一个叫作 'twemoji' 的现有缓存
    • ❸ 检查当前请求是否匹配咱们缓存中的任何内容
    • ❹ 若是匹配则当即返回缓存内容,不然继续正常运行
    • ❺ 若是打开缓存时出现问题,只需继续正常运行
    • ❻ 咱们还想只检查 twimg.com 域名下的资源

    在上面的代码清单2.1中,Service Worker Toolbox 寻找 URL 匹配'/emoji/v2/svg/' 而且来自 *.twimg.com 站点的任何请求。一旦它拦截了匹配此路由的任意 HTTP 请求,它会将它们存储在名为 'twemoji' 的缓存之中。等下次用户再次发起匹配此路由的请求时,呈现给用户的将是缓存的结果。

    这段代码是很是强大的,它赋予咱们这些开发者一种能力,使咱们能够精准控制如何以及什么时候在网站上缓存资源。若是起初这段代码会让你有些困惑,也不要担忧。在下章中,咱们会深刻 Service Worker 缓存并使用这个强大功能来构建页面。

    离线浏览

    我天天上下班的途中都是在火车上度过的。很幸运,旅途不算太长,但不幸的是在某些区域网络信号很弱,甚至是掉线。这意味着若是我正在手机上浏览网页,有时我可能会失去链接,或者链接至关不稳定。这是至关使人沮丧的!

    幸运的是,Service Worker 缓存是个强大的功能,它其实是将网站资源保存到用户的设备上。这意味着使用 Service Workers 就可让你拦截任何 HTTP 请求并直接用设备上缓存的资源进行响应。你甚至不须要访问网络就能够获取缓存的资源。

    考虑到这一点,咱们能够使用这些功能来构建离线页面。使用 Service Worker 缓存,你能够缓存个别的资源,甚至是整个网页,这彻底取决于你。若是用户没有网络链接,Twitter Lite 会为用户展示一个自定义的离线页面。

    Figure 2.9

    图2.9 若是用户没有网络链接,Twitter PWA 会为用户显示一个自定义的错误页面。

    用户如今看到的是一个有帮助的自定义离线页面,而不是可怕的错误: “没法访问此网站” (参见图2.9)。他们还能够经过点击提供的按钮来检查链接是否恢复。对于用户来讲,这种 Web 体验更好。在第8章中,你将掌握开始构建本身的离线页面所需的必要技能,并为你的用户提供富有弹性的浏览体验。

    外观感觉

    Twitter Lite 很快,并且针对小屏幕进行了优化,还能离线工做。还有什么?好吧,它须要如同原生应用同样的外观感觉!若是你仔细查看过 Web 应用主页的 HTML 的话,可能会注意到下面这行代码:

    <link rel="manifest" href="/manifest.json">

    这个连接指向一个被称为“清单文件”的文件。这个文件只是简单的 JSON 文件,它遵循 W3C 的 Web App Manifest 规范,并使开发者可以控制应用中不一样元素的外观感受。它提供 Web 应用的信息,好比名称,做者,图标和描述。

    它带来了一些好处。首先,它使浏览器可以将 Web 应用安装到设备的主屏幕,以便为用户提供更快捷的访问和更丰富的体验。其次,经过在清单文件中设置品牌颜色,你能够自定义浏览器自动显示的启动画面。它还容许你自定义浏览器的地址栏以匹配你的品牌颜色。

    使用清单文件真正地使 Web 应用的外观感受更加完美,并为你的用户提供了更丰富的体验。Twitter Lite 使用清单文件以利用浏览器中的许多内置功能。

    在第5章中,咱们会探索如何使用清单文件来加强 PWA 的外观感觉,并为用户提供有吸引力的浏览体验。

    最终产品

    Twitter Lite 是一个全面的例子,它很好地诠释了 PWA 应该是怎样的。它涵盖了贯穿本书的绝大部分功能,这些功能都是为了构建一个快速、有吸引力和可靠的 Web 应用。

    在第1章中,咱们讨论过了 Web 应用应该具有的全部功能。让咱们来回顾下到目前为止 Twitter PWA 的细节。该应用是:

    • 响应式的 - 它适应较小的屏幕尺寸
    • 链接无关 - 因为 Service Worker 缓存,它能够离线工做
    • 应用式的交互 - 它使用应用外壳架构进行构建
    • 始终保持最新 - 感谢 Service Worker 的更新过程
    • 安全的 - 它经过 HTTPS 进行工做
    • 可发现的 - 搜索引擎能够找到它
    • 可安装的 - 使用清单文件
    • 可连接的 - 能够简单的经过 URL 来共享

    哇!真是个大清单,但幸运的是咱们所获得的这些收益很多都是构建 PWA 的附属品。

  • 2.4 总结

    • PWA 带给 Web 的功能使开发者能够为用户构建更快、更可靠、更吸引人的网站。
    • 这些功能被添加到浏览器中,这意味着它们也能够与你熟悉的任何库或框架一块儿很好地工做。不管你是有一个现成的应用,仍是想从头开始构建新的 Web 应用,均可以根据须要来定制 PWA。
    • 在本章中,咱们研究一种叫作应用外壳架构的架构方式,你能够使用它来利用 Service Worker 缓存来为你的用户当即提供有意义的页面。
    • 最后,咱们剖析了 Twitter 的 PWA ,并了解了浏览器中现有的许多功能。
    • 在本书的其他部分中,咱们将逐个深刻了解这些功能,你将学习到如何构建一个精简的 PWA, 就像 Twitter 同样。
    • 第2部分: 更快的 Web 应用

      若是你曾经急于从网站上获取紧急信息,那你会懂得等待网页加载能够是多么一件使人沮丧的事情。事实上,在尼尔森诺曼集团的一项研究中,他们发现,10秒的延迟一般会使用户当即离开网站,即便他们留下来,他们也很难了解将会发生什么,让他们在这种处境下继续坚持下去直到网页加载完彷佛变得不太可能。若是你经营的是一个基于在线的业务,你可能已经失去了将此人转换为销售的机会。这就是为何构建快速和有效工做的网页如此重要,不管用户设备是怎样的。

      在本书的第2部分,咱们会专一于如何使用 Service Workers 来提高 PWA 的性能。从缓存技术到备用图片格式,Service Workers 的灵活性都足以应对各类状况。

      在第3章中,咱们会深刻 Service Worker 缓存并帮助你理解可应用于 Web 应用的不一样缓存技术。咱们先从一个很是基础的缓存示例入手,而后扩展为不一样的缓存方法。不管网站的前端代码是如何编写的,使用 Service Worker 缓存均可以大大改善页面的加载速度。咱们还会看一些缓存相关的陷阱并提出建议以帮助你来处理它们。这一章会以Service Worker Toolbox 的简短介绍来结尾,Service Worker Toolbox 是一个有用的库,它使得编写缓存代码更加简单。

      在第4章中,咱们会深刻 Fetch API,并看看如何利用它来构建更快的 Web 应用。这一章涵盖了一些小技巧,能够使用它们来将你的网站性能提高至最佳。咱们还涉及到了一项返回轻量级图片格式 (WebP) 的技术,而后还会看下如何在 Android 设备上接入 “Save-Data” 以减小网页的总体体积。

    • 3.1 HTTP 缓存基础

      现代浏览器真的十分聪明,它们能够解释和理解各类 HTTP 请求和响应,而且可以在须要数据以前进行存储和缓存。我喜欢将浏览器缓存信息的能力看做牛奶上的最迟销售日期。一样的方式,你能够将牛奶保存在冰箱中,直至到达保质期,浏览器也能够在一段时间内缓存网站相关的信息。在过时后,它会去获取更新后的版本。这能够确保网页加载更快并消耗更少的带宽。

      在深刻 Service Worker 缓存以前,前后退一步,了解下传统 HTTP 缓存的工做原理是很重要的。自从20世纪90年代初引入 HTTP/1.0 以来,Web 开发者便已经可以使用 HTTP 缓存了。HTTP 缓存容许服务器发送正确的 HTTP 首部,这些首部信息将指示浏览器在一段时间内缓存响应。

      Web 服务器能够利用浏览器的能力来缓存数据,并使用它来减小重复请求的加载时间。若是一个用户在一次会话中访问同一个页面两次,若是数据没有改变,一般不须要为它们提供新的资源。这样一来,Web 服务器能够使用 Expires 首部来通知 Web 客户端,它能够使用资源的当前副本,直到指定的“过时时间”。反过来,浏览器能够缓存此资源,而且只有在有效期满后才会再次检查新版本。

      Figure 3.1

      图3.1 当浏览器发起一个资源的 HTTP 请求时, 服务器会发送的 HTTP 响应会包含该资源相关的有用信息

      在上图中,你能够看到当浏览器发起一个资源的 HTTP 请求时,服务器返回的资源还附带一些 HTTP 首部。这些首部包含有用的信息,浏览器能够经过它们来了解资源相关的更多信息。HTTP 响应告诉浏览器这个资源是什么类型的,要缓存多长时间,它是否压缩过,等等。

      HTTP 缓存是提升网站性能的绝佳方式,但它也有自身的缺陷。使用 HTTP 缓存意味着你要依赖服务器来告诉你什么时候缓存资源和什么时候过时。若是你的内容具备相关性,任何更新均可能致使服务器发送的到期日期很容易变得不一样步,并影响你的网站。

      能力越大,责任越大, 对 HTTP 缓存来讲,真是再正确不过了。当咱们对 HTML 进行重大更改时,咱们也可能会更改 CSS 以及对应的新的 HTML 结构,并更新任何 JavaScript 以适应样式和内容的更改。若是你曾经在发布更改后的网站时没有获得正确的 HTTP 缓存的话,我相信你会明白这是因为错误的缓存资源所致使的网站被破坏。

      下面的图是个人我的博客在文件被错误缓存状况下的样子。

      Figure 3.2

      图3.2 当缓存的文件不一样步时,网站的感观都会受其影响

      你能够想象一下,不管是对于开发者仍是用户,这都是很是使人沮丧的!在上面的图3.2中,你能够看到页面的 CSS 样式没有加载,这是因为不正确的缓存而致使的文件不匹配。

    • 3.2 Service Workers 缓存基础

      本章读到此处,你可能会思考,咱们都已经有 HTTP 缓存了,为什么还须要 Service Worker 缓存呢? Service Worker 缓存有何不一样呢?好吧,它能够替代服务器来告诉浏览器资源要缓存多久,做为开发者的你能够全权掌控。Service Worker 缓存极其强大,由于对于如何缓存资源,它赋予了你程序式的精准控制能力。与全部 PWA 的功能同样,Service Worker 缓存是对 HTTP 缓存的加强,并能够与之配合使用。

      Service Workers 的强大在于它们拦截 HTTP 请求的能力。在本章中,咱们将使用这种拦截 HTTP 请求和响应的能力,从而为用户提供直接来自缓存的超快速响应!

      3.2.1 在 Service Worker 安装过程当中预缓存

      使用 Service Workers,你能够进入任何传入的 HTTP 请求,并决定想要如何响应。在你的 Service Worker 中,能够编写逻辑来决定想要缓存的资源,以及须要知足什么条件和资源须要缓存多久。一切尽归你掌控!

      在上一章中,咱们简要地看过一个示例,以下面图3.3所示。当用户首次访问网站时,Service Worker 会开始下载并安装自身。在安装阶段中,咱们能够进入这个事件,并准备缓存 Web 应用所需的全部重要资源。

      Figure 3.3

      图3.3 在 Service Worker 安装阶段,咱们能够获取资源并为下次访问准备好缓存

      以此图为例,咱们来建立一个基础的缓存示例,以便更好地了解它是如何实际工做的。在下面的清单3.1中,我建立了一个简单的 HTML 页面,它注册了 Service Worker 文件。

      代码清单 3.1
      <!DOCTYPE html>
      <html>
        <head> <meta charset="UTF-8"> <title>Hello Caching World!</title> </head> <body> <!-- Image --> <img src="/images/hello.png" /> ❶ <!-- JavaScript --> <script async src="/js/script.js"></script> ❷ <script>  // 注册 service worker  if ('serviceWorker' in navigator) { ❸  navigator.serviceWorker.register('/service-worker.js').then(function (registration) {  // 注册成功  console.log('ServiceWorker registration successful with scope: ', registration.scope);  }).catch(function (err) { ❹  // 注册失败 :(  console.log('ServiceWorker registration failed: ', err);  });  }  </script> </body> </html>
      • ❶ 引用 “hello” 图片
      • ❷ 引用基础的 JavaScript 文件
      • ❸ 检查当前浏览器是否支持 Service Workers
      • ❹ 若是在 Service Worker 注册期间发生错误,咱们能够捕获它并作出适当的处理

      在上面的清单3.1中,你能够看到一个引用了图片和 JavaScript 文件的简单网页。该网页没有任何华丽的地方,但咱们会用它来学习如何使用 Service Worker 缓存来缓存资源。上面的代码会检查你的浏览器是否支持 Service Worker,若是支持,它会尝试去注册一个叫作 service-worker.js 的文件。

      好了,咱们已经准备好了基础页面,下一步咱们须要建立缓存资源的代码。清单3.2中的代码会进入叫作 service-worker.js 的 Service Worker 文件。

      代码清单 3.2
      var cacheName = 'helloWorld'; ❶ self.addEventListener('install', event => { ❷ event.waitUntil( caches.open(cacheName) ❸ .then(cache => cache.addAll([ ❹ '/js/script.js', '/images/hello.png' ])) ); });
      • ❶ 缓存的名称
      • ❷ 咱们将进入 Service Worker 的安装事件
      • ❸ 使用咱们指定的缓存名称来打开缓存
      • ❹ 把 JavaScript 和 图片文件添加到缓存中

      在第1章中,咱们看过了 Service Worker 生命周期和它激活以前所经历的不一样阶段。其中一个阶段就是 install 事件,它发生在浏览器安装并注册 Service Worker 时。这是把资源添加到缓存中的绝佳时间,在后面的阶段可能会用到这些资源。例如,若是我知道某个 JavaScript 文件可能整个网站都会使用它,咱们就能够在安装期间缓存它。这意味着另外一个引用此 JavaScript 文件的页面可以在后面的阶段轻松地从缓存中获取文件。

      清单3.2中的代码进入了 install 事件,并在此阶段将 JavaScript 文件和 hello 图片添加到缓存中。在上面的清单中,我还引用了一个叫作 cacheName 的变量。这是一个字符串,我用它来设置缓存的名称。你能够为每一个缓存取不一样的名称,甚至能够拥有一个缓存的多个不一样的副本,由于每一个新的字符串使其惟一。当看到本章后面的版本控制和缓存清除时,你将会感觉到它所带来的便利。

      在清单3.2中,你能够看到一旦缓存被开启,咱们就能够开始把资源添加进去。接下来,咱们调用了 cache.addAll() 并传入文件数组。event.waitUntil() 方法使用了 JavaScript 的 Promise 并用它来知晓安装所需的时间以及是否安装成功。

      若是全部的文件都成功缓存了,那么 Service Worker 便会安装完成。若是任何文件下载失败了,那么安装过程也会随之失败。这点很是重要,由于它意味着你须要依赖的全部资源都存在于服务器中,而且你须要注意决定在安装步骤中缓存的文件列表。定义一个很长的文件列表便会增长缓存失败的概率,多一个文件便多一份风险,从而致使你的 Servicer Worker 没法安装。

      如今咱们的缓存已经准备好了,咱们可以开始从中读取资源。咱们须要在清单3.3中添加代码,让 Service Worker 开始监听 fetch 事件。

      代码清单 3.3
      self.addEventListener('fetch', function (event) { ❶ event.respondWith( caches.match(event.request) ❷ .then(function (response) { if (response) { ❸ return response; ❹ } return fetch(event.request); ❺ }) ); });
      • ❶ 添加 fetch 事件的事件监听器
      • ❷ 检查传入的请求 URL 是否匹配当前缓存中存在的任何内容
      • ❸ 若是有 response 而且它不是 undefined 或 null 的话就将它返回
      • ❹ 不然只是如往常同样继续,经过网络获取预期的资源

      清单3.3中的代码是咱们 Service Worker 杰做的最后一部分。咱们首先为 fetch 事件添加一个事件监听器。接下来,咱们使用 caches.match() 函数来检查传入的请求 URL 是否匹配当前缓存中存在的任何内容。若是存在的话,咱们就简单地返回缓存的资源。可是,若是资源并不存在于缓存当中,咱们就如往常同样继续,经过网络来获取资源。

      若是你打开一个支持 Service Workers 的浏览器并导航至最新建立的页面,你应该会注意到相似于下图3.4中的内容。

      Figure 3.4

      图3.4 示例代码生成了带有图片和 JavaScript 文件的基础网页

      请求的资源如今应该是能够在 Service Worker 缓存中获取的。当我刷新页面时,Service Worker 会拦截 HTTP 请求并从缓存中当即加载合适的资源,而不是发起网络请求到服务器端。Service Worker 中只需短短几行代码,你便拥有了一个直接从缓存加载的网站,并能当即响应重复访问!

      附注一点,Service workers 只能在 HTTPS 这样的安全来源中使用。然而,当开在本机上开发 Service Workers 时,你可以使用 http://localhost 。Service Workers 已经创建了这样的方式,以确保发布后的安全,并且同时还兼顾了灵活性,使开发者在本机上工做变得更加容易。

      一些现代浏览器能够使用浏览器内置的开发者工具来查看 Service Worker 缓存中的内容。例如,若是你打开 Google Chrome 的开发者工具并切换至 “Application” 标签页,你可以看到相似于下图3.5中的内容。

      Figure 3.5

      图3.5 当你想看缓存中存储什么时, Google Chrome 的开发者工具会很是有用

      图3.5展现了名称为 helloWorld 的缓存项中存储了 scripts.js 和 hello.png 两个文件。如今资源已经存储在缓存中,从此这些资源的任何请求都会从缓存中当即取出。

      3.2.2 拦截并缓存

      在清单3.2中,咱们看过了如何在 Service Worker 安装期间缓存任何重要的资源,这被称之为“预缓存”。当你确切地知道你要缓存的资源时,这个示例能很好地工做,可是资源多是动态的,或者你可能对资源彻底不了解呢?例如,你的网站多是一个体育新闻网站,它须要在比赛期间不断更新,在 Service Worker 安装期间你是不会知道这些文件的。

      由于 Service Workers 可以拦截 HTTP 请求,对于咱们来讲,这是发起请求而后将响应存储在缓存中的绝佳机会。这意味着咱们改成先请求资源,而后当即缓存起来。这样一来,对于一样资源的发起的下一次 HTTP 请求,咱们能够当即将其从 Service Worker 缓存中取出。

      Figure 3.6

      图3.6 对于发起的任何 HTTP 请求,咱们能够检查资源是否在缓存中已经存在,若是没有的话再经过网络来获取

      咱们来更新下以前清单3.1中的代码。

      代码清单 3.4
      <!DOCTYPE html>
      <html>
        <head> <meta charset="UTF-8"> <title>Hello Caching World!</title> <link href="https://fonts.googleapis.com/css?family=Lato" rel="stylesheet"> ❶ <style>  #body {  font-family: 'Lato', sans-serif;  }  </style> </head> <body> <h1>Hello Service Worker Cache!</h1> <!-- JavaScript --> <script async src="/js/script.js"></script> ❷ <script>  if ('serviceWorker' in navigator) { ❸  navigator.serviceWorker.register('/service-worker.js').then(function (registration) {  console.log('ServiceWorker registration successful with scope: ', registration.scope);  }).catch(function (err) { ❹  console.log('ServiceWorker registration failed: ', err);  });  }  </script> </body> </html>
      • ❶ 添加网络字体的引用
      • ❷ 为当前页面提供功能的 JavaScript 文件
      • ❸ 首先,咱们须要检查浏览器是否支持 Service Workers
      • ❹ 若是在 Service Workers 注册期间报错,咱们能够捕获它并作出适当的处理

      相比较于清单3.1,清单3.4中的代码并无太大变化,除了咱们在 HEAD 标签中添加了一个网络字体的引用。因为这是一个可能会发生变化的外部资源,因此咱们能够在 HTTP 请求完成后缓存该资源。你还会注意到,咱们用来注册 Service Worker 的代码并无改变。事实上,除了一些例外,这段代码是注册 Service Worker 想当标准的方式。在本书中,咱们会反复使用这段样板代码来注册 Service Worker 。

      如今页面已经完成,咱们准备开始为 Service Worker 文件添加一些代码。下面的清单3.5展现了咱们要使用的代码。

      代码清单 3.5
      var cacheName = 'helloWorld'; ❶ self.addEventListener('fetch', function (event) { ❷ event.respondWith( caches.match(event.request) ❸ .then(function (response) { if (response) { ❹ return response; } var requestToCache = event.request.clone(); ❺ return fetch(requestToCache).then( ❻ function (response) { if (!response || response.status !== 200) { ❼ return response; } var responseToCache = response.clone(); ❽ caches.open(cacheName) ❾ .then(function (cache) { cache.put(requestToCache, responseToCache); ❿ }); return response; } ); }) ); });
      • ❶ 缓存的名称
      • ❷ 为 fetch 事件添加事件监听器以拦截请求
      • ❸ 当前请求是否匹配缓存中存在的任何内容?
      • ❹ 若是匹配的话,就此返回缓存并再也不继续执行
      • ❺ 这很重要,咱们克隆了请求。请求是一个流,只能消耗一次。
      • ❻ 尝试按预期同样发起原始的 HTTP 请求
      • ❼ 若是因为任何缘由请求失败或者服务器响应了错误代码,则当即返回错误信息
      • ❽ 再一次,咱们须要克隆响应,由于咱们须要将其添加到缓存中,并且它还将用于最终返回响应
      • ❾ 打开名称为 “helloWorld” 的缓存
      • ❿ 将响应添加到缓存中

      清单3.5中的代码看上去好多啊!咱们分解来看,并解释每一个部分。代码先经过添加事件监听器来进入 fetch 事件。咱们首先要作的就是检查请求的资源是否存在于缓存之中。若是存在,咱们能够就此返回缓存并再也不继续执行代码。

      然而,若是请求的资源于缓存之中没有的话,咱们就按原计划发起网络请求。在代码更进一步以前,咱们须要克隆请求。须要这么作是由于请求是一个流,它只能消耗一次。由于咱们已经经过缓存消耗了一次,而后发起 HTTP 请求还要再消耗一次,因此咱们须要在此时克隆请求。而后,咱们须要检查 HTTP 响应,确保服务器返回的是成功响应而且没有任何问题。咱们毫不想缓存一个错误的结果!

      若是成功响应,咱们会再次克隆响应。你可能会疑惑咱们为何须要再次克隆响应,请记住响应是一个流,它只能消耗一次。由于咱们想要浏览器和缓存都可以消耗响应,因此咱们须要克隆它,这样就有了两个流。

      最后,代码中使用这个响应并将其添加至缓存中,以便下次再使用它。若是用户刷新页面或访问网站另外一个请求了这些资源的页面,它会当即从缓存中获取资源,而再也不是经过网络。

      Figure 3.7

      图3.7 使用 Google Chrome 的开发者工具,咱们能够看到网络字体已经经过网络获取并添加至缓存中,以确保重复请求时速度更快

      若是你仔细看下上面的图3.7,你会注意到页面上三个资源的缓存中有新项。在上面的代码示例中,每次返回成功的 HTTP 响应时,咱们都可以动态地向缓存中添加资源。对于你可能想要缓存资源,但不太肯定它们可能更改的频率或确切地来自哪里,那么这种技术多是完美的。

      Service Workers 赋予做为开发者的你经过代码进行彻底的控制,并容许你轻松构建适合需求的自定义缓存解决方案。事实上,使用咱们前面提到的两种缓存技术能够组合起来,以使加载速度更快。彻底由你来掌控这一切。

      例如,假设你正在构建一个使用应用外壳架构的新 Web 应用。你可能会想要使用咱们在清单3.2中的代码来预缓存外壳。对于以后任何的 HTTP 请求都会使用拦截并缓存的技术进行缓存。或者你也许只想缓存现有网站中已知的、不会常常更改的部分。经过简单地拦截并缓存这些资源,就能为用户提供更好的性能,却只需短短几行代码。取决于你的状况,Service Worker 缓存能够适用你的需求,并当即使用户获得的体验全部提高。

      3.2.3 整合全部代码

      到目前为止,咱们已经运行的代码示例都是有帮助的,可是单独思考它们并不太容易。在第1章中,咱们讨论了能够用 Service Workers 来构建超棒 Web 应用的多种不一样方式。报纸 Web 应用即是其中一个,咱们能够在现实世界中使用所学到的关于 Service Workers 缓存的一切知识。我将咱们的示例应用命名为 'Progressive Times'。该 Web 应用是一个新闻网站,人们会按期访问并阅读多个页面,因此提早保存将来的页面是有意义的,以便它们可以当即加载。咱们甚至能够保存网站自己,以便用户能够离线浏览。

      咱们的示例 Web 应用包含一些来自世界各地的有趣新闻。不管你是否相信,这个新闻网站的全部报道都是真实的,而且有可靠的信息来源!Web 应用包含绝大多数你能够想象的网站元素,好比 CSS、JavaSript 和图片。为了保持示例代码的基础性,我还为每篇文章准备了扁平的 JSON 文件,在现实生活中,这应该指向一个后端端点以获取相似格式的数据。就自身而言,这个 Web 应用并不怎么使人印象深入,可是当咱们开始利用 Service Workers 的能力时,即可以把它提高到一个新的水平。

      Web 应用使用应用外壳架构来动态地获取每篇文章的内容并将数据填充到页面上。

      Figure 3.8

      图3.8 Progressive Times 示例应用使用应用外壳架构

      使用应用外壳架构还意味着咱们能够利用预缓存技术,为了确保 Web 应用的重复访问可以当即加载。咱们还能够假定访问者会点击连接和阅读新闻文章的完整内容。若是当 Service Worker 安装后咱们将这些内容缓存,这意味着对于他们下个页面的加载速度会快不少。

      让咱们把到目前为止在本章在所学的整合起来,并为 Progressive Times 应用添加 Service Worker ,它将预处理重要资源,并缓存任何其余请求。

      代码清单 3.6
      var cacheName = 'latestNews-v1'; // 在安装过程当中缓存咱们已知的资源 self.addEventListener('install', event => { event.waitUntil( caches.open(cacheName) .then(cache => cache.addAll([ ❶ './js/main.js', './js/article.js', './images/newspaper.svg', './css/site.css', './data/latest.json', './data/data-1.json', './article.html', './index.html' ])) ); }); // 缓存任何获取的新资源 self.addEventListener('fetch', event => { ❷ event.respondWith( caches.match(event.request, { ignoreSearch: true }) ❸ .then(function (response) { if (response) { return response; ❹ } var requestToCache = event.request.clone(); return fetch(requestToCache).then( ❺ function (response) { if (!response || response.status !== 200) { return response; } var responseToCache = response.clone(); caches.open(cacheName) .then(function (cache) { cache.put(requestToCache, responseToCache); ❻ }); return response; }); }) ); });
      • ❶ 在安装期间打开缓存并存储一组资源进行缓存
      • ❷ 监听 fetch 事件
      • ❸ 咱们想要忽略任何查询字符串参数,这样便不会获得任何缓存未命中
      • ❹ 若是咱们发现了成功的匹配,就此返回缓存并再也不继续执行
      • ❺ 若是咱们没在缓存中找到任何内容,则发起请求
      • ❻ 存储在缓存中,这样便不须要再次发起请求

      清单3.6中的代码是安装期间的预缓存和获取资源时进行缓存的组合应用。该 Web 应用使用了应用外壳架构,这意味着咱们能够利用 Service Worker 缓存来只请求填充页面所需的数据。咱们已经成功存储了外壳的资源,因此剩下的就是来自服务器的动态新闻内容。

      这个网页是托管在 Github 的,若是想亲身体验,能够导航至 bit.ly/chapter-pwa-3 轻松访问。事实上,我将本书中所使用的全部代码示例都添加到了这个 Github 仓库中。

      每章都有自述文件,它解释了你须要作什么来开始构建和实验每章中的示例代码。大约90%的章节都只是前端代码,因此你所须要的只是启动你的本地主机并开始使用。还有一点值得注意的是你须要在 http://localhost 环境上运行代码,而不是 file:// 。

    • 3.3 缓存先后的性能比对

      此刻,我但愿我已经说服了你,Service Worker 缓存是多么优秀。还没!?好吧,但愿这些使用缓存后得到的性能改善能令你改变主意!

      以咱们的 Progressive Times 应用为例,能够比较使用 Service Worker 缓存先后的差异。我最喜欢的一种网站性能测试的方法是使用 webpagetest.org 这个工具。

      Figure 3.9

      图3.9 WebPagetest.org 是一个免费工具,能够使用来自世界各地的真实设备对你的网站进行测试

      Webpagetest.org 是一个很棒的工具。只需简单地输入网站 URL,它就可让你使用真实设备和各类类型的浏览器从世界任何地点来分析你的网站。测试运行在真实设备上,并为你提供关于网站有用的分类和性能分析。最棒的是,它是开源而且彻底无偿使用的。

      若是我经过 Webpagetest.org 运行咱们的示例应用,它会生成相似下面图3.10的表格。

      Figure 3.10

      图3.10 WebPagetest.org 经过使用真实设备生成有关 Web 应用性能的有用信息

      为了在真实设备上测试咱们示例应用的性能,我在 Webpagetest 上使用了新加坡的 2G 节点。若是你曾经试图经过缓慢的网络访问一个网站的话,那么你会知道,等待网站完成是多么使人讨厌的过程。对于 Web 开发者来讲,重要的是咱们应该像用户同样对网站进行测试,其中包括使用速度较慢的移动网络及低端设备。一旦 Webpagetest 完成对 Web 应用的性能分析,它会生成你在上面图3.10中看到的结果。

      首次访问,页面加载须要大概12秒。这不怎么理想,但对于很慢的 2G 网络来讲这也是意料之中的。可是,当你再次访问时,页面的加载时间将不到0.5秒,而且不会发送 HTTP 请求给服务器。示例应用使用了应用外壳架构,若是你还记得这种设计的话,你会知道从此任何请求都能快速响应,由于所需资源已经缓存了。若是使用正确的话, Service Worker 缓存会极大地提高应用的总体加载速度,而且不管用户使用何种设备和网络,都能加强他们的浏览体验。

    • 3.4 深刻 Service Workers 缓存

      在本章中,咱们先看了如何使用 Service Worker 缓存来提高 Web 应用的性能。本章的剩余部分,咱们将密切关注如何对文件进行版本控制,以确保没有不匹配的缓存,以及一些在使用 Service Worker 缓存时可能遇到的问题。

      3.4.1 对文件进行版本控制

      你的 Service Worker 须要有一个更新的时间点。若是你更改了 Web 应用,并想要确保用户接收到最新版本的文件,而不是老版本的。你能够想象一下,错误地提供旧文件会对网站形成严重破坏!

      Service Workers 的伟大之处在于每当对 Service Worker 文件自己作出任何更改时,都会自动触发 Service Worker 的更新流程。在第1章中,咱们看过了 Service Worker 生命周期。记住当用户导航至你的网站时,浏览器会尝试在后台从新下载 Service Worker 。即便下载的 Service Worker 文件与当前的相比只有一个字节的差异,浏览器也会认为它是新的。

      这个有用的功能给予咱们完美的机会来使用新文件更新缓存。更新缓存时能够使用两种方式。第一种方式,能够更新用来存储缓存的名称。若是回看清单3.2中的代码,能够看到变量 cacheName 使用的值为 'helloWorld' 。若是你把这个值更新为 'helloWorld-2',这会自动建立一个新缓存并开始从这个缓存中提供文件。以前的缓存将被孤立并再也不使用。

      第二种方式,就我我的感受,它应该是最实用的,就是实际上对文件进行版本控制。这种技术被称为“缓存破坏”,并且已经存在不少年了。当静态文件被缓存时,它能够存储很长一段时间,而后才能到期。若是期间你对网站进行更新,这可能会形成困扰,由于文件的缓存版本存储在访问者的浏览器中,它们可能没法看到所作的更改。经过使用一个惟一的文件版本标识符来告诉浏览器该文件有新版本可用,缓存破坏解决了这个问题。

      例如,若是咱们想在 HTML 中添加一个 JavaScript 文件的引用,咱们可能但愿在文件名末尾附加一个哈希字符串,相似于下面的代码。

      <script type="text/javascript" src="/js/main-xtvbas65.js"></script>

      缓存破坏背后的理念是每次更改文件时建立一个全新的文件名,这样以确保浏览器能够获取最新的内容。为了更好地解释缓存破坏,让咱们想象一下,在咱们的报纸 Web 应用中有这样一个场景。好比说你有一个文件叫作 main.js,你将它存储在缓存之中。根据 Service Worker 的设置方式,每次都会从缓存中获取这个版本的文件。若是你更新了 main.js 文件,Service Worker 仍然会拦截并返回老的缓存版本,尽管你想要的是新版本的文件!可是,若是你将文件重命名为 main.v2.js 并在这个新版本文件中更新了代码,你能够确保浏览器每次都会获得最新版本的文件。用这种方式,你的报纸应用永远都会返回最新的结果给用户。

      实现此解决方案有多种不一样的方法,使用哪一种都取决于你的编码环境。一些开发者更喜欢在构建期间生成这些哈希文件名称,而其余的开发者可能会使用代码并动态生成文件名。不管使用哪一种方式,这项技术都是通过验证的,可确保你始终提供正确的文件。

      3.4.2 处理额外的查询参数

      当 Service Worker 检查已缓存的响应时,它使用请求 URL 做为键。默认状况下,请求 URL 必须与用于存储已缓存响应的 URL 彻底匹配,包括 URL 查询部分的任何字符串。

      若是对文件发起的 HTTP 请求附带了任意查询字符串,而且查询字符串会更改,这可能会致使一些问题。例如,若是你对一个先前匹配的 URL 发起了请求,则可能会发现因为查询字符串略有不一样而致使该 URL 找不到。当检查缓存时想要忽略查询字符串,使用 ignoreSearch 属性并设置为 true 。

      代码清单 3.7
      self.addEventListener('fetch', function (event) { event.respondWith( caches.match(event.request, { ignoreSearch: true }).then(function (response) { return response || fetch(event.request); }) ); });

      清单3.7中代码使用了 ignoreSearch 选项来忽略请求参数和缓存请求的 URL 的查询部分。你能够经过使用其余忽略选项 (如 ignoreMethod 和 ignoreVary) 进一步扩展。例如,ignoreMethod 选项会忽略请求参数的方法,因此 POST 请求能够匹配缓存中的 GET 项。ignoreVary 选项会忽略已缓存响应中的 vary 首部。

      3.4.3 须要多少内存?

      每当我与开发者们谈论 Service Worker 缓存时,常常会出现围绕内存和存储空间的问题。Service Worker 会使用多少空间来进行缓存?这种内存使用是否会影响到个人设备?

      最诚实的回答是真的取决于你的设备和它的存储状况。就像全部浏览器的存储,若是设备受到存储压力,浏览器能够自由地丢弃它。这不必定是一个问题,由于数据能够在须要时再次从网络中获取。在第7章中,咱们会来看看另外一种类型的存储,它被称之为“持久化存储”,它能够用来更持久地存储缓存数据。

      如今,老的浏览器仍然可以在它们内存中存储缓存的响应,而且使用的空间不一样于 Service Worker 用来缓存资源的空间。惟一的不一样的是 Service Worker 缓存让你来掌控并容许你经过程序来建立、更新和删除缓存项,从而使你能够不经过网络链接来访问资源。

      3.4.4 将缓存提高到一个新的高度 - Servicer Worker toolbox

      若是你发现本身在 Service Workers 中编写缓存资源的代码是有规律的,你可能会发现 Service Worker toolbox (https://github.com/GoogleChrome/sw-toolbox) 是有帮助的。它是由 Google 团队编写的,它是一个辅助库,以使你快速开始建立本身的 Service Workers,内置的处理方法可以涵盖最多见的网络策略。只需短短几行代码,你就能够决定是否只是要从缓存中提供指定资源,或者从缓存中提供资源并提供备用方案,或者只能从网络返回资源而且永不缓存。这个库可让你彻底控制缓存策略。

      Figure 3.11

      图3.11 Service Worker toolbox 提供了用于建立 Service Workers 的辅助库。

      Service Worker toolbox 为你提供了一种快速简便的方式来复用常见的网络缓存策略,而不是一次又一次地重写。比方说,你但愿确保始终从缓存中取出 CSS 文件,但若是获取不到的话,则回退到经过网络来获取文件。使用 Service Worker toolbox,只需按照同本章中同样的方式注册 Service Worker 便可。接下来,在 Service Worker 文件中导入 toolbox 并开始定义你想要缓存的路由。

      代码清单 3.8
      importScripts('/sw-toolbox/sw-toolbox.js'); ❶ toolbox.router.get('/css/(.*)', toolbox.cacheFirst); ❷
      • ❶ 加载 Service Worker toolbox 库
      • ❷ 开始缓存路径匹配 '/css' 的任何请求

      在清单3.8中,先使用 importScripts 函数导入 Service Worker toolbox 库。Service Workers 能够访问一个叫作 importScripts() 的全局函数,它能够将同一域名下脚本导入至它们的做用域。这是将另外一个脚本加载到现有脚本中的一种很是方便的方法。它保持代码整洁,也意味着你只须要在须要时加载文件。

      一旦脚本导入后,咱们就能够开始定义想要缓存的路由。在上面的清单中,咱们定义了一个路由,它匹配路径是 /css/,而且永远使用缓存优选的方式。这意味着资源会永远从缓存中提供,若是不存在的话再回退成经过网络获取。Toolbox 还提供了一些其余内置缓存策略,好比只经过缓存获取、只经过网络获取、网络优先、缓存 优先或者尝试从缓存或网络中找到最快的响应。每种策略均可以应用于不一样的场景,甚至你能够混用不一样的路由来匹配不一样的策略,以达到最佳效果。

      Service Worker toolbox 还为你提供了预缓存资源的功能。在清单3.2中,咱们在 Service Worker 安装期间预缓存了资源,咱们能够使用 Service Worker toolbox 以一样的方式来实现,而且只须要一行代码。

      代码清单 3.9

      toolbox.precache(['/js/script.js', '/images/hello.png']);

      清单3.9中的代码接收在 Service Worker 安装步骤中应该被缓存的 URL 数组。这行代码会确保在 Service Worker 安装阶段资源被缓存。

      每当我接手一个新项目时,毫无疑问我喜欢使用的库就是 Service Worker toolbox 。它简化了你的代码并为你提供通过验证的缓存策略,只需几行代码即可实现。事实上,咱们在第2章剖析过的 Twitter PWA 也使用了 Service Worker toolbox 来使得代码更容易理解,并依赖于这些通过验证的缓存方法。

    • 3.5 总结

      • HTTP 缓存是提高网站性能的绝佳方法,但它并非没有缺陷。
      • Service Worker 缓存极其强大,由于它赋予你经过程序来精准控制如何缓存资源。当与 HTTP 缓存一块儿使用时,你将享受二者同时带来的便利。
      • 正确使用 Service Worker 缓存会带来巨大的性能提高和带宽节省。
      • 能够使用多种不一样的方法来缓存资源,而且每种方法均可以根据用户的须要进行调整。
      • WebPagetest 是一个很棒的工具, 它使用真实设备来测试 Web 应用的性能。
      • Service Worker toolbox 是个方便的库,它为你提供通过验证的缓存技术
      • 4.1 Fetch API

        做为 Web 开发者,为了异步更新应用,咱们一般须要从服务器获取数据的能力。传统上,这个数据是经过使用 JavaScript 和 XMLHttpRequest 对象来获取的。也被称为 AJAX,它是开发者求之不得的,由于它容许你更新网页,而没必要经过在后台进行 HTTP 请求来从新加载页面。在咱们的示例应用 Progressive Times 中,咱们也会异步获取新闻文章列表。

        若是你曾经实现过复杂的逻辑来从服务器获取数据,使用 XMLHttpRequest 对象来编写代码会至关棘手。随着开始添加愈来愈多的逻辑和回调函数,代码很快就变得一团糟。

        代码清单 4.1
        var request;
        if (window.XMLHttpRequest) { request = new XMLHttpRequest(); } else if (window.ActiveXObject) { try { request = new ActiveXObject('Msxml2.XMLHTTP'); } catch (e) { try { request = new ActiveXObject('Microsoft.XMLHTTP'); } catch (e) {} } } request.onreadystatechange = function () { if (this.readyState == 4 && this.status == 200) { doSomething(this.responseText); } }; // 打开,发送 request.open('GET', '/some/url', true); request.send();

        清单4.1中的代码看上去有些多,只是简单地发起一个 HTTP 请求而已!有趣的是,XMLHttpRequest 对象最初是由 Microsoft Exchange Server 的 Outlook Web Access 的开发人员建立的。通过一系列的置换以后,它最终成为现在在 JavaScript 中发起 HTTP 请求的标准。上面的示例完成了它的目的,但代码自己并不够简洁。上面的代码还有另一个问题,你的逻辑越复杂,代码就越复杂。过去,有许多库和技术能够使这种代码更简单,更易于阅读,诸如 jQuery 和 Zepto 等流行库,它们具备更简洁的 API 。

        幸运的是,现代浏览器厂商都意识到这是须要改进的,这也正是 Fetch API 诞生的缘由。Fetch API 是 Service Worker 全局做用域的一部分,它能够用来在任何 Service Worker 中发起 HTTP 请求。截止到目前为止,咱们一直在 Service Worker 代码中使用 Fetch API,但咱们还不曾深刻研究它。咱们来看几个代码示例,以便更好地理解 Fetch API 。

        代码清单 4.2
        fetch('/some/url', { ❶ method: 'GET' }).then(function (response) { ❷ // 成功 }).catch(function (err) { ❸ // 出问题了 });
        • ❶ 使用 GET 请求访问的 URL
        • ❷ 若是成功,则返回响应
        • ❸ 若是出问题了,咱们能够作出相应的响应

        清单4.2中的代码是 Fetch API 实际应用的基础示例。你可能还注意到了没有回调函数和事件,取而代之的是 then() 方法。这个方法是 ES6 中新功能 Promises 的一部分,目的是使咱们的代码更具可读性、更便于开发者理解。Promise 表明异步操做的最终结果,咱们不知道实际的结果值是什么,直到未来某个时刻操做完成。

        上面的代码看起来很容易理解,可是使用 Fetch API 发起 POST 请求呢?

        代码清单 4.3
        fetch('/some/url', { ❶ method: 'POST', headers: { 'auth': '1234' ❷ }, body: JSON.stringify({ ❸ name: 'dean', login: 'dean123', }) }) .then(function (data) { ❹ console.log('Request success: ', data); }) .catch(function (error) { ❺ console.log('Request failure: ', error); });
        • ❶ 使用 POST 请求访问的 URL
        • ❷ 请求中包含的 headers
        • ❸ POST 请求的 body
        • ❹ 若是成功,则返回响应
        • ❺ 若是出问题了,咱们能够作出相应的响应

        假如说,你想要使用 POST 请求将某个用户详情发送到服务器。在上面的清单中,只需在 fetch 选项中将 method 更改成 POST 并添加 body 参数。使用 Promises 不只能够使代码更整洁,并且还能够使用链式代码,以便在 fetch 请求之间共享逻辑。

        目前全部支持 Service Workers 的浏览器中均可以使用 Fetch API ,但若是想在不支持的浏览器上使用的话,你可能要考虑使用 polyfill 。它只是一段代码,为你提供你所指望的现代浏览器功能。例如,若是最新版本的 IE 具备一些你所须要的功能,但旧版本中没有,polyfill 能够用来为老旧浏览器提供相似的功能。能够把它看成是 API 的包装,用来保持 API 完整可用。这有一个由 Github 团队编写的 polyfill (https://github.com/github/fetch),它会确保老旧浏览器可以使用 fetch API 发起请求。只须要将它放置在网页中,你便可以开始使用 fetch API 来编写代码。

      • 4.2 Fetch 事件

        Service Worker 拦截任何发出的 HTTP 请求的能力使得它真的很是强大。属于此 Service Worker 做用域内的每一个 HTTP 请求将触发此事件,例如 HTML 页面、脚本、图片、CSS,等等。这可让开发者彻底控制浏览器如何处理响应这些资源获取的方式。

        在第1章中,咱们在实战中看过了 fetch 事件的基础示例。还记得独角兽吗?

        代码清单 4.4
        self.addEventListener('fetch', function (event) { ❶ if (/\.jpg$/.test(event.request.url)) { ❷ event.respondWith( fetch('/images/unicorn.jpg')); ❸ } });
        • ❶ 为 fetch 事件添加事件监听器
        • ❷ 检查传入的 HTTP 请求是不是 JPEG 类型的图片
        • ❸ 尝试获取独角兽的图片并用它做为替代图片来响应请求

        在上面的代码中,咱们监听了 fetch 事件,若是 HTTP 请求的是 JPEG 文件,咱们就拦截请求并强制返回一张独角兽图片,而不是原始 URL 请求的图片。上面的代码会为该网站上的每一个 JPEG 图片请求执行一样的操做。对于任何其余文件类型,它会直接忽略并继续执行。

        同时清单4.4中的代码是个有趣的示例,它并无展现出 Service Workers 的真正能力。咱们会在这个示例的基础上更进一步,返回咱们自定义的 HTTP 响应。

        代码清单 4.5
        self.addEventListener('fetch', function (event) { ❶ if (/\.jpg$/.test(event.request.url)) { ❷ event.respondWith( new Response('<p>This is a response that comes from your service worker!</p>', { headers: { 'Content-Type': 'text/html' } ❸ }); ); } });
        • ❶ 为 fetch 事件添加事件监听器
        • ❷ 检查传入的 HTTP 请求是不是 JPEG 类型的图片
        • ❸ 建立自定义 Response 并做出相应地响应

        在清单4.5中,代码经过监听 fetch 事件的触发来拦截任何 HTTP 请求。接下来,它判断传入请求是不是 JPEG 文件,若是是的话,就使用自定义 HTTP 响应进行响应。使用 Service Workers ,你可以建立自定义 HTTP 响应,包括编辑响应首部。此功能使得 Service Workers 极其强大,同时也能够理解为何它们须要经过 HTTPS 请求才能获取。想象一下,若是不是这样的话,黑客动动手指即可以完成一些恶意的操做!

        4.2.1 Service Worker 生命周期

        就在本书开头的第1章中,你了解过了 Service Worker 生命周期以及在它构建 PWA 时所扮演的角色。再来仔细看一遍下面的图。

        Figure 4.1

        图4.1 Service Worker 生命周期

        看过上面的图,你会想到当用户第一次访问网站的时候,并不会有激活的 Service Worker 来控制页面。只有当 Service Worker 安装完成而且用户刷新了页面或跳转至网站的其余页面,Service Worker 才会激活并开始拦截请求。

        为了更清楚地解释这个问题,咱们想象一下,一个单页应用 (SPA) 或一个加载完成后使用 AJAX 进行交互的网页。当注册和安装 Service Worker 时,咱们将使用到目前为止在本书中所介绍的方法,页面加载后发生的任何 HTTP 请求都将被忽略。只有当用户刷新页面,Service Worker 才会激活并开始拦截请求。这并不理想,你但愿 Service Worker 能尽快开始工做,并包括在 Service Worker 未激活期间所发起的这些请求。

        若是你想要 Service Worker 当即开始工做,而不是等待用户跳转至网站的其余页面或刷新本页面,有一个小技巧,你能够用它来当即激活你的 Service Worker 。

        代码清单 4.6
        self.addEventListener('install', function(event) { event.waitUntil(self.skipWaiting()); });

        清单4.6中的代码重点在于 Service Worker 的 install 事件里。经过使用 skipWaiting() 函数,最终会触发 activate 事件,并告知 Service Worker 当即开始工做,而无需等待用户跳转或刷新页面。

        Figure 4.2

        图4.2 self.skipWaiting() 会使 Service Worker 解雇当前活动的 worker 而且一旦进入等待阶段就会激活自身

        skipWaiting() 函数强制等待中的 Service Worker 被激活 。self.skipWaiting() 函数还能够与 self.clients.claim() 一块儿使用,以确保底层 Service Worker 的更新当即生效。

        下面清单4.7中的代码能够结合 skipWaiting() 函数,以确保 Service Worker 当即激活自身。

        代码清单 4.7
        self.addEventListener('activate', function(event) { event.waitUntil(self.clients.claim()); });

        同时使用清单4.6和4.7中的代码,能够快速激活 Service Worker。若是你的网站在页面加载后会发起复杂的 AJAX 请求,那么这些功能对你来讲是完美的。若是你的网站主要是静态页面,而不是在页面加载后发起 HTTP 请求,那么你可能不须要使用这些功能。

      • 4.3 Fetch 实战

        正如咱们在本章中所看到的,Service Workers 为开发者提供了无限的网络控制权。拦截 HTTP 请求、修改 HTTP 响应以及自定义响应,这些只是经过进入 fetch 事件所获取到的能力的一小部分。

        直到此刻,绝大部分咱们所看过的代码示例并不是真实世界的示例。在下一节中,咱们将深刻两种有用的技术,可以使你的网站更快,更具吸引力和更富弹性。

        4.3.1 使用 WebP 图片的示例

        现在,图片在 Web 上扮演着重要的角色。能够想象下一个网页上彻底没有图片的世界!高质量的图片能够真正地使网站出彩,但不幸的是它们也是有代价的。因为它们的文件大小较大,因此须要下载的内容多,致使页面加载慢。若是你曾经使用过网络链接较差的设备,那么你会了解到这种体验有多使人沮丧。

        你可能熟悉 WebP 这种图片格式。它是由 Google 团队开发的,与 PNG 图片相比,文件大小减小26%,与 JPEG 图片相比,文件大小大约减小25-34%。这是一个至关不错的节省,最棒的是,选择这种格式图像质量不会受到影响。

        Figure 4.3

        图4.3 与原始格式相比,WebP 图片的文件大小要小得多,而且图片的质量没有显着差别

        图4.3并排展现了两张内容相同、图片质量无显著差异的图片,左边是 JEPG,右边是 WebP 。默认状况下,只有 Chrome、Opera 和 Android 支持 WebP 图片,不幸的是目前 Safari、Firefox 和 IE 还不支持。

        支持 WebP 图片的浏览会经过在每一个 HTTP 请求中传递 accept: image/webp 首部通知你它们可以支持。鉴于咱们拥有 Service Workers,这彷佛是一个完美的机会,以开始拦截请求,并将更轻,更精简的图片返回给可以渲染它们的浏览器。

        假设有下面这样一个基础的网页。它只是引用了一张纽约布鲁克林大桥的图片。

        代码清单 4.8
        <!DOCTYPE html>
        <html>
          <head> <meta charset="UTF-8"> <title>Brooklyn Bridge - New York City</title> </head> <body> <h1>Brooklyn Bridge</h1> <img src="./images/brooklyn.jpg" alt="Brooklyn Bridge - New York"> <script>  // 注册 service worker  if ('serviceWorker' in navigator) {  navigator.serviceWorker.register('./service-worker.js').then(function (registration) {  // 注册成功  console.log('ServiceWorker registration successful with scope: ', registration.scope);  }).catch(function (err) {  // 注册失败 :(  console.log('ServiceWorker registration failed: ', err);  });  }  </script> </body> </html>

        清单4.8中的图片是 JPEG 格式,大小为137KB。若是将它转换为 WebP 格式的并存储在服务器上,就可以为支持 WebP 的浏览器返回 WebP 格式,为不支持的返回原始格式。

        咱们在 Service Worker 中构建代码以开始拦截此图片的 HTTP 请求。

        代码清单 4.9
        "use strict"; // 监听 fetch 事件 self.addEventListener('fetch', function(event) { if (/\.jpg$|.png$/.test(event.request.url)) { ❶ var supportsWebp = false; if (event.request.headers.has('accept')) { ❷ supportsWebp = event.request.headers .get('accept') .includes('webp'); } if (supportsWebp) { ❸ var req = event.request.clone(); var returnUrl = req.url.substr(0, req.url.lastIndexOf(".")) + ".webp"; ❹ event.respondWith( fetch(returnUrl, { mode: 'no-cors' }) ); } } });
        • ❶ 检查传入的 HTTP 请求是不是 JPEG 或 PNG 类型的图片
        • ❷ 检查 accept 首部是否支持 WebP
        • ❸ 浏览器是否支持 WebP?
        • ❹ 建立返回 URL

        上面清单中的代码不少,咱们分解来看。最开始的几行,我添加了事件监听器来监放任何触发的 fetch 事件。对于每一个发起的 HTTP 请求,我会检查当前请求是不是 JEPG 或 PNG 图片。若是我知道当前请求的是图片,我能够根据传递的 HTTP 首部来返回最适合的内容。在本例中,我检查每一个首部并寻找 image/webp 的 mime 类型。一旦知道首部的值,我便能判断出浏览器是否支持 WebP 并返回相应的 WebP 图片。

        一旦 Service Worker 激活并准备好,对于支持 WebP 的浏览器,任何 JPEG 或 PNG 图片的请求都会返回一样内容的 WebP 图片。若是浏览器不支持 WebP 图片,它不会在 HTTP 请求首部中声明支持,Service Worker 会忽略该请求并继续正常工做。

        一样内容的 WebP 图片只有87KB,相比于原始的 JPEG 图片,咱们节省了59KB,大约是原始文件大小的37%。对于使用移动设备的用户,浏览整个网站会节省想当多的带宽。

        Service Workers 开启了一个无限可能性的世界,这个示例能够进行扩展,包含一些其余的图片格式,甚至是缓存。你能够轻松地支持 叫作 JPEGXR 的 IE 改进图片格式。咱们没有理由不为咱们的用户提供更快的网页!

        4.3.2 使用 Save-Data 首部的示例

        我最近在出国旅行,当我迫切须要从航空公司的网站获取一些信息时,我使用的是 2G 链接,页面永远在加载中,最终我完全放弃了。回国后,我还得向手机运营商支付平常服务费用,真是太让人不爽了!

        在全球范围内,4G网络覆盖面正在迅速发展,但仍有很长的路要走。在2007年末,3G网络仅覆盖了孟加拉国、巴西、中国、印度、尼日利亚、巴基斯坦和俄罗斯等国家,将近全球人口的50%。虽然移动网络覆盖面愈来愈广,但在印度一个500MB的数据包须要花费至关于17个小时的最低工资,这听起来使人难以想象。

        幸运的是,诸如 Google Chrome、Opera 和 Yandex 这样的浏览器厂商已经意识到众多用户所面临的痛苦。使用这些浏览器的最新版本,用户将有一个选项,以容许他们“选择性加入”节省数据的功能。经过启动这项功能,浏览器会为每一个 HTTP 请求添加一个新的首部。这是咱们这些开发者的机会,寻找这个首部并返回相应的内容,为咱们的用户节省数据。例如,若是用户开启了节省数据选项,你能够返回更轻量的图片、更小的视频,甚至是不一样的标记。这是一个简单的概念,却行之有效!

        这听上去是使用 Service Worker 的完美场景!在下节中,咱们将编写代码,它会拦截请求,检查用户是否“选择性加入”节省数据并返回“轻量级”版本的 PWA 。

        还记得咱们在第3章中构建的 PWA 吗?它叫作 Progressive Times,包含来自世界各地的有趣新闻。

        Figure 4.4

        图4.4 Progressive Times 示例应用是贯穿本书的基础应用

        在 Progressive Times 应用中,咱们使用网络字体来提高应用的外观感觉。

        这些字体是从第三方服务下载的,大约30KB左右。虽然网络字体真的能加强网页的外观感受,但若是用户只是想节省数据和金钱,那么网页字体彷佛是没必要要的。不管用户的网络链接状况如何,你的 PWA 都没有理由不去适应用户。

        不管你是使用台式机仍是移动设备,启用此功能都是至关简单的。若是是在移动设备上,你能够在菜单的设置里开启。

        Figure 4.5

        图4.5 能够在移动设备或手机上开启节省数据功能。注意红色标注的区域。

        一旦设置启用后,每一个发送到服务器的 HTTP 请求都会包含 Save-Data 首部。若是使用开发者工具查看,它看起来就以下图所示。

        Figure 4.6

        图4.6 启用了节省数据功能,每一个 HTTP 请求都会包含 Save-Data 首部

        一旦启用了节省数据功能,有几种不一样的技术能够将数据返回给用户。由于每一个 HTTP 请求都会发送到服务器,你能够根据来自服务器端代码中 Save-Data 首部来决定提供不一样的内容。然而,只需短短几行 JavaScript 代码就能够使用 Service Workers 的力量,你能够轻松地拦截 HTTP 请求并相应地提供更轻量级的内容。若是你正在开发一个 API 驱动的前端应用,而且彻底没有访问服务器,那这就是个完美的选择。

        Service Workers 容许你拦截发出的 HTTP 请求,进行检测并根据信息采起行动。使用 Fetch API,你能够轻松实现一个解决方案来检测 Save-Data 首部并提供更轻量级的内容。

        咱们开始建立一个名为 service-worker.js 的 JavaScript 文件,并添加清单4.10中的代码。

        代码清单 4.10
        "use strict"; this.addEventListener('fetch', function (event) { if(event.request.headers.get('save-data')){ // 咱们想要节省数据,因此限制了图标和字体 if (event.request.url.includes('fonts.googleapis.com')) { // 不返回任何内容 event.respondWith(new Response('', {status: 417, statusText: 'Ignore fonts to save data.' })); } } });

        基于咱们已经看过的示例,清单4.10中代码应该比较熟悉了。在代码的开始几行中,添加了事件监听器以监放任何触发的 fetch 事件。对于每一个发起的请求,都会检查首部以查看是否启用了 Save-Data 。

        若是启用了 Save-Data 首部,我会检查当前 HTTP 请求是不是来自 “fonts.googleapis.com” 域名的网络字体。由于我想为用户节省任何没必要要的数据,我返回一个自定义的 HTTP 响应,状态码为417,状态文本是自定义的。HTTP 状态代码向用户提供来自服务器的特定信息,而417状态码表示“服务器不能知足 Expect 请求首部域的要求”。

        经过使用这项简单的技术和几行代码,咱们可以减小页面的总体下载量,并确保用户节省了任何没必要要的数据。这项技术能够进一步扩展,定制返回低质量的图片或者网站上其余更大的文件下载。

        若是你想实际查看本章中的任何代码,它托管在 Github 上,能够经过导航至 bit.ly/chapter-pwa-4 轻松访问。

      • 4.4 总结

        • Fetch API 是一个新的浏览器 API,它旨在使代码更简洁、更便于阅读
        • Fetch 事件容许你拦截任何浏览器发出的 HTTP 请求。这个功能极其强大,它容许你修改响应,甚至是建立自定义的 HTTP 响应,而不与服务器通讯
        • 与 PNG 图片相比,WebP 图片的文件大小减小了26%,与 JPEG 图片相比,WebP 图片的文件大小大约减小了25-34%。
        • 使用 Service Workers,你可以进入 fetch 事件并查看浏览器是否支持 WebP 图片。使用这项技术,你能够为用户提供更小的图片,从而提高页面加载速度
        • 一些现代浏览器能够“选择性加入”功能以容许用户节省数据。若是启用此功能,浏览器会为每一个 HTTP 请求添加一个新的首部,使用 Service Workers 能够进入 fetch 事件并决定是否返回网站的“轻量级”版本
相关文章
相关标签/搜索