PWA书籍
第1部分: 定义 PWA
2015年,国际电信联盟预估到15年年底全球上网人口将到达32亿,也就是说全球将近一半的人口都在上网。 想象一下每秒钟的上网人数,32亿。大约32000个足球场才装得下这么多人!这几乎是一个大到无法理解的数字。当这些人上网时,他们使用的设备不尽相同、他们的网速也各不相同、甚至同一个的网速也会变化。作为 Web 开发者,试图满足所有这些不同的场景似乎令人望而生畏!这正是 PWA 出现的契机。它们赋予了开发者可以构建速度更快、富有弹性并且更吸引人的网站的能力,这些网站能够被全球数十亿人访问。在本书的第1部分中,我们将直接深入地定义到底什么才是 PWA 。
在第1章中,你将了解渐进式网络应用 ( Progressive Web Apps ),即我们所熟知的 PWA,以及 PWA 带来的好处。然后,我们一起来看看已经利用 PWA 的能力来改善用户浏览体验的企业。我们还会详细分析一个真实世界中 PWA ,并了解下像 Twitter 和 Flipkart 这样的公司是如何建立自己的 PWA 的。PWA 的关键组成部分是 Service Worker, 我们将深入介绍此主题,以及了解它在 Web 浏览器中加载时所经历的生命周期。
在第2章中,首先介绍了构建 PWA 时可以使用的不同架构方法,以及如何最佳地组织你的代码。我们将研究两种不同的方法,“汲取功能”或“应用外壳架构” - 这两种方法都可以满足项目的需要。PWA 最棒的一点就是你无需重写已存在的 Web 应用便能开始使用 PWA 的功能,只要你觉得这些功能会使用户受益并提升他们的体验, 就可以添加它们。最后,本章会以剖析一个现有的 PWA 来结尾,该 PWA 是 Twitter 团队开发的 Twitter Lite ( 精简版 Twitter ) 。
在第1部分结束之际,你应该对 PWA 是什么,以及它们能带给用户的好处有一个清晰的认知。第1部分将为本书的下一部分奠定基础,在下一部分中我们将直接进入编码环节,从头开始构建一个 PWA 。
1.1 PWA 有什么优势?
在我们开始探索为什么 PWA 对于当今 Web 世界是个重大飞跃之前,值得回忆下自 Web 问世以来的历程。这要追溯到1990年的圣诞节,Tim Berners-Lee 爵士和他在 CERN 的团队创建了工作网络所需的所有工具,他们创建了 HTTP、HTML 和 WorldWideWeb (全世界第一个网页浏览器)。WorldWideWeb 只能运行由超链接的简单纯文本组成的网页。事实上,这些第一代的网页仍然在线,并且可以浏览!
回到现在,我们所浏览的网页与最初的网页并没有太大的不同。当然,现在我们有了像 CSS 和 Javacript 这样的功能,但网页的核心依旧是使用 HTML、HTTP 以及一些其他构建模块来构建的,这些都是 Tim Berners-Lee 及他的团队在多年前所创建的。这些辉煌的构建模块意味着 Web 已经能够以惊人的速度增长。然而,我们用来访问网页的设备数量也在不断增长。无论你的用户是在旅途中还是坐在书桌前,他们都无时无刻不在获取信息。我们对于 Web 的期望从未如此之高。
虽然我们的移动设备变得愈发强大,但我们的移动网络并不是总能满足需求。如果你使用智能手机,你就会知道移动连接是有多么的脆弱。2G、3G 或 4G 这些连接本身都很不错,但是它们时常会失去连接,或者网速变得很差。如果你的业务是跟网络相关的,那这就是你需要去解决的问题。
从历史上来说,原生应用 (下载到手机的) 已经能够提供更好的整体用户体验,你只要下载好原生应用,它便会立即加载。即使没有网络连接,也并非是完全不可用的: 你的设备上已经存储了供用户使用的绝大部分资源。原生应用具备提供有弹性、吸引人的体验的能力,同时也意味它的数量已经呈爆炸式增长。目前在苹果和 Google 的应用商店中,已经有超过400万的原生应用!
从历史上来说,Web 无法提供原生应用所具备的这些强大功能,比如离线能力,瞬时加载和更高的可靠性。这也正是 PWA 成为 Web 颠覆者的契机。主要的浏览器厂商一直在努力改进构建 Web 的方式, 并创建了一组新功能以使 Web 开发者能够创建快速、可靠和吸引人的网站。PWA 应该具备以下特点:
- 响应式
- 独立于网络连接
- 类似原生应用的交互体验
- 始终保持更新
- 安全
- 可发现
- 可重连
- 可安装
- 可链接
作为 Web 开发者,这是我们传统构建网站方式的一种转变。这意味着我们可以开始构建可以应对不断变化的网络条件或无网络连接的网站。这还意味着我们可以建立更吸引人的网站来为我们的用户提供一流的浏览体验。
读到这,你可能会想,这太疯狂了!那些不支持这些新功能的老浏览器怎么办? 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所示的样子。
图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)
图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。
图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 生命周期,它会在用户访问该网站的博客页面时发生。
图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 便已准备就绪,随时可以使用了!
图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 仓库中托管基础网站,而无需后端。
图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)
图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 就是一个很好的现成例子。我们来看看下面的图片,以获得更好的理解。
图2.2 Google 的 Inbox 利用 Service Workers 来缓存 UI 外壳。
你可能对 Google 的 Inbox 已经很熟悉了。(参见图2.2) 它是一个便利的 Web 应用,它允许你组织和发送邮件。在底层它使用 Service Workers 来缓存,并为用户提供超级快的体验。如你在上图中所见,当你访问该网站的时候,首先它的 UI 外壳会立即呈现在你眼前。这是非常棒的,因为用户获得了即时反馈,这会让他们感到网站速度非常快,即使我们仍在等待其余部分的动态内容加载。该应用给用户一种感观上的速度快,即使它用来获取内容的时间并不比之前短。用户还会注意到 “loading” 指示符,它表示网站正在发生一些事情,此刻正忙。这比等待一个空白页面加载很久好多了!
一旦外壳加载完,网站的动态内容就会使用 JavaScript 来获取并加载。
图2.3 一旦 UI 外壳加载完,就可以获取网站的动态内容,然后添加到页面的剩余部分。
上面的图2.3展示了 Google 的 Inbox 网站在动态内容加载完后就会将其填充到 Web 应用之中。使用同样的技术,你可以为网站的重复访问提供瞬时加载,还可以缓存应用的 UI 外壳,这样它就能离线工作。这样即使用户当前没有连接,他们也可以看到应用的 UI 外壳。
在第3章中,你将学习如何利用 Service Workers 来缓存你的内容并且为你的用户提供离线体验。在本书中,我们会构建一个 PWA,它会使用应用外壳架构,你可以下载并遵循此代码,然后使用此方法来构建你自己的应用。
性能优势
使用了应用外壳架构的 Web 应用“瞬时”加载说起来很容易,但这对用户到底意味着什么呢?多快才是“瞬时”?为了更好的从视觉上感受出使用应用外壳架构的 PWA 加载有多块,我使用了叫做 webpagetest.org 的工具来生成下面的幻灯片。(参见图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。
图2.5 在 Service Worker 安装过程中,我们可以获取资源并为下次访问做好预缓存。
当用户首次访问网站时,Service Worker 会开始下载并安装自身。在安装阶段,我们可以进入这个事件并准备缓存 UI 外壳所需的所有资源。也就是说,基础的 HTML 页面和任何可能需要的 CSS 或 JavaScript 。
此时,我们可以立即提供网站的“外壳”,因为它已经添加到 Service Worker 缓存之中。这些资源的 HTTP 请求再也不需要转到服务器了。一旦用户导航到另一个页面,他们将立即看到外壳。(参见图2.6)
图2.6 对于发起的任意 HTTP 请求,我们可以检查资源是否存在于缓存之中,如果不存在的话,我们再通过网络来获取它们。
动态内容被加载到网站中,网站会正常运行。因为你能够进入这些请求的 fetch 事件,所以你可以决定是否要缓存它们。你可能有经常更新的动态内容,因此缓存它们没有意义。但是,你的用户仍然会得到更快、更好的浏览体验。在下章中,我们会深入 Service Worker 缓存,在那里你将了解到更多关于这个强大功能的具体内容。
-
2.3 逐步剖析现有的 PWA
尽管 PWA 还是个比较新的概念,但已有一些了不起的 PWA 已经在网络上供每天数百万用户使用。
在下章中,我们会深入代码并向你展示如何开始构建自己的 PWA 。在我们更进一步之前,为了更好的理解 PWA 的这些功能是如何工作的,剖析现有的 PWA 还是有必要的。
在本节中,我们来看一个我个人非常喜欢的 PWA 。Twitter 的手机网站就是 PWA,它为使用移动设备的用户提供了增强体验。如果你使用 Twitter,那这是在旅途中查看推文的好方法。(参见图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 安装阶段被缓存。
图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 会为用户展现一个自定义的离线页面。
图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 客户端,它可以使用资源的当前副本,直到指定的“过期时间”。反过来,浏览器可以缓存此资源,并且只有在有效期满后才会再次检查新版本。
图3.1 当浏览器发起一个资源的 HTTP 请求时, 服务器会发送的 HTTP 响应会包含该资源相关的有用信息
在上图中,你可以看到当浏览器发起一个资源的 HTTP 请求时,服务器返回的资源还附带一些 HTTP 首部。这些首部包含有用的信息,浏览器可以通过它们来了解资源相关的更多信息。HTTP 响应告诉浏览器这个资源是什么类型的,要缓存多长时间,它是否压缩过,等等。
HTTP 缓存是提高网站性能的绝佳方式,但它也有自身的缺陷。使用 HTTP 缓存意味着你要依赖服务器来告诉你何时缓存资源和何时过期。如果你的内容具有相关性,任何更新都可能导致服务器发送的到期日期很容易变得不同步,并影响你的网站。
能力越大,责任越大, 对 HTTP 缓存来说,真是再正确不过了。当我们对 HTML 进行重大更改时,我们也可能会更改 CSS 以及对应的新的 HTML 结构,并更新任何 JavaScript 以适应样式和内容的更改。如果你曾经在发布更改后的网站时没有得到正确的 HTTP 缓存的话,我相信你会明白这是由于错误的缓存资源所导致的网站被破坏。
下面的图是我的个人博客在文件被错误缓存情况下的样子。
图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 应用所需的所有重要资源。
图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中的内容。
图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中的内容。
图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 缓存中取出。
图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 响应,确保服务器返回的是成功响应并且没有任何问题。我们绝不想缓存一个错误的结果!
如果成功响应,我们会再次克隆响应。你可能会疑惑我们为什么需要再次克隆响应,请记住响应是一个流,它只能消耗一次。因为我们想要浏览器和缓存都能够消耗响应,所以我们需要克隆它,这样就有了两个流。
最后,代码中使用这个响应并将其添加至缓存中,以便下次再使用它。如果用户刷新页面或访问网站另一个请求了这些资源的页面,它会立即从缓存中获取资源,而不再是通过网络。
图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 应用使用应用外壳架构来动态地获取每篇文章的内容并将数据填充到页面上。
图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 这个工具。
图3.9 WebPagetest.org 是一个免费工具,可以使用来自世界各地的真实设备对你的网站进行测试
Webpagetest.org 是一个很棒的工具。只需简单地输入网站 URL,它就可以让你使用真实设备和各种类型的浏览器从世界任何地点来分析你的网站。测试运行在真实设备上,并为你提供关于网站有用的分类和性能分析。最棒的是,它是开源并且完全免费使用的。
如果我通过 Webpagetest.org 运行我们的示例应用,它会生成类似下面图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,内置的处理方法能够涵盖最常见的网络策略。只需短短几行代码,你就可以决定是否只是要从缓存中提供指定资源,或者从缓存中提供资源并提供备用方案,或者只能从网络返回资源并且永不缓存。这个库可以让你完全控制缓存策略。
图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 时所扮演的角色。再来仔细看一遍下面的图。
图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 立即开始工作,而无需等待用户跳转或刷新页面。
图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%。这是一个相当不错的节省,最棒的是,选择这种格式图像质量不会受到影响。
图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,包含来自世界各地的有趣新闻。
图4.4 Progressive Times 示例应用是贯穿本书的基础应用
在 Progressive Times 应用中,我们使用网络字体来提升应用的外观感受。
这些字体是从第三方服务下载的,大约30KB左右。虽然网络字体真的能增强网页的外观感觉,但如果用户只是想节省数据和金钱,那么网页字体似乎是不必要的。无论用户的网络连接情况如何,你的 PWA 都没有理由不去适应用户。
无论你是使用台式机还是移动设备,启用此功能都是相当简单的。如果是在移动设备上,你可以在菜单的设置里开启。
图4.5 可以在移动设备或手机上开启节省数据功能。注意红色标注的区域。
一旦设置启用后,每个发送到服务器的 HTTP 请求都会包含 Save-Data 首部。如果使用开发者工具查看,它看起来就如下图所示。
图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 事件并决定是否返回网站的“轻量级”版本