渐进式Web应用

7/26/2021 PWA渐进式web应用

# 渐进式Web应用

# 介绍

  • 理解pwa的核心概念
  • 理解 serviceWorker技术栈
  • 掌握渐进式Web应用的应用场景

# why

词是被遗忘的名之浅影。因为名有力量,所以词也有力量。词可以点燃人们心中的火焰,也可以让最狠的心流下眼泪。七个词便可以让一个人爱上你,十个词便可以让最坚强的人顿失意志。但是词不过是火之描绘,而名才是火之本身。

——PatrickRothfuss,《风之名》

每隔几年,Web就会经历一个关键性时刻。在这个时刻,几种独立的技术相互碰撞,在公众中引起轰动。这些技术可能已经诞生多年,也可能是刚刚获得浏览器支持的新技术。但对于旁观者来说,似乎在一个闪光的瞬间,Web突然向前跃进了一步。Ajax技术就是这样。它似乎是某一天不知从何处突然蹦出来的(尽管很多底层技术已经存在多年,如XMLHttpRequest),改变了我们对Web的理解:一系列相互链接的、大部分是静态的页面。

Ajax本身只是Web2.0革命的一部分而Web2.0是另一个强大的名字,它在2004年从天而降,一夜之间引爆了世界。

几年后,**移动优先(mobilefirst)**出现了,它标志着我们对Web开发的看法发生了转变。通过给一套设计原则命名,这两个单词获得了难以置信的力量。只用这两个单词,我们就可以大声说,用户坐在台式机前,用20英寸的显示器和一根连接到墙上的电缆上网的时代已经结束了。这两个单词让我们明白,是时候改变Web开发的方式了。

一个同样巨大的转变正在发生。幸运的是,它有一个名字。

渐进式Web应用(Progressive Web App), 简称 PWA。

学习重点:

  • 理解PWA的诞生原因,以及演进过程大于实现PWA的技术本身

# 第一章 渐进式web应用介绍

# 应用场景

渐进式Web应用是一种崭新的Web应用,它结合了原生应用的优点和Web少冲突的特点。

渐进式Web应用始于简单的网站,但随着用户的使用,它不断获得新的权限,并从网站变成一种更像传统原生应用的形式。

使用场景描述:

想象一下,你早上醒来,抓起手机,浏览本地列车公司的网站。你快速查看了上班需要乘坐的列车的时间表,然后关闭浏览器,并把手机放进口袋。下班时,你再次访问该网站,查看下一趟列车何时发车(你甚至没有注意到正在乘坐的电梯没有手机信号,因为列车公司的网站依然在运行,即使你处于离线状态)。次日,当你再次访问该网站时,浏览器询问你是否要添加快捷方式到主屏幕上,你欣然同意。当天晚些时候,当你从主屏幕的图标登录该网站时,它通知你,由于施工,列车可能会延误,并问你是否想接收关于列车时刻变化的后续通知。第三天早上,当你醒来时,手机收到消息推送,说该列车会延误15分钟。你按下闹钟上的延时按钮,又多睡了一会。

渐进式Web应用从一个简单的网站开始,慢慢获得新的权限,直到和原生应用一样,而不是试图把你送到应用商店,希望你安装应用。就这样,列车公司在你的手机上一步一步赢得了永久性的位置。

这种新的渐进增强模型,取代了只提供“安装”和“不安装”两种选择的原生应用。渐进式Web应用能赢得用户的信任,并按需获得新的权限。

你可能会问,为什么这是对原生应用的改进呢?我们为什么不坚持使用原生应用呢?好吧,除非你是少数的幸运儿,否则你应该知道,原生应用已经行不通了。用户安装应用的可能性在逐年减小,获取新用户的成本在逐渐增长,留住用户变得越来越困难。

# 当前的移动环境

当第一款iPhone在2007年推出时,它的杀手级功能是让你能够在手机上浏览网站。当移动应用于一年后诞生时,开发人员终于能够突破网页的功能限制了(同时,由于应用商店的引入,也面临许多新的限制)。Web具有高级图像技术、地理位置识别、消息推送、离线可用性、主屏幕图标等特性,但在许多开发人员的眼中,Web似乎相形见绌。原生应用接管了全球(和我们的手机)。但这种趋势正在转变。虽然我们在手机和移动应用上花费的时间比以往任何时候都多,但我们使用的应用却越来越少。用户安装的应用少了,而且只使用其中几个。如果你的应用在应用商店中排在前10位,你可能不会有这种困扰。但是,现在尝试将一款新的应用打入市场几乎是不可能的,成本就更别提了。

# 用户行为

根据2016年comScore的报告,在移动设备上,平均每人将84%的时间花在最流行的5个应用上。抱歉,这些不是你的应用。在平板电脑上,这个比例甚至更高,用户将95%的时间花在了这5个应用上。

该报告还表明,移动网站比原生应用更容易获得大量用户。拥有500万以上访问者的移动网站接近600个,比拥有类似受众的原生应用多4.5倍。排名前1000位的移动网站拥有的用户数量几乎是排名前1000位的原生应用的3倍,而且前者的用户增长速度是后者的两倍。

让用户安装并使用应用,如同在一个漏斗形空间里挣扎求生。用户需要知道你的应用(通过传统的在线广告或你的网站),然后必须访问其在应用商店中的页面,接下来需要点击安装。他们需要同意授予应用不同的权限,然后等待应用下载并安装。最后,他们至少要启动一次应用,甚至使用它。

当用户安装他们知道并喜欢的应用(比如抖音或者微信)时,这个漏斗看起来并不算太糟糕。但是大量研究表明,在这个漏斗的每一个环节,平均有20%的用户丢失了。应用开发人员为广告点击付费后,发现只有不到20%的用户实际启动了应用,这种情况并不罕见。

网站竭尽所能地让你安装他们的应用,甚至采用了一种新的广告方式。你肯定见过这种广告:当你打开一个网站,想看一篇短文或者查询明天的天气时,发现所需信息近在眼前,但是一个横幅广告随即弹了出来,挡住了你想看的内容。它问你是否愿意安装一个应用,而不直接阅读已经在你眼前的内容。

与传统网站不同,原生应用的生命并不限于用户首次发现它到离开这段短暂的时间(有时候,这段时间只有几秒钟)。一旦安装,原生应用就在你的主屏幕上占据了永久性的位置。它可以随时通过消息推送提醒你它的存在。这让开发者有很多机会在应用的长生命周期里尝试获得投资回报。

但随着渐进式Web应用的推出,潮流终于开始转向了。这些超能力所带来的优势曾经是原生应用专有的,但现在可以移植到Web应用上了。将这些能力与Web少冲突、一步漏斗的特点(使用点击链接取代安装应用)结合起来,你就会明白为什么用户、Web开发者和企业都能因拥抱渐进式Web应用而获益良多。

  • 利用PWA技术,讲移动网页的用户转换为永久用户

# 渐进式web应用的优势

随着上述超能力的引入,渐进式Web应用实现了我们寄予原生应用的很多期望。

# 无连接状态下的可用性

和传统网站不同,渐进式Web应用不依赖于用户的连接状态。当用户访问一个渐进式Web应用时,它会注册一个serviceworker,serviceworker可以检测并响应用户连接状态的变化。无论用户是离线、在线,还是处于网络不稳定状态,它都可以提供完整的用户体验。

用户可以在穿越大西洋的航班上使用你的渐进式Web应用,甚至可以进行操作(例如发布信息、回复事件或者评论文章),用户知道他的操作会在网络恢复之后立刻完成,即便他关闭了应用和浏览器。

渐进式Web应用引入了一定程度的可靠性,将用户的信任程度提升到原本只有原生应用才能达到的程度。用户知道他可以在任何时间打开WhatsApp应用,写一条短信,然后关掉手机,而无须担心连接状态。到目前为止,Web网站还没获得这种信任,这也是用户更偏向于使用原生应用的原因之一。

# 加载速度快

使用serviceworker,我们可以创建一个瞬间运行的网站,无论用户的网速极快,还是使用不可靠的2G连接,甚至是在完全没有网络连接的状态下。网站可以在几毫秒内加载,这比过去的Web要快得多,甚至比原生应用还要快。

# 推送通知

渐进式Web应用可以向用户推送通知(甚至是在用户离开网站几天后)。这些通知提供了一个好机会,让你得以重新吸引用户,并提醒他们回到应用。渐进式Web应用的通知是完全原生化的,和原生应用的通知没有区别。

# 主屏快捷方式

一旦用户表现出对渐进式Web应用感兴趣,浏览器就会自动建议用户添加快捷方式到主屏幕上——和原生应用完全一致。

# 浏览器标签页、Web和serviceworker

任何渐进式Web应用的核心都是serviceworker技术。serviceworker体现了我们对Web开发看法的转变。我们花几分钟了解一下这项技术的定位,这对于理解它的潜力至关重要。

在serviceworker出现之前,我们的代码只能在服务器或者浏览器端运行,而serviceworker的出现引入了另外一层。

serviceworker是一种脚本,可以通过注册它来控制你站点中的一个或多个页面。一旦安装完毕,serviceworker就会独立存在,而不是属于某个浏览器窗口或者标签页。

serviceworker可以监听并响应在其控制之下的所有页面的事件。向Web请求文件等事件,可以被它拦截、修改、传递并返回给页面。

image-20191206152647717

这意味着页面和Web之间增加了一层,它可以响应请求,而无论网络连接状态如何。serviceworker层甚至可以在用户离线的情况下正常工作。它可以检测到离线状态或者服务器响应慢的情况,并返回缓存内容取而代之。

再进一步,这意味着即使用户关闭浏览器中运行着你的应用的所有标签,serviceworker层依然可以和服务器通信。它可以接收并显示推送通知,或者确保任何用户操作都能够被传递到服务器(即使用户一边走进电梯一边进行操作,并在重新建立网络连接前关闭了应用)。

对于开发者来说,若能理解serviceworker和其他相关技术,收益将是巨大的。这些技术让我们可以利用现有的Web技术,包括JavaScript、HTML和CSS,编写出完全可以媲美甚至超越原生移动应用的Web应用。

# 第二章 serviceworker初探

# 环境准备

  • 客户端代码
  • nodejs服务端
  • grunt打包

image-20191206175452787

# hello world

判断浏览器中是否支持PWA

if ("serviceWorker" in navigator) {
  navigator.serviceWorker.register("/serviceworker.js").then(function (registration) {
    console.log("ServiceWorkerregisteredwithscope:", registration.scope);
  }).catch(function (err) {
    console.log("Serviceworkerregistrationfailed:", err);
  });
}
1
2
3
4
5
6
7

现在,如果你在浏览器中刷新示例应用,应该会在浏览器的控制台中看到一条错误信息,告诉你“Serviceworkerregistrationfailed.”。

serviceworker注册失败了,promise的状态也因此失败,因为目前我们还没有创建serviceworker.js文件。

创建一个空文件,命名为serviceworker.js,并放置在项目根目录的public文件夹中,即public/serviceworker.js。

self.addEventListener("fetch", function (event) {
    console.log("Fetchrequestfor:", event.request.url);
});
1
2
3

addEventListener方法(在serviceworker中,self指向serviceworker本身),在我们的serviceworker中添加了一个事件监听器。这个监听器会监听所有经过serviceworker的fetch事件,并运行我们接下来定义的函数,将事件对象event作为唯一参数传递。在我们定义的函数中,通过访问事件的request对象(这是fetch事件中的一个属性),把这次请求的URL打印到控制台中。

现在刷新页面,应该能看到页面发起的所有请求都被记录在浏览器的控制台中。

# 修改serviceworker

修改serviceworker文件的时候,这些修改并没有在刷新浏览器之后立即生效。这是因为旧版本的serviceworker依然处于激活(active)状态,与此同时,新的serviceworker仍然处于等待(waiting)状态,直到旧版本不再控制页面为止。

虽然这看起来可能非常不方便,但它实际上是serviceworker的一项非常强大的特性。

# 拦截css

self.addEventListener("fetch", function (event) {
    if (event.request.url.includes("bootstrap.min.css")) {
        event.respondWith(new Response(
            ".hotelslogan{background:green!important;}nav{display:none}",
            {
                headers: {
                    "ContentType": "text/css"
                }
            }
        ));
    }
});
1
2
3
4
5
6
7
8
9
10
11
12

# service worker的浏览器支持度

虽然serviceworker的规范在2014年才发布,但是浏览器接纳serviceworker的速度却出人意料地快。到2015年底,Chrome、Opera、Firefox和SamsungInternet都已经支持serviceworker了。

# 什么是渐进增强

渐进增强(progressiveenhancement)是我们的应用以及任何现代Web应用的核心理念。

渐进增强能够为用户提供尽可能多的功能体验。这意味着开发的网站不会仅仅因为用户浏览器不支持某项功能就崩溃。

在注册serviceworker时,我们首先要验证浏览器支持情况。支持serviceworker的浏览器用户将会享受到增强的体验,而其他用户仍将获得过往的全部体验。我们在逐步增强应用的同时,不会影响到其他的用户。

注意不要混淆渐进增强和渐进式Web应用这两个术语。虽然理想的做法是使用渐进增强的方式来开发渐进式Web应用,但是从技术上来说这不是必须的。你可以开发一个这样的渐进式Web应用,它在现代浏览器上运行得很好,但是在其他所有浏览器中都很糟糕——请不要这样做。

# https和service worker

正如你刚才看到的,serviceworker可以拦截请求、修改内容,甚至把内容完全替换成新的响应。为了保护用户和防止中间人攻击,避免恶意的第三方利用这些权限,只有使用安全连接(HTTPS)的页面才能注册serviceworker。

随着Web变得越发强大,它的许多新特性都要求使用HTTPS。除了serviceworker,其他很多新特性同样有这个要求。例如,SpeechRecognition等其他API虽然没有强制要求使用HTTPS,但是在HTTPS环境下,其功能要强大得多。甚至还有一些功能过去在非安全连接下可以使用,但是已经变成仅在HTTPS环境下可用了,例如GeolocationAPI。

如果你还需要更多的动力以迁移到HTTPS,Google宣布它已开始在搜索排名中给予使用安全连接的网页略高的权重。

如今,网站使用HTTPS比以前更便宜,也更简单。很多新的证书机构甚至已经开始免费提供SSL证书,而且配置服务器的新工具使得配置过程变得更加简单。如果你仍然坚持使用HTTP,很快就会没有借口了。

# 捕获离线请求

self.addEventListener("fetch", function (event) {
    event.respondWith(fetch(event.request));
});
1
2
3

我们监听并捕获了所有的fetch事件,然后使用另一个完全相同的fetch操作进行响应。如果你在浏览器中查看这个网站,会看到网站的行为和我们添加serviceworker之前是完全一致的。

那么这有什么意义呢?你可能还记得,在上一个例子中,我提到了fetch返回的响应是包裹在一个promise中的。通过promise包裹响应之后,我们就可以在promise失败的时候捕获异常,并进行处理。

把serviceworker.js的代码替换成下列代码:

self.addEventListener("fetch", function (event) {
    event.respondWith(fetch(event.request).catch(function () {
        return new Response("WelcometotheGothamImperialHotel.\n" + "Thereseemstobeaproblemwithyourconnection.\n" + "Welookforwardtotellingyouaboutourhotelassoonasyougoonline.");
    }));
});
1
2
3
4
5

现在,你应该会看到个性化消息,而不是浏览器的本地错误提示。

# 创建HTML响应

由于我们正在推动Web向前迈进,而不是后退,所以我们需要改进代码,向离线用户发送一个优雅的HTML页面,而不是纯文本内容。

varresponseContent = "<html>" + "<body>" + "<style>" + "body{textalign:center;backgroundcolor:#333;color:#eee;}" + "</style>" + "<h1>GothamImperialHotel</h1>" + "<p>Thereseemstobeaproblemwithyourconnection.</p>" + "<p>Comevisitusat1ImperialPlaza,GothamCityforfreeWiFi.</p>" + "</body>" + "</html>";

self.addEventListener("fetch", function (event) {
    event.respondWith(
        fetch(event.request)
            .catch(function () {
                return new Response(responseContent, { headers: { "ContentType": "text/html" } });
            }));
});
1
2
3
4
5
6
7
8
9

你可能会疑惑,为何我们一定要在创建响应时需要定义content-type。你可以尝试修改代码,把头部信息删掉,只返回响应内容,并观察结果。浏览器将会把响应视为纯文本。所有的内容都会显示为纯文本,包括HTML标签和样式。

通常你不需要告诉浏览器HTML是HTML,那么在这里发生了什么呢?大部分Web服务器的配置,都会为大部分常见文件类型自动提供正确的头部信息。当服务器发送HTML文件时,会构造一个包含HTML和多个头部的响应,其中包括一个ContentType头部,让浏览器知道该如何处理响应。由于我们是从头开始构造响应的,所以不仅要关心响应的内容(HTML),还要处理响应的头部。

# 理解serviceworker作用域

在本章前面,我们把serviceworker文件放在了项目的根目录中,现在我们来探究为什么一定要放在根目录而不是子目录中(例如/js/sw.js)。

由于serviceworker功能强大,可以修改任何通过它的请求,因此需要对其进行一定的安全限制。

试想一下,你拥有一个网站,其中列出了哥谭最好的餐馆(例如http://www.GothamEats.com/)。假设你允许每个餐馆在你的域名下托管一个网站,以提供餐馆的菜单和照片(例如http://www.GothamEats.com/Ginnos)。如果Ginnos餐馆的所有者上传了一个serviceworker脚本到他的网站(例如http://www.GothamEats.com/Ginnos/sw.js),它可以改变其竞争对手网站(例如http://www.GothamEats.com/Ralphs)的所有流量,说该餐馆歇业了,会发生什么?假如浏览器有一天允许这样的情况发生,哥谭的秩序将会受到损害。

为了预防这种问题,每个serviceworker都有一个有限的控制范围。这个范围就是通过放置serviceworker的JavaScript文件的目录决定的。之前,我们通过把serviceworker.js文件放置在项目的根目录中,允许其控制来自站点中任何地方的所有请求。如果我们把它放置到js目录中,只有源于该子目录的请求才会通过它。

你可以在注册serviceworker的时候传入一个scope选项,用来覆盖serviceworker默认的作用域。这样做可以把serviceworker的作用域限制为目录的较小子集,但是不能扩展到比它的可用范围更广(例如,你可以限制位于/ginnos/sw.js的serviceworker,让它只能影响到/ginnos/menu/的请求,但是你不能将其作用域扩展到域的根目录)。

//这两条命令将具有完全相同的作用域:
navigator.serviceWorker.register("/sw.js");
navigator.serviceWorker.register("/sw.js",{scope:"/"});

//这两条命令将注册两个不同的serviceworker
navigator.serviceWorker.register("/swginnos.js",{scope:"/Ginnos"});

//每个serviceworker各自控制了一个不同的目录:
navigator.serviceWorker.register("/swralphs.js",{scope:"/Ralphs"});

1
2
3
4
5
6
7
8
9
10

# 第三章 CacheStorage API

本章的目标是让用户在离线访问我们的站点时,可以看到indexoffine.html文件的内容,包括其中的图片和样式。

你可能会猜测,为了实现这一点,我们必须捕获失败的请求,并返回代替的内容:

self.addEventListener("fetch", function (event) {
    event.respondWith(fetch(event.request).catch(function () {
        return fetch("/indexoffline.html");
    }));
});
1
2
3
4
5

代码虽然没有编码错误,但是有逻辑问题。我们只有在知道用户离线的时候,才试图去获取离线文件。我们需要在用户在线的时候先拿到文件,并存储在设备上,这样在用户离线的时候才能提供内容。

现在仍然未解的一个谜团是在用户设备上用来存储内容的神秘领域。所幸的是,当serviceworker被引入的时候,我们还得到了新的CacheStorageAPI——这正是那个未解的谜团。

# CacheStorage是什么

CacheStorage是一种全新的缓存层,你拥有完全的控制权。我们都知道老式的浏览器缓存。这种缓存在后台不倦地工作,决定哪些文件需要缓存,何时从缓存或者网络获取文件,以及何时删除旧的缓存文件。作为开发人员,这完全不在你的控制范围内。你影响浏览器缓存内容的唯一方式,就是通过HTTP头部(在服务器发送每个响应时一并发送)向浏览器提示相关的内容。

CacheStorage也不像旧的AppCacheAPI,后者使用了一种更落后、更死板的方式,通过缓存清单文件定义了哪些文件应该能够脱机使用。AppCacheAPI已经从Web标准中移除,并且JakeArchibald在文章“ApplicationCacheisaDouchebag”中猛烈地抨击了它。

CacheStorage采取了不同的方式,将控制权放到了开发人员的手中。和前面提到的技术不同,这是通过暴露一系列基本的方法(如创建和打开任意数量的缓存,以及在其中存储、检索或删除响应)来实现的。

将serviceworker和CacheStorage的能力结合在一起,我们可以通过程序直接控制缓存哪些内容、删除哪些缓存,以及哪些内容从缓存返回、哪些内容从网络返回。

# 决定何时进行缓存

让我们来看看简化版的serviceworker生命周期

image-20191206185628872

到目前为止,我们只使用serviceworker监听了fetch事件,这类事件只能够被激活状态的serviceworker所捕获。我们需要监听一个较早发生的事件,并使用这个事件来缓存我们的serviceworker所依赖的文件。

为此,我们可以使用serviceworker的install事件。在每个serviceworker的生命周期中,这个事件只会发生一次,即在首次注册之后以及激活之前发生。在serviceworker接管页面并开始监听fetch事件之前,我们通过监听这个事件,得到了一个极好的机会来缓存所有希望离线可用的文件。

如果出现问题,我们甚至可以在install事件中取消安装serviceworker。这使得安装阶段成为了缓存所需请求的绝佳机会。如果在缓存时出现问题,可以中止安装,因为我们知道,浏览器会在用户下次访问页面时再次尝试安装serviceworker。

# 在CacheStorage中存储请求

self.addEventListener("install", function (event) {
    event.waitUntil(caches.open("gihcache")
        .then(function (cache) {
            return cache.add("/indexoffline.html");
        }));
});
1
2
3
4
5
6

首先,我们为install事件添加了事件监听器。在新的serviceworker注册之后,这个事件会立即在其安装阶段被调用。

由于我们的serviceworker将依赖于indexoffiine.html,我们需要先验证它是否已经成功缓存,然后才能认为安装成功,并激活新的serviceworker。因为需要异步获取文件并缓存起来,所以我们需要延迟install事件,直到异步事件完成。

为了实现这一点,我们在install事件中调用了waitUntil。waitUntil会延长事件的存在时间,直到传入的promise得以解决。这样我们就可以等到成功将文件存储在缓存中,再声明install事件完成,并且,如果在任何步骤遇到了问题,可以通过拒绝promise中止安装。r

在waitUntil函数中,我们调用了caches.open并传入了缓存的名称(这提示了CacheStorage的另一个强大特性:我们可以为网站创建多份缓存)。

caches.open打开并返回一个现有的缓存,或者如果没有找到对应名称的缓存,就将创建并返回它。caches.open返回一个包裹在promise中的cache对象,因此我们接下来使用then语句,并传入一个函数,该函数将这个cache对象作为参数。

我们需要做的最后一件事情,是调用cache.add('/indexoffiine.html'),这个方法将请求文件并将文件放入缓存中,缓存的键名就是"/indexoffiine.html"。

在install事件中,通过waitUntil等待缓存完成,确保了在整个链条中如果遇到任何问题,serviceworker都不会被安装。在已经激活的serviceworker的任何代码中,我们都可以认为安装事件已经成功完成了,并且indexoffine.html是在缓存中可用的。

# 从CacheStorage中取回请求

既然已经将页面的离线版本存储到CacheStorage当中,我们需要从缓存中取回并返回给用户。在serviceworker.js中添加下列代码,放置到监听install事件的代码的后面:

在serviceworker.js中添加下列代码,放置到监听install事件的代码的后面:

self.addEventListener("fetch", function (event) {
    event.respondWith(fetch(event.request).catch(function () {
        return caches.match("/indexoffline.html");
    }));
});
1
2
3
4
5

你可能会觉得这段代码似曾相识,和上一章的代码很相似。唯一的区别是,我们没有构造一个新的响应或者从Web获取内容,而是通过调用caches.match,从CacheStorage中返回内容。

你可能还注意到,代码从缓存中返回请求的时候,甚至没有先验证它是否存在于缓存中。这是因为我们已经让serviceworker的安装强依赖于缓存这一请求。

**match会返回一个promise,并向resolve方法传入在缓存中找到的第一个response对象,**当找不到任何内容的时候,它的值是undefined。即使在找不到对应的响应时,match方法返回的promise也不会被拒绝。出于这个原因,除非你可以确保肯定存在匹配,否则可能需要在返回之前先判断是否找到了匹配:

caches.match("/logo.png").then(function(response){if(response){return response;}});
1

# 在示例应用缓存

如果你再次访问首页(让这个新的serviceworker有机会安装),然后在离线状态下又一次访问,应该能够看到indexoffiine.html的内容。

但我们还没有完成。我们的代码现在只知道如何缓存并提供单个文件——indexoffiine.html。由于没有样式和图片,我们为离线用户提供的体验非常糟糕。此外,我们的代码在任何请求失败的情况下,都会傻傻地缓存并返回同一个HTML文件。不管用户请求的是index.html、bootstrap.min.css还是gihoffine.css,只要任何请求失败,serviceworker总是会返回相同的HTML文件(见图33)。

Last Updated: 8/1/2021, 5:24:42 PM