2015年,国际电信联盟预估到15年年底全球上网人口将到达32亿,也就是说全球将近一半的人口都在上网。想象一下每秒钟的上网人数,32亿。大约32000个足球场才装得下这么多人!这几乎是一个大到无法理解的数字。当这些人上网时,他们使用的设备不尽相同、他们的网速也各不相同、甚至同一个的网速也会变化。作为Web开发者,试图满足所有这些不同的场景似乎令人望而生畏!这正是PWA出现的契机。它们赋予了开发者可以构建速度更快、富有弹性并且更吸引人的网站的能力,这些网站能够被全球数十亿人访问。在本书的第1部分中,我们将直接深入地定义到底什么才是PWA。
在第1章中,你将了解渐进式网络应用(ProgressiveWebApps),即我们所熟知的PWA,以及PWA带来的好处。然后,我们一起来看看已经利用PWA的能力来改善用户浏览体验的企业。我们还会详细分析一个真实世界中PWA,并了解下像Twitter和Flipkart这样的公司是如何建立自己的PWA的。PWA的关键组成部分是ServiceWorker,我们将深入介绍此主题,以及了解它在Web浏览器中加载时所经历的生命周期。
在第2章中,首先介绍了构建PWA时可以使用的不同架构方法,以及如何最佳地组织你的代码。我们将研究两种不同的方法,“汲取功能”或“应用外壳架构”-这两种方法都可以满足项目的需要。PWA最棒的一点就是你无需重写已存在的Web应用便能开始使用PWA的功能,只要你觉得这些功能会使用户受益并提升他们的体验,就可以添加它们。最后,本章会以剖析一个现有的PWA来结尾,该PWA是Twitter团队开发的TwitterLite(精简版Twitter)。
在第1部分结束之际,你应该对PWA是什么,以及它们能带给用户的好处有一个清晰的认知。第1部分将为本书的下一部分奠定基础,在下一部分中我们将直接进入编码环节,从头开始构建一个PWA。
在我们开始探索为什么PWA对于当今Web世界是个重大飞跃之前,值得回忆下自Web问世以来的历程。这要追溯到1990年的圣诞节,TimBerners-Lee爵士和他在CERN的团队创建了工作网络所需的所有工具,他们创建了HTTP、HTML和WorldWideWeb(全世界第一个网页浏览器)。WorldWideWeb只能运行由超链接的简单纯文本组成的网页。事实上,这些第一代的网页仍然在线,并且可以浏览!
回到现在,我们所浏览的网页与最初的网页并没有太大的不同。当然,现在我们有了像CSS和Javacript这样的功能,但网页的核心依旧是使用HTML、HTTP以及一些其他构建模块来构建的,这些都是TimBerners-Lee及他的团队在多年前所创建的。这些辉煌的构建模块意味着Web已经能够以惊人的速度增长。然而,我们用来访问网页的设备数量也在不断增长。无论你的用户是在旅途中还是坐在书桌前,他们都无时无刻不在获取信息。我们对于Web的期望从未如此之高。
从历史上来说,原生应用(下载到手机的)已经能够提供更好的整体用户体验,你只要下载好原生应用,它便会立即加载。即使没有网络连接,也并非是完全不可用的:你的设备上已经存储了供用户使用的绝大部分资源。原生应用具备提供有弹性、吸引人的体验的能力,同时也意味它的数量已经呈爆炸式增长。目前在苹果和Google的应用商店中,已经有超过400万的原生应用!
从历史上来说,Web无法提供原生应用所具备的这些强大功能,比如离线能力,瞬时加载和更高的可靠性。这也正是PWA成为Web颠覆者的契机。主要的浏览器厂商一直在努力改进构建Web的方式,并创建了一组新功能以使Web开发者能够创建快速、可靠和吸引人的网站。PWA应该具备以下特点:
作为Web开发者,这是我们传统构建网站方式的一种转变。这意味着我们可以开始构建可以应对不断变化的网络条件或无网络连接的网站。这还意味着我们可以建立更吸引人的网站来为我们的用户提供一流的浏览体验。
读到这,你可能会想,这太疯狂了!那些不支持这些新功能的老浏览器怎么办?PWA最棒的一点就是它们真的是“渐进式”的。如果你构建一个PWA,即使在一个不支持的老旧浏览器上运行,它仍然可以作为一个普通的网站来运行。驱动PWA的技术就是这样设计的,只有在支持这些新功能的浏览器中才会增强体验。如果用户的设备支持,那么他们将获得所有额外的好处和更多的改进功能。无论怎样,这对你和你的用户来说都是双赢!
那么PWA到底是由什么组成的呢?我们一直将它们作为一组功能和原理来讨论,但真正使某个网站成为“PWA”的到底是什么呢?最最简单的PWA其实只是普通的网站。它们是由我们这些Web开发者所熟悉和喜欢的技术所创建的,即HTML、CSS和JavaScript。然而,PWA却更进一步,它为用户提供了增强的体验。我非常喜欢GoogleChrome团队的开发人员AlexRussell的描述方式:
“这些应用没有通过应用商店进行打包和部署,它们只是汲取了所需要的原生功能的网站而已。”
PWA使用了叫做ServiceWorkers的重要新功能,它可以令你深入网络请求并构建更好的Web体验。随着本章的深入,我们将进一步了解它们以及它们带给浏览器的改进。PWA还允许你将其“添加”到设备的主屏幕上。它会像原生应用那样,通过点击图标便可让你轻松访问一个Web应用。(我们将在第5章中深入讨论)
PWA还可以离线工作。使用ServiceWorkers,你可以选择性地缓存部分网站以提供离线体验。如果你现在在没有网络连接的情况下浏览网站,那么对于绝大多数网站,你看到的应该是类似于下面图1.1所示的样子。
图1.1作为用户,离线页面可能会非常令人沮丧,尤其是迫切需要获取这些信息时!
有了ServiceWorkers,我们的用户无需再面对恐怖的“无网络连接”屏幕了。使用ServiceWorkers,你可以拦截并缓存任何来自你网站的网络请求。无论你是为移动设备,桌面设备还是平板设备构建网站,都可以在有网络连接或没有网络连接的情况下控制如何响应请求。(我们将在第3章中深入了解缓存,并在第8章中构建一个离线网页。)
简而言之,PWA不仅仅是一组非常棒的新功能,它们实际上是我们构建更好的网站的一种方式。PWA正在迅速成为一套最佳实践。构建PWA所采取的步骤将有利于访问你网站的任何人,无论他们选择使用何种设备。
一旦你解锁了开始构建PWA所需的基本构建块,你会很快发现,比较高级的例子并没有看上去那么高级。对于不知情的外行人来说,这本书可能看起来无足轻重,但是一旦你进入构建PWA的节奏后,你会发现一切都是如此的简单!
作为一名开发者,我当然知道当一项新技术或一系列功能出现时,是有多么的令人兴奋。但为你的网站发掘并引进最新最好的库或框架的强烈欲望往往会掩盖其为企业带来的价值。无论你是否相信,PWA能实际上为我们的用户带来真正的价值,并使网站更具吸引力,更有弹性,甚至更快。
PWA最棒的一点是可以一步步地来增强现有的Web应用。我们在本书中学习的技术集合可以应用于任何现有的网站,甚至是你正在构建的新的Web应用。无论你选择何种技术栈来开发网站,PWA都将与你的解决方案紧密结合在一起,因为它只是简单地基于HTML、CSS和JavaScript。这简直太棒了!
现在你对PWA已经有了基本的了解,让我们先暂时停下脚步,想象一下用PWA来构建的各种可能性。假设你的在线业务是报纸,人们通过它来了解更多关于当地的新闻。如果你知道有人经常访问你的网站并浏览多个页面,为什么不提前缓存这些页面,这样他们就可以完全离线地浏览新闻?或者想象下,你的Web应用服务于一个慈善机构,志愿者们在这个网络连接不稳定或压根无网络连接的区域进行工作。PWA的功能将允许你构建一个离线应用,使他们在没有网络连接的现场也能收集信息。一旦他们回到办公室或有网络连接的区域,数据就可以同步到服务器。对于Web开发者来说,PWA是个彻底的颠覆者,并且我个人对它们将带给Web的功能感到兴奋不已。
在本章的前面,我提到了你可以将PWA“添加”到设备的主屏幕上。一旦添加后,它便会出现在你的主屏幕上并可以通过点击图标来访问你的网站。可以把它当做台式机的快捷方式,以使你轻松访问网站。
2015年,印度最大的电商网站Flipkart开始构建FlipkartLite,它是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应用来帮助你实现此目标。
正如我之前所提到的,释放PWA力量的关键在于ServiceWorkers。就其核心来说,ServiceWorkers只是后台运行的worker脚本。它们是用JavaScript编写的,只需短短几行代码,它们便可使开发者能够拦截网络请求,处理推送消息并执行许多其他任务。
最棒的一点是,如果用户的浏览器不支持ServiceWorkers的话,它们只是简单地回退,你的网站还作为普通的网站。正是由于这一点,它们被描述为“完美的渐进增强”。渐进增强术语是指你可以先创建能在任何地方运行的体验,然后为支持更高级功能的设备增强体验。
ServiceWorker是如何...工作的呢?那么为了尽可能地简单易懂,我真的很想解释下Google的JeffPosnick是如何描述他们的:
“将你的网络请求想象成飞机起飞。ServiceWorker是路由请求的空中交通管制员。它可以通过网络加载,或甚至通过缓存加载。”
作为“空中交通管制员”,ServiceWorkers可以让你全权控制网站发起的每一个请求,这为许多不同的使用场景开辟了可能性。空中交通管制员可能将飞机重定向到另一个机场,甚至延迟降落,ServiceWorker的行为方式也是如此,它可以重定向你的请求,甚至彻底停止。
虽然ServiceWorkers是用JavaScript编写的,但需要明白它们与你的标准JavaScript文件略有不同,这一点很重要。ServiceWorker:
你无需成为JavaSript专家后才可以开始尝试ServiceWorkers。它们是事件驱动的,你可以简单地选择想要进入的事件。当你对这些不同的事件有了基本的了解后,开始使用ServiceWorkers要比你想象中简单!
为了更好地解释ServiceWorkers,我们来看看下面的图1.3。
图1.3ServiceWorkers能够拦截进出的HTTP请求,从而完全控制你的网站。
ServiceWorker运行在worker上下文中,这意味着它无法访问DOM,它与应用的主要JavaScript运行在不同的线程上,所以它不会被阻塞。它们被设计成是完全异步的,因此你无法使用诸如同步XHR和localStorage之类的功能。在上面的图1.3中,你可以看到ServiceWorker处于不同的线程,并且可以拦截网络请求。记住,ServiceWorker就像是“空中交通管制员”,它可以让你全权控制网站中所有进出的网络请求。这种能力使它们极其强大,并允许你来决定如何响应请求。
简单点说,该网站不停地在接收包括图像甚至视频在内的内容请求。为了理解ServiceWorker生命周期是如何工作的,我们从网站每一天数百万次交互请求中挑选出一个。
图1.4展示了ServiceWorker生命周期,它会在用户访问该网站的博客页面时发生。
图1.4ServiceWorker生命周期
让我们慢慢来理解上面的图1.4,一步步地了解ServiceWorker生命周期是如何工作的。
当用户首次导航至URL时,服务器会返回响应的网页。在图1.4中,你可以看到在第1步中,当你调用register()函数时,ServiceWorker开始下载。在注册过程中,浏览器会下载、解析并执行ServiceWorker(第2步)。如果在此步骤中出现任何错误,register()返回的promise都会执行reject操作,并且ServiceWorker会被废弃。
一旦ServiceWorker成功执行了,install事件就会激活(第3步)。ServiceWorkers很棒的一点就是它们是基于事件的,这意味着你可以进入这些事件中的任意一个。我们将在本书的第3章中使用这些不同的事件来实现超快速缓存技术。
一旦安装这步完成,ServiceWorker便会激活(第4步)并控制在其范围内的一切。如果生命周期中的所有事件都成功了,ServiceWorker便已准备就绪,随时可以使用了!
图1.5ServiceWorker生命周期经历了不同阶段,这有点像交通灯系统
对我个人而言,我觉得记住ServiceWorker生命周期最简单的方法就是把它当成一组交通信号灯。在注册过程中,ServiceWorker处于红灯状态,因为它还需要下载和解析。接下来,它处于黄灯状态,因为它正在执行,还没有完全准备好。如果上述所有步骤都成功了,你的ServiceWorker在将处于绿灯状态,随时可以使用。
需要注意的是,当第一次加载页面时,ServiceWorker还没有激活,所以它不会处理任何请求。只有当它安装和激活后,才能控制在其范围内的一切。这意味着,只有你刷新页面或者导航到另一个页面,ServiceWorker内的逻辑才会启动。
我很肯定,到目前为止你一直迫切地想看看代码应该是怎么样的,所以我们开始吧。
因为ServiceWorker只是运行在后台线程的JavaScript文件,所以在HTML页面中你可以像引用任何JavaScript文件一样来引用它。假设我们创建了一个ServiceWorker文件,并将其命名为sw.js。要注册它,需要在HTML页面中使用如下代码。(参见代码清单1.1)
Thebestwebpageever在script标签内,我们首先检查浏览器实际上是否支持ServiceWorkers。如果支持,我们就使用navigator.serviceWorker.register('/sw.js')函数注册,该函数又会通知浏览器下载ServiceWorker文件。如果注册成功,它会开始ServiceWorker生命周期的剩余阶段。navigator.serviceWorker.register()函数返回promise,如果注册成功的话,我们可以决定如何继续进行。
之前我提到过ServiceWorkers是事件驱动的,而且ServiceWorkers最强大的功能之一就是允许你通过进入fetch事件来监听任何网络请求。当一个资源发起fetch事件时,你可以决定如何继续进行。你可以将发出的HTTP请求或接收的HTTP响应更改成任何内容。这相当简单,但同时却非常强大!
假设你的ServiceWorker文件中的代码片段如下。(参见代码清单1.2)
self.addEventListener('fetch',function(event){if(/\.jpg$/.test(event.request.url)){event.respondWith(fetch('/images/unicorn.jpg'));}});在上面的代码中,我们监听了fetch事件,如果HTTP请求的是JPEG文件,我们就拦截请求并强制返回一张独角兽图片,而不是原始URL请求的图片。上面的代码会为该网站上的每个JPEG图片请求执行同样的操作。虽然独角兽的图片棒极了,但是你可能不想在现实世界的网站上这样做,因为你的用户可能不满意这样的结果!上面的代码示例可以让你了解ServiceWorkers的能力。只是短短几行代码,我们在浏览器中创建了一个强大的代理。
为了让ServiceWorker能在网站上运行,需要通过HTTPS来提供服务。虽然这让使用它们变得有些困难,但这样做有一个很重要的原因。还记得将ServiceWorker比喻成空中交通管制员吗?能力越大,责任越大,对于ServiceWorker而言,它们实际上也可能用于恶意的用途。如果有人能够在你的网页上注册一个狡诈的ServiceWorker,他们将能够劫持连接并将其重定向到恶意端点。事实上,坏蛋们可能会利用你的HTTP请求做任何他们想要的事情!为了避免这种情况发送,ServiceWorker只能在通过HTTPS提供服务的网页上注册。这确保了网页在通过网络的过程中没有被篡改。
如果你是一个想要构建PWA的网站开发者,读到这你可能会有点沮丧,千万别!传统上,为你的网站获取SSL证书可能会花费了相当多的钱,但不管你是否相信,实际上现在有许多免费的解决方案可以供Web开发者使用。
首先,如果你想要在自己的电脑上测试ServiceWorkers,你可以通过localhost提供的页面来执行此操作。它们已经创建了这个功能,以使开发者在部署应用之前轻松进行本地调试。
如果你像我一样,使用GitHub进行源代码控制的话,那么你可以使用GitHubPages来测试ServiceWorker。(参见图1.6)通过GitHubPages,你可以直接在你的GitHub仓库中托管基础网站,而无需后端。
图1.6GitHubPages允许你通过SSL直接在GitHub仓库中托管网站。
使用GitHubPages的优点是,默认情况下,你的网页是通过HTTPS来提供的。当我第一次开始试用ServiceWorkers时,GitHubPages允许我快速地启动一个网站,并随时验证我的想法。
在本章的前面,我们介绍过一个名为Flipkart的电子商务公司的案例,Flipkart决定将它的网站构建成PWA。Flipkart是印度最大的电子商务网站,一个快速,有吸引力的网站对于他们业务的成功至关重要。还值得注意的是,在像印度这样的新兴市场,移动数据包的成本可能相当高,并且移动网络可能是不稳定的。出于这些原因,许多新兴市场中的电子商务公司都需要构建轻便、精简的网页,以满足任何网络上的用户需求。
2015年,Flipkart采用了仅使用原生应用的策略,并决定暂时关闭它的移动网站。后来公司发现,在原生应用中提供快速和有吸引力的用户体验变得愈发困难。所以Flipkart决定重新思考它们的开发方式。通过引入可立即运行的移动Web应用,离线工作和重新吸引用户的功能,使其开发人员又回到移动Web开发工作之中,这些引入的功能都是PWA所提供的。
当他们实现了自己的新的PWA后,便看到了立竿见影的效果。不仅网站几乎是瞬时加载的,而且当他们离线时还能够继续浏览分类页面,查看以前的搜索结果和产品页面。数据使用量是Flipkart的关键指标,最重要的是将FlipkartLite与原生应用进行比较时,FlipkartLite的数据量减少了3倍。
在上面的代码清单2.1中,ServiceWorkerToolbox寻找URL匹配'/emoji/v2/svg/'并且来自*.twimg.com站点的任何请求。一旦它拦截了匹配此路由的任意HTTP请求,它会将它们存储在名为'twemoji'的缓存之中。等下次用户再次发起匹配此路由的请求时,呈现给用户的将是缓存的结果。
这段代码是非常强大的,它赋予我们这些开发者一种能力,使我们可以精准控制如何以及何时在网站上缓存资源。如果起初这段代码会让你有些困惑,也不要担心。在下章中,我们会深入ServiceWorker缓存并使用这个强大功能来构建页面。
我每天上下班的途中都是在火车上度过的。很幸运,旅途不算太长,但不幸的是在某些区域网络信号很弱,甚至是掉线。这意味着如果我正在手机上浏览网页,有时我可能会失去连接,或者连接相当不稳定。这是相当令人沮丧的!
幸运的是,ServiceWorker缓存是个强大的功能,它实际上是将网站资源保存到用户的设备上。这意味着使用ServiceWorkers就可以让你拦截任何HTTP请求并直接用设备上缓存的资源进行响应。你甚至不需要访问网络就可以获取缓存的资源。
考虑到这一点,我们可以使用这些功能来构建离线页面。使用ServiceWorker缓存,你可以缓存个别的资源,甚至是整个网页,这完全取决于你。如果用户没有网络连接,TwitterLite会为用户展现一个自定义的离线页面。
图2.9如果用户没有网络连接,TwitterPWA会为用户显示一个自定义的错误页面。
用户现在看到的是一个有帮助的自定义离线页面,而不是可怕的错误:“无法访问此网站”(参见图2.9)。他们还可以通过点击提供的按钮来检查连接是否恢复。对于用户来说,这种Web体验更好。在第8章中,你将掌握开始构建自己的离线页面所需的必要技能,并为你的用户提供富有弹性的浏览体验。
TwitterLite很快,而且针对小屏幕进行了优化,还能离线工作。还有什么?好吧,它需要如同原生应用一样的外观感受!如果你仔细查看过Web应用主页的HTML的话,可能会注意到下面这行代码:
它带来了一些好处。首先,它使浏览器能够将Web应用安装到设备的主屏幕,以便为用户提供更快捷的访问和更丰富的体验。其次,通过在清单文件中设置品牌颜色,你可以自定义浏览器自动显示的启动画面。它还允许你自定义浏览器的地址栏以匹配你的品牌颜色。
使用清单文件真正地使Web应用的外观感觉更加完美,并为你的用户提供了更丰富的体验。TwitterLite使用清单文件以利用浏览器中的许多内置功能。
在第5章中,我们会探索如何使用清单文件来增强PWA的外观感受,并为用户提供有吸引力的浏览体验。
TwitterLite是一个全面的例子,它很好地诠释了PWA应该是怎样的。它涵盖了贯穿本书的绝大部分功能,这些功能都是为了构建一个快速、有吸引力和可靠的Web应用。
在第1章中,我们讨论过了Web应用应该具备的所有功能。让我们来回顾下到目前为止TwitterPWA的细节。该应用是:
哇!真是个大清单,但幸运的是我们所得到的这些收益不少都是构建PWA的附属品。
在上面的清单3.1中,你可以看到一个引用了图片和JavaScript文件的简单网页。该网页没有任何华丽的地方,但我们会用它来学习如何使用ServiceWorker缓存来缓存资源。上面的代码会检查你的浏览器是否支持ServiceWorker,如果支持,它会尝试去注册一个叫做service-worker.js的文件。
好了,我们已经准备好了基础页面,下一步我们需要创建缓存资源的代码。清单3.2中的代码会进入叫做service-worker.js的ServiceWorker文件。
清单3.2中的代码进入了install事件,并在此阶段将JavaScript文件和hello图片添加到缓存中。在上面的清单中,我还引用了一个叫做cacheName的变量。这是一个字符串,我用它来设置缓存的名称。你可以为每个缓存取不同的名称,甚至可以拥有一个缓存的多个不同的副本,因为每个新的字符串使其唯一。当看到本章后面的版本控制和缓存清除时,你将会感受到它所带来的便利。
如果所有的文件都成功缓存了,那么ServiceWorker便会安装完成。如果任何文件下载失败了,那么安装过程也会随之失败。这点非常重要,因为它意味着你需要依赖的所有资源都存在于服务器中,并且你需要注意决定在安装步骤中缓存的文件列表。定义一个很长的文件列表便会增加缓存失败的几率,多一个文件便多一份风险,从而导致你的ServicerWorker无法安装。
现在我们的缓存已经准备好了,我们能够开始从中读取资源。我们需要在清单3.3中添加代码,让ServiceWorker开始监听fetch事件。
self.addEventListener('fetch',function(event){event.respondWith(caches.match(event.request).then(function(response){if(response){returnresponse;}returnfetch(event.request);}));});清单3.3中的代码是我们ServiceWorker杰作的最后一部分。我们首先为fetch事件添加一个事件监听器。接下来,我们使用caches.match()函数来检查传入的请求URL是否匹配当前缓存中存在的任何内容。如果存在的话,我们就简单地返回缓存的资源。但是,如果资源并不存在于缓存当中,我们就如往常一样继续,通过网络来获取资源。
如果你打开一个支持ServiceWorkers的浏览器并导航至最新创建的页面,你应该会注意到类似于下图3.4中的内容。
图3.4示例代码生成了带有图片和JavaScript文件的基础网页
请求的资源现在应该是可以在ServiceWorker缓存中获取的。当我刷新页面时,ServiceWorker会拦截HTTP请求并从缓存中立即加载合适的资源,而不是发起网络请求到服务器端。ServiceWorker中只需短短几行代码,你便拥有了一个直接从缓存加载的网站,并能立即响应重复访问!
一些现代浏览器可以使用浏览器内置的开发者工具来查看ServiceWorker缓存中的内容。例如,如果你打开GoogleChrome的开发者工具并切换至“Application”标签页,你能够看到类似于下图3.5中的内容。
图3.5当你想看缓存中存储什么时,GoogleChrome的开发者工具会非常有用
图3.5展示了名称为helloWorld的缓存项中存储了scripts.js和hello.png两个文件。现在资源已经存储在缓存中,今后这些资源的任何请求都会从缓存中立即取出。
在清单3.2中,我们看过了如何在ServiceWorker安装期间缓存任何重要的资源,这被称之为“预缓存”。当你确切地知道你要缓存的资源时,这个示例能很好地工作,但是资源可能是动态的,或者你可能对资源完全不了解呢?例如,你的网站可能是一个体育新闻网站,它需要在比赛期间不断更新,在ServiceWorker安装期间你是不会知道这些文件的。
因为ServiceWorkers能够拦截HTTP请求,对于我们来说,这是发起请求然后将响应存储在缓存中的绝佳机会。这意味着我们改为先请求资源,然后立即缓存起来。这样一来,对于同样资源的发起的下一次HTTP请求,我们可以立即将其从ServiceWorker缓存中取出。
图3.6对于发起的任何HTTP请求,我们可以检查资源是否在缓存中已经存在,如果没有的话再通过网络来获取
我们来更新下之前清单3.1中的代码。
现在页面已经完成,我们准备开始为ServiceWorker文件添加一些代码。下面的清单3.5展示了我们要使用的代码。
varcacheName='helloWorld';self.addEventListener('fetch',function(event){event.respondWith(caches.match(event.request).then(function(response){if(response){returnresponse;}varrequestToCache=event.request.clone();returnfetch(requestToCache).then(function(response){if(!response||response.status!==200){returnresponse;}varresponseToCache=response.clone();caches.open(cacheName).then(function(cache){cache.put(requestToCache,responseToCache);});returnresponse;});}));});清单3.5中的代码看上去好多啊!我们分解来看,并解释每个部分。代码先通过添加事件监听器来进入fetch事件。我们首先要做的就是检查请求的资源是否存在于缓存之中。如果存在,我们可以就此返回缓存并不再继续执行代码。
然而,如果请求的资源于缓存之中没有的话,我们就按原计划发起网络请求。在代码更进一步之前,我们需要克隆请求。需要这么做是因为请求是一个流,它只能消耗一次。因为我们已经通过缓存消耗了一次,然后发起HTTP请求还要再消耗一次,所以我们需要在此时克隆请求。然后,我们需要检查HTTP响应,确保服务器返回的是成功响应并且没有任何问题。我们绝不想缓存一个错误的结果!
如果成功响应,我们会再次克隆响应。你可能会疑惑我们为什么需要再次克隆响应,请记住响应是一个流,它只能消耗一次。因为我们想要浏览器和缓存都能够消耗响应,所以我们需要克隆它,这样就有了两个流。
最后,代码中使用这个响应并将其添加至缓存中,以便下次再使用它。如果用户刷新页面或访问网站另一个请求了这些资源的页面,它会立即从缓存中获取资源,而不再是通过网络。
图3.7使用GoogleChrome的开发者工具,我们可以看到网络字体已经通过网络获取并添加至缓存中,以确保重复请求时速度更快
如果你仔细看下上面的图3.7,你会注意到页面上三个资源的缓存中有新项。在上面的代码示例中,每次返回成功的HTTP响应时,我们都能够动态地向缓存中添加资源。对于你可能想要缓存资源,但不太确定它们可能更改的频率或确切地来自哪里,那么这种技术可能是完美的。
ServiceWorkers赋予作为开发者的你通过代码进行完全的控制,并允许你轻松构建适合需求的自定义缓存解决方案。事实上,使用我们前面提到的两种缓存技术可以组合起来,以使加载速度更快。完全由你来掌控这一切。
例如,假设你正在构建一个使用应用外壳架构的新Web应用。你可能会想要使用我们在清单3.2中的代码来预缓存外壳。对于之后任何的HTTP请求都会使用拦截并缓存的技术进行缓存。或者你也许只想缓存现有网站中已知的、不会经常更改的部分。通过简单地拦截并缓存这些资源,就能为用户提供更好的性能,却只需短短几行代码。取决于你的情况,ServiceWorker缓存可以适用你的需求,并立即使用户得到的体验所有提升。
到目前为止,我们已经运行的代码示例都是有帮助的,但是单独思考它们并不太容易。在第1章中,我们讨论了可以用ServiceWorkers来构建超棒Web应用的多种不同方式。报纸Web应用便是其中一个,我们可以在现实世界中使用所学到的关于ServiceWorkers缓存的一切知识。我将我们的示例应用命名为'ProgressiveTimes'。该Web应用是一个新闻网站,人们会定期访问并阅读多个页面,所以提前保存未来的页面是有意义的,以便它们能够立即加载。我们甚至可以保存网站本身,以便用户可以离线浏览。
图3.8ProgressiveTimes示例应用使用应用外壳架构
让我们把到目前为止在本章在所学的整合起来,并为ProgressiveTimes应用添加ServiceWorker,它将预处理重要资源,并缓存任何其他请求。
在清单3.8中,先使用importScripts函数导入ServiceWorkertoolbox库。ServiceWorkers可以访问一个叫做importScripts()的全局函数,它可以将同一域名下脚本导入至它们的作用域。这是将另一个脚本加载到现有脚本中的一种非常方便的方法。它保持代码整洁,也意味着你只需要在需要时加载文件。
一旦脚本导入后,我们就可以开始定义想要缓存的路由。在上面的清单中,我们定义了一个路由,它匹配路径是/css/,并且永远使用缓存优选的方式。这意味着资源会永远从缓存中提供,如果不存在的话再回退成通过网络获取。Toolbox还提供了一些其他内置缓存策略,比如只通过缓存获取、只通过网络获取、网络优先、缓存优先或者尝试从缓存或网络中找到最快的响应。每种策略都可以应用于不同的场景,甚至你可以混用不同的路由来匹配不同的策略,以达到最佳效果。
ServiceWorkertoolbox还为你提供了预缓存资源的功能。在清单3.2中,我们在ServiceWorker安装期间预缓存了资源,我们可以使用ServiceWorkertoolbox以同样的方式来实现,并且只需要一行代码。
toolbox.precache(['/js/script.js','/images/hello.png']);清单3.9中的代码接收在ServiceWorker安装步骤中应该被缓存的URL数组。这行代码会确保在ServiceWorker安装阶段资源被缓存。
每当我接手一个新项目时,毫无疑问我喜欢使用的库就是ServiceWorkertoolbox。它简化了你的代码并为你提供经过验证的缓存策略,只需几行代码便可实现。事实上,我们在第2章剖析过的TwitterPWA也使用了ServiceWorkertoolbox来使得代码更容易理解,并依赖于这些经过验证的缓存方法。
清单4.2中的代码是FetchAPI实际应用的基础示例。你可能还注意到了没有回调函数和事件,取而代之的是then()方法。这个方法是ES6中新功能Promises的一部分,目的是使我们的代码更具可读性、更便于开发者理解。Promise代表异步操作的最终结果,我们不知道实际的结果值是什么,直到将来某个时刻操作完成。
上面的代码看起来很容易理解,但是使用FetchAPI发起POST请求呢?
fetch('/some/url',{method:'POST',headers:{'auth':'1234'},body:JSON.stringify({name:'dean',login:'dean123',})}).then(function(data){console.log('Requestsuccess:',data);}).catch(function(error){console.log('Requestfailure:',error);});假如说,你想要使用POST请求将某个用户详情发送到服务器。在上面的清单中,只需在fetch选项中将method更改为POST并添加body参数。使用Promises不仅可以使代码更整洁,而且还可以使用链式代码,以便在fetch请求之间共享逻辑。
在上面的代码中,我们监听了fetch事件,如果HTTP请求的是JPEG文件,我们就拦截请求并强制返回一张独角兽图片,而不是原始URL请求的图片。上面的代码会为该网站上的每个JPEG图片请求执行同样的操作。对于任何其他文件类型,它会直接忽略并继续执行。
同时清单4.4中的代码是个有趣的示例,它并没有展示出ServiceWorkers的真正能力。我们会在这个示例的基础上更进一步,返回我们自定义的HTTP响应。
self.addEventListener('fetch',function(event){if(/\.jpg$/.test(event.request.url)){event.respondWith(newResponse('
Thisisaresponsethatcomesfromyourserviceworker!
',{headers:{'Content-Type':'text/html'}}););}});在清单4.5中,代码通过监听fetch事件的触发来拦截任何HTTP请求。接下来,它判断传入请求是否是JPEG文件,如果是的话,就使用自定义HTTP响应进行响应。使用ServiceWorkers,你能够创建自定义HTTP响应,包括编辑响应首部。此功能使得ServiceWorkers极其强大,同时也可以理解为什么它们需要通过HTTPS请求才能获取。想象一下,如果不是这样的话,黑客动动手指便可以完成一些恶意的操作!就在本书开头的第1章中,你了解过了ServiceWorker生命周期以及在它构建PWA时所扮演的角色。再来仔细看一遍下面的图。
图4.1ServiceWorker生命周期
看过上面的图,你会想到当用户第一次访问网站的时候,并不会有激活的ServiceWorker来控制页面。只有当ServiceWorker安装完成并且用户刷新了页面或跳转至网站的其他页面,ServiceWorker才会激活并开始拦截请求。
为了更清楚地解释这个问题,我们想象一下,一个单页应用(SPA)或一个加载完成后使用AJAX进行交互的网页。当注册和安装ServiceWorker时,我们将使用到目前为止在本书中所介绍的方法,页面加载后发生的任何HTTP请求都将被忽略。只有当用户刷新页面,ServiceWorker才会激活并开始拦截请求。这并不理想,你希望ServiceWorker能尽快开始工作,并包括在ServiceWorker未激活期间所发起的这些请求。
如果你想要ServiceWorker立即开始工作,而不是等待用户跳转至网站的其他页面或刷新本页面,有一个小技巧,你可以用它来立即激活你的ServiceWorker。
self.addEventListener('install',function(event){event.waitUntil(self.skipWaiting());});清单4.6中的代码重点在于ServiceWorker的install事件里。通过使用skipWaiting()函数,最终会触发activate事件,并告知ServiceWorker立即开始工作,而无需等待用户跳转或刷新页面。
图4.2self.skipWaiting()会使ServiceWorker解雇当前活动的worker并且一旦进入等待阶段就会激活自身
skipWaiting()函数强制等待中的ServiceWorker被激活。self.skipWaiting()函数还可以与self.clients.claim()一起使用,以确保底层ServiceWorker的更新立即生效。
下面清单4.7中的代码可以结合skipWaiting()函数,以确保ServiceWorker立即激活自身。
self.addEventListener('activate',function(event){event.waitUntil(self.clients.claim());});同时使用清单4.6和4.7中的代码,可以快速激活ServiceWorker。如果你的网站在页面加载后会发起复杂的AJAX请求,那么这些功能对你来说是完美的。如果你的网站主要是静态页面,而不是在页面加载后发起HTTP请求,那么你可能不需要使用这些功能。
上面清单中的代码很多,我们分解来看。最开始的几行,我添加了事件监听器来监听任何触发的fetch事件。对于每个发起的HTTP请求,我会检查当前请求是否是JEPG或PNG图片。如果我知道当前请求的是图片,我可以根据传递的HTTP首部来返回最适合的内容。在本例中,我检查每个首部并寻找image/webp的mime类型。一旦知道首部的值,我便能判断出浏览器是否支持WebP并返回相应的WebP图片。
同样内容的WebP图片只有87KB,相比于原始的JPEG图片,我们节省了59KB,大约是原始文件大小的37%。对于使用移动设备的用户,浏览整个网站会节省想当多的带宽。
我最近在出国旅行,当我迫切需要从航空公司的网站获取一些信息时,我使用的是2G连接,页面永远在加载中,最终我彻底放弃了。回国后,我还得向手机运营商支付日常服务费用,真是太让人不爽了!
在全球范围内,4G网络覆盖面正在迅速发展,但仍有很长的路要走。在2007年底,3G网络仅覆盖了孟加拉国、巴西、中国、印度、尼日利亚、巴基斯坦和俄罗斯等国家,将近全球人口的50%。虽然移动网络覆盖面越来越广,但在印度一个500MB的数据包需要花费相当于17个小时的最低工资,这听起来令人不可思议。
幸运的是,诸如GoogleChrome、Opera和Yandex这样的浏览器厂商已经意识到众多用户所面临的痛苦。使用这些浏览器的最新版本,用户将有一个选项,以允许他们“选择性加入”节省数据的功能。通过启动这项功能,浏览器会为每个HTTP请求添加一个新的首部。这是我们这些开发者的机会,寻找这个首部并返回相应的内容,为我们的用户节省数据。例如,如果用户开启了节省数据选项,你可以返回更轻量的图片、更小的视频,甚至是不同的标记。这是一个简单的概念,却行之有效!
这听上去是使用ServiceWorker的完美场景!在下节中,我们将编写代码,它会拦截请求,检查用户是否“选择性加入”节省数据并返回“轻量级”版本的PWA。
还记得我们在第3章中构建的PWA吗?它叫做ProgressiveTimes,包含来自世界各地的有趣新闻。
图4.4ProgressiveTimes示例应用是贯穿本书的基础应用
在ProgressiveTimes应用中,我们使用网络字体来提升应用的外观感受。
这些字体是从第三方服务下载的,大约30KB左右。虽然网络字体真的能增强网页的外观感觉,但如果用户只是想节省数据和金钱,那么网页字体似乎是不必要的。无论用户的网络连接情况如何,你的PWA都没有理由不去适应用户。
无论你是使用台式机还是移动设备,启用此功能都是相当简单的。如果是在移动设备上,你可以在菜单的设置里开启。
图4.5可以在移动设备或手机上开启节省数据功能。注意红色标注的区域。
一旦设置启用后,每个发送到服务器的HTTP请求都会包含Save-Data首部。如果使用开发者工具查看,它看起来就如下图所示。
图4.6启用了节省数据功能,每个HTTP请求都会包含Save-Data首部
一旦启用了节省数据功能,有几种不同的技术可以将数据返回给用户。因为每个HTTP请求都会发送到服务器,你可以根据来自服务器端代码中Save-Data首部来决定提供不同的内容。然而,只需短短几行JavaScript代码就可以使用ServiceWorkers的力量,你可以轻松地拦截HTTP请求并相应地提供更轻量级的内容。如果你正在开发一个API驱动的前端应用,并且完全没有访问服务器,那这就是个完美的选择。
ServiceWorkers允许你拦截发出的HTTP请求,进行检测并根据信息采取行动。使用FetchAPI,你可以轻松实现一个解决方案来检测Save-Data首部并提供更轻量级的内容。
我们开始创建一个名为service-worker.js的JavaScript文件,并添加清单4.10中的代码。
"usestrict";this.addEventListener('fetch',function(event){if(event.request.headers.get('save-data')){//我们想要节省数据,所以限制了图标和字体if(event.request.url.includes('fonts.googleapis.com')){//不返回任何内容event.respondWith(newResponse('',{status:417,statusText:'Ignorefontstosavedata.'}));}}});基于我们已经看过的示例,清单4.10中代码应该比较熟悉了。在代码的开始几行中,添加了事件监听器以监听任何触发的fetch事件。对于每个发起的请求,都会检查首部以查看是否启用了Save-Data。
如果启用了Save-Data首部,我会检查当前HTTP请求是否是来自“fonts.googleapis.com”域名的网络字体。因为我想为用户节省任何不必要的数据,我返回一个自定义的HTTP响应,状态码为417,状态文本是自定义的。HTTP状态代码向用户提供来自服务器的特定信息,而417状态码表示“服务器不能满足Expect请求首部域的要求”。
通过使用这项简单的技术和几行代码,我们能够减少页面的整体下载量,并确保用户节省了任何不必要的数据。这项技术可以进一步扩展,定制返回低质量的图片或者网站上其他更大的文件下载。