渐进式web应用开发---service worker

今天看到一篇文章講service worker ,感觉挺好的。于是赶紧摘录过来。原文地址见末尾

渐进式web应用开发---service worker 原理及介绍(一)

渐进式web应用(progressive Web app) 是现代web应用的一种新形式。它利用了最新的web功能,结合了原生移动应用的独特特性与web的优点,为用户带来了新的体验。

一:传统web端开发及现有移动端领域

04-05年之间,ajax出现,让传统的web开发有了一种新的体验,在我们很早之前都是在后端jsp,php等这些后端语言使用form表单提交一些简单的数据,html由后端拼接输出,但是自从有了ajax以后,改变了我们对web的理解,我们需要有更好的用户体验,因此这个时候有了前端这个行业,前后端分离了,前端负责用户体验及页面的事情,而后端则专注于业务逻辑的开发。

几年之后,移动优先原则 出现了,它标志着我们对web开发的看法发生了改变。彻底了让我们有了一种新的方式来使用网络,在我们很早之前都是用户坐在台式机前,用20英寸显示器和一根链接到墙上的电缆上网的时代已经结束了。移动领域出现后,让我们有更好的方式是使用互联网。
在07年的时候,第一款iphone被推出的时候,我们那个时候就可以使用手机来浏览我们的网站。

但是Web它具有高级图像技术、地理位置识别、消息推送、离线可用性、主屏幕图标等这些特性。这些技术在当时web领域中存在限制,因此用户体验得不到提升。因此这个时候原生应用就出现了,来解决这些事情。

但是这种趋势也正在改变,虽然我们大部分时间都花在手机应用上,比如玩游戏,看网页,平时装了很多很多应用app,但是我们平时经常用到的应用并不多,因此用户安装应用变少了,并且平时经常用的app就那么几个,我们平时安装一个app是这么一个流程,首先我要通过一些网页知道有这个app, 然后去我们手机的应用商店搜索该app,然后进行下载并安装,安装完成后,我们需要启动该应用app。然后就是使用该app.

让用户安装一个app应用程序是一个昂贵的选择,但是它相对于传统web端开发的优势是,原生应用并不仅仅用户首次打开该app到离开这些短暂的时间,一旦安装完成后,那么原生应用就会在我们的手机主屏幕中显示一个app图标,并且占据了我们手机的内存空间,但是如果该App有内容更新的时候,它可以随时通过消息推送的技术来告诉用户,相对于传统的web开发,用户体验会得到一个提升。

但是随着渐进式web应用的到来,这些曾经在原生应用有的技术,现在我们的web也能做这些事情了,下面我们可以看下我们的渐进式web应用有哪些优势。

二:渐进式web应用的优势

渐进式web应用可以做如下事情:

1. 无连接状态下的可用性。
2. 加载速度会更快。
3. 推送消息技术可以实现。
4. 可以在主屏幕上显示快捷方式。
5. 可以媲美原生应用。

2.1 无连接状态下的可用性

渐进式web应用它不依赖于用户的连接状态,当用户访问一个渐进式web应用时,它会注册一个service worker。 service worker可以检测并响应用户连接状态的变化。无论用户是离线、在线、还是处理网络断开的状态下,它都可以提供完整的用户体验。

2.2 加载速度会更快

使用service worker技术,我们可以创建一个瞬间运行的网站,无论用户的网速是很快,还是说用户的网络是2g网络,或者说用户根本就没有网络的链接状态,网站都可以在几毫秒内加载出来。这比我们的web要快很多,甚至比原生应用还快。

2.3 推送消息技术可以实现

渐进式web应用开发可以向用户发送消息,这些推送消息提供了一个好机会,可以重新吸引用户,并且提醒他们重新回到我们的网页。渐进式web应用的通知是完全原生化的,和原生应用推送消息没有区别的。

2.4 可以在主屏幕上显示快捷方式

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

2.5 可以媲美原生应用

渐进式web应用从主屏幕启动的过程中可以完全原生化,和原生应用非常相似。在加载过程中我们可以显示启动画面,可以全屏模式运行,摆脱浏览器和手机系统的UI界面,甚至我们可以锁定屏幕方向。

如上实现我们所有的关键技术就是 service worker,service worker它是一种脚本,可以通过注册它来控制我们站点中的一个或多个页面,一旦我们注册完毕后,service worker就会独立存在,它不属于某个窗口或浏览器标签页的。

service worker 可以监听并响应在其控制下的所有页面事件,比如向web请求文件等事件,它可以修改请求中的响应,可以拦截,修改,传递并返回给页面。如下所示:

如上图所示,service worker 在web应用和服务器层之间,它可以响应请求,无论网络的链接状态如何,service worker它甚至可以在用户离线的情况下正常工作。它可以检测到离线状态或者服务器响应慢的情况,它可以返回缓存内容取而代之。

它还可以当用户关闭我们的浏览器中的所有标签页,service worker依然可以和服务器进行通信,它可以接收并显示推送通知。

这就是我们service worker的优点。它是我们渐进式web应用的核心。它弥补了web应用的缺失环节。在过去我们只能使用原生来做的事情了,现在我们也可以通过web来做这些事情了。

service worker对于我们学校的成本来讲也是非常低的,它只是简简单单的javascript文件。对于我们前端开发来讲学习没有任何难度。如上就是渐进式web应用的原理,接下来我们可以慢慢来学习我们的service worker技术了。

service worker的兼容性

最后我们来看看service worker 在我们浏览器和移动端的兼容性如下图所示:

 

渐进式web应用开发---service worker (二)

1. 创建第一个service worker 及环境搭建

在上一篇文章,我们已经讲解了 service worker 的基本原理,请看上一篇文章 . 从这篇文章开始我们来学习下 service worker的基本知识。

在讲解之前,我们先来搭建一个简单的项目,和之前一样,首先我们来看下我们整个项目目录结构,如下所示:

复制代码
|----- 项目
|  |--- public
|  | |--- js               # 存放所有的js
|  | | |--- main.js        # js入口文件
|  | |--- style            # 存放所有的css
|  | | |--- main.styl      # css 入口文件
|  | |--- index.html       # index.html 页面
|  | |--- images
|  |--- package.json
|  |--- webpack.config.js
|  |--- node_modules
复制代码

如上目录结构就是我们项目的最简单的目录结构。我们先来看下我们各个目录的文件代码。

index.html 代码如下:

复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>service worker 实列</title>
</head>
<body>
  <div id="app">22222</div>
</body>
</html>
复制代码

其他的文件代码目前可以忽略不计,基本上没有什么代码。因此我们在项目的根目录下 运行  npm run dev 后,就会启动我们的页面。如下运行的页面。如下所示:

如上就是我们的页面简单的整个项目的目录架构了,现在我们来创建我们的第一个 service worker了。

一:创建我们的第一个service worker

首先从当前页面注册一个新的service worker, 因此在我们的 public/js/main.js 添加如下代码:

复制代码
// 加载css样式
require('../styles/main.styl');

console.log(navigator);

if ("serviceWorker" in navigator) {
  navigator.serviceWorker.register('/sw.js')
    .then(function(registration){
      console.log("Service Worker registered with scope: ", registration.scope);
    }).catch(function(err) {
      console.log("Service Worker registered failed:", err);
    });
}
复制代码

如上代码,首先在我们的chrome下打印 console.log(navigator); 看到如下信息:

切记:要想支持service worker 的话,在本地必须以 http://localhost:8081/ 或 http://127.0.0.1:8081 来访问页面,在线上必须以https来访问页面,否则的话,浏览器是不支持的,因为http访问会涉及到service worker安全性问题。 比如我在本地启动的是以 http://0.0.0.0:8081/ 这样来访问页面的话,浏览器是不支持 service worker的。如下所示:

如上代码,使用了if语句判断了浏览器是否支持 service worker, 如果支持的话,我们会使用 navigator.serviceWorker.register 方法注册了我们的 service worker,该方法接收2个参数,第一个参数是我们的脚本的URL,第二个参数是作用域(晚点会讲到)。

如上register方法会返回一个promise对象,如果promise成功,说明注册service worker 成功,就会执行我们的then函数代码,否则的话就会执行我们的catch内部代码。

如上代码我们的 navigator.serviceWorker.register('/sw.js'), 注册了一个 sw.js,因此我们需要在我们的 项目的根目录下新建一个 sw.js 。目前它没有该目录文件,然后我们继续刷新页面,可以看到它进入了catch语句代码;如下所示:

现在我们需要在 我们的项目根目录下新建 sw.js.

注意:我们的sw.js文件路径放到了项目的根目录下,这就意味着 serviceworker 和网站是同源的,因此在 项目的根目录下的所有请求都可以代理的。如果我们把 sw.js 放入到我们的 public 目录下的话,那么就意味这我们只能代理public下的网络请求了。但是我们可以在注册service worker的时候传入一个scope选项,用来覆盖默认的service worker的作用域。比如如下:

navigator.serviceWorker.register('/sw.js');
navigator.serviceWorker.register('/sw.js', {scope: '/'});

如上两条命令是完全相同的作用域了。

下面我们的两条命令将注册两个不同的作用域,因为他们是放在两个不同的目录下。如下代码:

navigator.serviceWorker.register('/sw.js', {scope: '/xxx'});
navigator.serviceWorker.register('/sw22.js', {scope: '/yyy'});

现在在我们的项目根目录下有sw.js 这个文件,因此这个时候我们再刷新下我们的页面,就可以看到打印出消息出来了 Service Worker registered with scope:  http://localhost:8081/ ,说明注册成功了。如下所示:

下面我们继续在我们的 项目的根目录 sw.js 添加代码如下:

console.log(self);

self.addEventListener("fetch", function(e) {
  console.log("Fetch request for: ", e.request.url);
});

如上代码,我们首先可以打印下self是什么,在service worker中,self它是指向service worker本身的。我们首先在控制台中看看self到底是什么,我们可以先打印出来看看,如下所示:

如上代码,我们的service worker添加了一个事件监听器,这个监听器会监听所有经过service worker的fetch事件,并允许我们的回调函数,我们修改sw.js后,我们并没有看到控制台打印我们的消息,因此我们先来简单的学习下我们的service worker的生命周期。

当我们修改sw.js 代码后,这些修改并没有在刷新浏览器之后立即生效,这是因为原先的service worker依然处于激活状态,但是我们新注册的 service worker 仍然处于等待状态,如果这个时候我们把原先第一个service worker停止掉就可以了,为什么有这么多service worker,那是因为我刷新下页面,浏览器会重新执行我们的main.js 代码中注册 sw.js 代码,因此会有很多service worker,现在我们要怎么做呢?我们打开我们的chrome控制台,切换到 Application 选项,然后禁用掉我们的第一个正处于激活状态下的service worker 即可,如下图所示:

禁用完成后,我们再刷新下页面即可看到我们console.log(self);会打印出来了,说明已经生效了。

但是我们现在是否注意到,我们的监听fetch事件,第一次刷新的时候没有打印出 console.log("Fetch request for: ", e.request.url); 这个信息,这是因为service worker第一次是注册,注册完成后,我们才可以监听该事件,因此当我们第二次以后刷新的时候,我们就可以使用fetch事件监听到我们页面上所有的请求了,我们可以继续第二次刷新后,在控制台我们可以看到如下信息,如下图所示:

下面为了使我们更能理解我们的整个目录架构,我们再来看下我们整个目录结构变成如下样子:

复制代码
|----- 项目
|  |--- public
|  | |--- js               # 存放所有的js
|  | | |--- main.js        # js入口文件
|  | |--- style            # 存放所有的css
|  | | |--- main.styl      # css 入口文件
|  | |--- index.html       # index.html 页面
|  | |--- images
|  |--- package.json
|  |--- webpack.config.js
|  |--- node_modules
|  |--- sw.js
复制代码

2. 使用service worker 对请求拦截

 我们第二次以后刷新的时候,我们可以监听到我们页面上所有的请求,那是不是也意味着我们可以对这些请求进行拦截,并且修改代码,然后返回呢?当然可以的,因此我们把sw.js 代码改成如下这个样子:代码如下所示:

复制代码
self.addEventListener("fetch", function(e) {
  if (e.request.url.includes("main.css")) {
    e.respondWith(
      new Response(
        "#app {color:red;}",
        {headers: { "Content-Type": "text/css" }}
      )
    )
  }
});
复制代码

如上代码,我们监听fetch事件,并检查每个请求的URL是否包含我们的 main.css 字符串,如果包含的话,service worker 会动态的创建一个Response对象,在响应中包含了自定义的css,并使用这个对象作为我们的响应,而不会向远程服务器请求这个文件。效果如下所示:

3. 从web获取内容

在第二点中,我们拦截main.css ,我们通过指定内容和头部,从零开始创建了一个新的响应对象,并使用它作为响应内容。但是service worker 更广泛的用途是响应来源于网络请求。我们也可以监听图片。

我们在我们的index.html页面中添加一张图片的代码;如下所示:

复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>service worker 实列</title>
</head>
<body>
  <div id="app">22222</div>
  <img src="/public/images/xxx.jpg" />
</body>
</html>
复制代码

然后我们在页面上浏览下,效果如下:

现在我们通过service worker来监听该图片的请求,然后替换成另外一张图片,sw.js 代码如下所示:

复制代码
self.addEventListener("fetch", function(e) {
  if (e.request.url.includes("/public/images/xxx.jpg")) {
    e.respondWith(
      fetch("/public/images/yyy.png")
    );
  }
});
复制代码

然后我们继续刷新下页面,把之前的service worker禁用掉,继续刷新页面,我们就可以看到如下效果:

和我们之前一样,我们监听fetch事件,这次我们查找的字符串是 "/public/images/xxx.jpg",当检测到有这样的请求的时候,我们会使用fetch命令创建一个新的请求,并把第二张图片作为响应返回,fetch它会返回一个promise对象。

注意:fetch方法的第一个参数是必须传递的,可以是request对象,也可以是包含一个相对路径或绝对路径的URL的字符串。比如如下:

复制代码
// url 请求
fetch("/public/images/xxx.jpg");

// 通过request对象中的url请求
fetch(e.request.url);

// 通过传递request对象请求,在这个request对象中,除了url,可能还包含额外的头部信息,表单数据等。
fetch(e.request);
复制代码

fetch它还有第二个参数,可以包含是一个对象,对象里面是请求的选项。比如如下:

fetch("/public/images/xxx.jpg", {
  method: 'POST',
  credentials: "include"
});

如上代码,对一个图片发起了一个post请求,并在头部中包含了cookie信息。fetch会返回一个promise对象。

4. 捕获离线请求

我们现在有了上面的service worker的基础知识后,我们来使用service worker检测用户何时处于离线状态,如果检测到用户是处于离线状态,我们会返回一个友好的错误提示,用来代替默认的错误提示。因此我们需要把我们的sw.js 代码改成如下所示:

复制代码
self.addEventListener("fetch", function(event) {
  event.respondWith(
    fetch(event.request).catch(function() {
      return new Response(
        "欢迎来到我们的service worker应用"
      )
    })
  );
});
复制代码

如上代码我们监听并捕获了所有的fetch事件,然后使用另一个完全相同的fetch操作进行相应,我们的fetch它是返回的是一个promise对象,因此它有catch失败时候来捕获异常的,因此当我们刷新页面后,然后再切换到离线状态时候,我们可以看到页面变成如下提示了,如下图所示:

5. 创建html响应

 如上响应都是对字符串进行响应的,但是我们也可以拼接html返回进行响应,比如我们可以把sw.js 代码改成如下:

复制代码
var responseContent = `<html>
  <body>
    <head>
      <meta charset="UTF-8">
      <title>service+worker异常</title>
      <style>body{color:red;font-size:18px;}</style>
    </head>
    <h2>service worker异常的处理</h2>
  </body>
`

self.addEventListener("fetch", function(event) {
  console.log(event.request);
  event.respondWith(
    fetch(event.request).catch(function() {
      return new Response(
        responseContent,
        {headers: {"Content-Type": "text/html"}}
      )
    })
  );
});
复制代码

然后我们继续刷新页面,结束掉第一个service worker应用,可以看到如下所示:

如上代码,我们先定义了给离线用户的html内容,并将其赋值给 responseContent变量中,然后我们添加一个事件监听器,监听所有的fetch事件,我们的回调函数就会被调用,它接收一个 event事件对象作为参数,随后我们调用了该事件对象的 respondWith 方法来响应这个事件,避免其触发默认行为。

respondWith方法接收一个参数,它可以是一个响应对象,也可以是一段通过promise得出响应对象的代码。

如上我们调用fetch,并传入原始请求对象,不仅仅是URL,它还包括头部信息、cookie、和请求方法等,如上我们的打印的 console.log(event.request); 如下所示:

fetch方法它返回的是一个promise对象,如果用户和服务器在线正常的话,那么他们就返回页面中正常的页面,那么这个时候promise对象会返回完成状态。如下我们把利息勾选框去掉,再刷新下页面,可以看到如下信息:

如上页面正常返回了,它就不会进入promise中catch异常的情况下,但是如果离线状态或者异常的情况下,那么就会进入catch异常代码。
因此catch函数就会被调用到。

6. 理解 CacheStorage缓存

在前面的学习当中,当用户离线的时候,我们可以向他们的页面显示自定义的html内容,而不是浏览器的默认错误提示,但是这样也不是最好的处理方式,我们现在想要做的是,如果用户在线的时候,我们以正常的index.html内容显示给用户,包括html内容,图片和css样式等,当用户离线的时候,我们的目标还是想显示index.html中的内容,图片和css样式等这样的显示给用户,也就是说,不管是在线也好,离线也好,我们都喜欢这样显示内容给用户访问。因此如果我们想要实现这么一个功能的话,我们需要在用户在线访问的时候,使用缓存去拿到文件,然后当用户离线的时候,我们就显示缓存中的文件和内容即可实现这样的功能。

什么是CacheStorage?

CacheStorage 是一种全新的缓存层,它拥有完全的控制权。既然CacheStorage可以缓存,那么我们现在要想的问题是,我们什么时候进行缓存呢?到目前为止,我们先来看下 service worker的简单的生命周期如下:

安装中 ----> 激活中 -----> 已激活

我们之前是使用 service worker 监听了fetch事件,那么该事件只能够被激活状态的service worker所捕获,但是我们目前不能在该事件中来缓存我们的文件。我们需要监听一个更早的事件来缓存我们的service worker页面所依赖的文件。

因此我们需要使用 service worker中的install事件,在每个service worker中,该事件只会发生一次。即首次在注册之后以及激活之前,该事件会发生。在service worker接管页面并开始监听fetch事件之前,我们通过该事件进行监听,因此就可以很好的缓存我们所有离线可用的文件。

如果安装有问题的话,我们可以在install事件中取消安装service worker,如果在缓存时出现问题的话,我们可以终止安装,因为当用户刷新页面后,浏览器会在用户下次访问页面时再次尝试安装service worker。通过这种方式,我们可以有效的为service worker创建安装依赖,也就是说在service worker安装并激活之前,我们必须下载并缓存这些文件。

因此我们现在把 sw.js 全部代码改成如下代码:

复制代码
// 监听install的事件
self.addEventListener("install", function(e) {
  e.waitUntil(
    caches.open("cacheName").then(function(cache) {
      return cache.add("/public/index.html");
    })
  )
});
复制代码

如上代码,我们为install事件添加了事件监听器,在新的service worker注册之后,该事件会立即在其安装阶段被调用。

如上我们的service worker 依赖于 "/public/index.html", 我们需要验证它是否成功缓存,然后我们才能认为它安装成功,并激活新的 service worker,因为需要异步获取文件并缓存起来,所以我们需要延迟install事件,直到异步事件完成,因此我们这边使用了waitUntil,waitUntil会延长事件存在的时间,直到promise成功,我们才会调用then方法后面的函数。
在waitUntil函数中,我们调用了 caches.open并传入了缓存的名称为 "cacheName". caches.open 打开并返回一个现有的缓存,如果没有找到该缓存,我们就创建该缓存并返回他。最后我们执行then里面的回调函数,我们使用了 cache.add("/public/index.html").这个方法将请求文件放入缓存中,缓存的键名是 "/public/index.html"。

2. 从CacheStorage中取回请求

上面我们使用 cache.add 将页面的离线版本存储到 CacheStorage当中,现在我们需要做的事情是从缓存中取回并返回给用户。

因此我们需要在sw.js 中添加fetch事件代码,添加如下代码:

复制代码
// 监听fetch事件
self.addEventListener("fetch", function(e) {
  e.respondWith(
    fetch(e.request).catch(function() {
      return caches.match("/public/index.html");
    })
  )
});
复制代码

如上代码和我们之前的fetch事件代码很相似,我们这边使用 caches.match 从CacheStorage中返回内容。

注意:match方法可以在caches对象上调用,这样会在所有缓存中寻找,也可以在某个特定的cache对象上调用。如下所示:

复制代码
// 在所有缓存中寻找匹配的请求
caches.match('/public/index.html');

// 在特定的缓存中寻找匹配的请求
cache.open("cacheName").then(function(cache) {
  return cache.match("/public/index.html");
});
复制代码

match方法会返回一个promise对象,并且向resolve方法传入在缓存中找到的第一个response对象,当找不到任何内容的时候,它的值是undefined。也就是说,即使match找不到对应的响应的时候,match方法也不会被拒绝。如下所示:

caches.match("/public/index.html").then(function(response) {
  if (response) {
    return response;
  }
});

3. 在demo中使用缓存

在如上我们已经把sw.js 代码改成如下了:

复制代码
// 监听install的事件
self.addEventListener("install", function(e) {
  e.waitUntil(
    caches.open("cacheName").then(function(cache) {
      return cache.add("/public/index.html");
    })
  )
});

// 监听fetch事件
self.addEventListener("fetch", function(e) {
  e.respondWith(
    fetch(e.request).catch(function() {
      return caches.match("/public/index.html");
    })
  )
});
复制代码

当我们第一次访问页面的时候,我们会监听install事件,对我们的 "/public/index.html" 进行缓存,然后当我们切换到离线状态的时候,我们再次刷新可以看到我们的页面只是缓存了 index.html页面,但是页面中的css和图片并没有缓存,如下所示:

现在我们要做的事情是,我们需要对我们所有页面的上的css,图片,js等资源文件进行缓存,因此我们的sw.js 代码需要改成如下所示:

复制代码
var CACHE_NAME = "cacheName";
var CACHE_URLS = [
  "/public/index.html",
  "/main.css",
  "/public/images/xxx.jpg"
];

self.addEventListener("install", function(e) {
  e.waitUntil(
    caches.open(CACHE_NAME).then(function(cache) {
      return cache.addAll(CACHE_URLS);
    })
  )
});

// 监听fetch事件
self.addEventListener("fetch", function(e) {
  e.respondWith(
    fetch(e.request).catch(function() {
      return caches.match(e.request).then(function(response) {
        if (response) {
          return response;
        } else if (e.request.headers.get("accept").includes("text/html")) {
          return caches.match("/public/index.html");
        }
      })
    })
  )
});
复制代码

如上代码,我们设置了两个变量,第一个变量 CACHE_NAME 是缓存的名称,第二个变量是一个数组,它包含了一份需要存储的URL列表。

然后我们使用了 cache.addAll()方法,它接收的参数是一个数组,它的作用是把数组里面的每一项存入缓存当中去,当然如果任何一个缓存失败的话,那么它返回的promise会被拒绝。

我们监听了install事件后对所有的资源文件进行了缓存后,当用户处于离线状态的时候,我们使用fetch的事件来监听所有的请求。当请求失败的时候,我们会调用catch里面的函数来匹配所有的请求,它也是返回一个promise对象,当匹配成功后,找到对应的项的时候,直接从缓存里面读取,如果没有找到的话,就直接返回 "/public/index.html" 的内容作为代替。为了安全起见,我们在返回"/public/index.html" 之前,我们还进行了一项检查,该检查确保请求是包含 text/html的accept的头部,因此我们就不会返回html内容给其他的请求类型。比如图片,样式请求等。

我们之前创建一个html响应的时候,必须将其 Content-Type的值定义为 text/html,方便浏览器可以正确的响应识别它是html类型的,那么为什么我们这边没有定义呢,而直接返回了呢?那是因为我们的 cache.addAll()请求并缓存的是一个完整的response对象,该对象不仅包含了响应体,还包含了服务器返回的任何响应头。

注意:使用 caches.match(e.request) 来查找缓存中的条目会存在一个陷阱。

比如用户可能不会总是以相同的url来访问我们的页面,比如它会从其他的网站中跳转到我们的页面上来,比如后面带了一些参数,比如:"/public/index.html?id=xxx" 这样的,也就是说url后面带了一些查询字符串等这些字段,如果我们还是和之前一样进行匹配,是匹配不到的,因此我们需要在match方法中添加一个对象选项;如下所示:

caches.match(e.request, {ignoreSearch: true});

这样的,通过 ignoreSearch 这个参数,通知match方法来忽略查询字符串。

现在我们先请求下我们的页面,然后我们勾选离线复选框,再来查看下我们的页面效果如下所示:

如上可以看到,我们在离线的状态下,页面显示也是正常的。

7. 理解service worker生命周期

 service worker 的生命周期如下图所示:

installing(正在安装)

当我们使用 navigator.serviceWorker.register 注册一个新的 service worker的时候,javascript代码就会被下载、解析、并进入安装状态。如果安装成功的话,service worker 就会进入 installed(已安装)的状态。但是如果在安装过程中出现错误,脚本将被永久进入 redundant(废弃中)。

installed/waiting(已安装/等待中)

一旦service worker 安装成功了,就会进入 installed状态,一般情况中,会马上进入 activating(激活中)状态。除非另一个正在激活的 service worker 依然在被控制中。在这种情况下它会维持在 waiting(等待中)状态。

activating(激活中)

在service worker激活并接管应用之前,会触发 activate 事件。

activated(已激活)

一旦 service worker 被激活了,它就准备好接管页面并监听功能性事件(比如fetch事件)。

redundant(废弃)

如果service worker在注册或安装过程中失败了,或者被新的版本代替,就会被置为 redundant 状态,处于这种 service worker将不再对应用产生任何影响。

注意:service worker的状态和浏览器的任何一个窗口或标签页都没有关系的,也就是说 如果service worker是activated(已激活)状态的话,它就会保持这个状态。

8. 理解 service worker 注册过程

 当我们第一次访问我们的网站的时候(我们也可以通过删除service worker后刷新页面的方式进行模拟),页面就会加载我们的main.js 代码,如下所示:

复制代码
if ("serviceWorker" in navigator) {
  navigator.serviceWorker.register('/sw.js', {scope: '/'}).then(function(registration) {
    console.log("Service Worker registered with scope: ", registration.scope);
  }).catch(function(err) {
    console.log("Service Worker registered failed:", err);
  });
}
复制代码

那么我们的应用就会注册service worker,那么service worker的文件将会被下载,然后就开始安装,install事件就会被触发,并且在service worker整个生命周期中只会触发一次,然后触发了我们的函数,将调用的时机记录在控制台中。service worker随后就会进入 installed状态。然后立即就会变成 activating 状态。这个时候,我们的另一个函数就会被触发,因此 activate事件就会把状态记录到控制台中。最后,service worker 进入 activated(已激活)状态。现在service worker被激活状态了。

但是当我们的service worker 正在安装的时候,我们的页面已经开始加载并且渲染了。也就是说 service worker 变成了 active状态了。因此就不能控制页面了,只有我们再刷新页面的时候,我们的已经被激活的service worker才能控制页面。因此我们就可以监听和操控fetch事件了。

9. 理解更新service worker

 我们首先来修改下 sw.js 代码,改成如下:

复制代码
self.addEventListener("fetch", function(e) {
  if (e.request.url.includes("main.css")) {
    e.respondWith(
      new Response(
        "#app {color:red;}",
        {headers: { "Content-Type": "text/css" }}
      )
    )
  }
});
复制代码

然后我们再刷新页面,发现页面中的颜色并没有改变,为什么呢?但是我们service worker明明控制了页面,但是页面为什么没有生效?
我们可以打开chrome开发者工具中 Application -> Service Worker 来理解这段代码的含义:如下图所示:

如上图所示,页面注册了多个service worker,但是只有一个在控制页面,旧的service worker是激活的,而新的service worker仍处于等待状态。

每当页面加载一个激活的service worker,就会检查 service worker 脚本的更新。如果文件在当前的service worker注册之后发生了修改,新的文件就会被注册和安装。安装完成后,它并不会替换原先的service worker,而是会保持 waiting 状态。它将会一直保持等待状态,直到原先的service worker作用域中的每个标签和窗口关闭。或者导航到一个不再控制范围内的页面。但是我们可以关闭原先的service worker, 那么原先的service worker 就会变成废弃状态。然后我们新的service worker就会被激活。因此我们可以如下图所示:

但是如果我们想修改完成后,不结束原来的service worker的话,想改动代码,刷新一下就生效的话,我们需要把 Update on reload这个复选框勾上即可生效,比如如下所示:

注意:

那么为什么安装完成新的service worker 不能实时生效呢?比如说安装新的service worker不能控制新的页面呢?原先的service worker 控制原先的页面呢?为什么浏览器不能跟踪多个service worker 呢?为什么所有的页面都必须由单一的service worker所控制呢?

我们可以设想下如下这么一个场景,如果我们发布了一个新版本的service worker,并且该service worker的install事件会从缓存中删除 update.json 该文件,并添加 update2.json文件作为代替,并且修改fetch事件,让其在请求用户数据的时候,返回新的文件,如果多个service worker控制了不同的页面,那么旧的service worker控制的页面可能会在缓存中搜索旧的 update.json 文件,但是该文件又被删除了,那么就会导致该应用奔溃。因此我们需要被确保打开所有的标签页或窗口由一个service worker控制的话,就可以避免类似的问题发生。

10. 理解缓存管理和清除缓存

为什么需要管理缓存呢?

我们首先把我们的sw.js 代码改成原先的如下代码:

复制代码
var CACHE_NAME = "cacheName";
var CACHE_URLS = [
  "/public/index.html",
  "/main.css",
  "/public/images/xxx.jpg"
];

self.addEventListener("install", function(e) {
  e.waitUntil(
    caches.open(CACHE_NAME).then(function(cache) {
      return cache.addAll(CACHE_URLS);
    })
  )
});

// 监听fetch事件
self.addEventListener("fetch", function(e) {
  e.respondWith(
    fetch(e.request).catch(function() {
      return caches.match(e.request).then(function(response) {
        if (response) {
          return response;
        } else if (e.request.headers.get("accept").includes("text/html")) {
          return caches.match("/public/index.html");
        }
      })
    })
  )
});
复制代码

如上代码,我们的service worker会在安装阶段下载并缓存所需要的文件。如果希望它再次下载并缓存这些文件的话,我们就需要触发另一个安装事件。在sw.js中的,我们把 CACHE_NAME 名字改下即可。比如叫 var CACHE_NAME = "cacheName2"; 这样的。

通过如上给缓存名称添加版本号,并且每次修改文件时自增它,可以实现两个目的。

1)修改缓存名称后,浏览器就知道安装新的service worker来代替旧的service worker了,因此会触发install事件,因此会导致文件被下载并存储在缓存中。

2)它为每一个版本的service worker都创建了一份单独的缓存。即使我们更新了缓存,在用户关闭所有页面之前,旧的service worker依然是激活的。旧的service worker可能会用到缓存中的某些文件,而这些文件又是可以被新的service worker所修改的,通过让每个版本的service worker所拥有自己的缓存,就可以确保不会出现其他的异常情况。

如何清除缓存呢?

caches.delete(cacheName); 该方法接收一个缓存名字作为第一个参数,并删除对应的缓存。

caches.keys(); 该方法是获取所有缓存的名称,并且返回一个promsie对象,其完成的时候会返回一个包含缓存名称的数组。如下所示:

caches.keys().then(function(cacheNames){
  cacheNames.forEach(function(cacheName){
    caches.delete(cacheName);
  });
});

如何缓存管理呢?

在service worker生命周期中,我们需要实现如下目标:
1)每次安装 service worker,我们都需要创建一份新的缓存。
2)当新的service worker激活的时候,就可以安全删除过去的service worker 创建的所有缓存。

因此我们在现有的代码中,添加一个新的事件监听器,监听 activate 事件。

复制代码
self.addEventListener("activate", function(e) {
  e.waitUntil(
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        cacheNames.map(function(cacheName) {
          if (CACHE_NAME !== cacheName && cacheName.startWith("cacheName")) {
            return caches.delete(cacheName);
          }
        })
      )
    })
  )
});
复制代码

11. 理解重用已缓存的响应

如上我们的带版本号缓存已经实现了,为我们提供了一个非常灵活的方式来控制我们的缓存,并且保持最新的缓存。但是缓存内的实现是非常低下的。

比如每次我们创建一个新的缓存的时候,我们会使用 cache.add() 或 cache.addAll() 这样的方法来缓存应用需要的所有文件。但是,如果用户已经在本地拥有了 cacheName 这个缓存的话,那如果这个时候我们创建 cacheName2 这个缓存的话,我们发现我们创建的 cacheName2 需要缓存的文件 在 cacheName 已经有了,并且我们发现这些文件是永远不会被改变的。如果我们重新缓存这些文件的话,就浪费了宝贵的带宽和时间从网络再次下载他们。

为了解决如上的问题,我们需要如下做:

如果我们创建一个新的缓存,我们首先要遍历一份不可变的文件列表,然后从现有的缓存中寻找他们,并直接复制到新的缓存中。因此我们的sw.js 代码变成如下所示:

复制代码
var CACHE_NAME = "cacheName";
var immutableRequests = [
  "/main.css",
  "/public/images/xxx.jpg"
];

var mutableRequests = [
  "/public/index.html"
];

self.addEventListener("install", function(e) {
  e.waitUntil(
    caches.open(CACHE_NAME).then(function(cache) {
      var newImmutableRequests = [];
      return Promise.all(
        immutableRequests.map(function(url) {
          return caches.match(url).then(function(response) {
            if (response) {
              return cache.put(url, response);
            } else {
              newImmutableRequests.push(url);
              return Promise.resolve();
            }
          });
        })
      ).then(function(){
        return cache.addAll(newImmutableRequests.concat(mutableRequests));
      })
    })
  )
});
复制代码

如上代码。

1)immutableRequests 数组中包含了我们知道永远不会改变的资源URL,这些资源可以安全地在缓存之间复制。

2)mutableRequests 中包含了每次创建新缓存时,我们都需要从网络中请求的url。

如上代码,我们的 install 事件会遍历所有的 immutableRequests,并且在所有现有的缓存中寻找他们。如果被找到的话,都会使用cache.put()复制到新的缓存中。如果没有找到该资源的话,会被放入到新的 newImmutableRequests 数组中。

一旦所有的请求被检查完毕,代码就会使用 cache.addAll()来缓存 mutableRequests 和 newImmutableRequests 中所有的URL。

 

原文链接:https://www.cnblogs.com/tugenhua0707/p/11148968.html

作者:龙恩0707

posted @ 2020-07-08 17:24  勒布朗_詹姆斯  阅读(524)  评论(0编辑  收藏  举报