zoukankan      html  css  js  c++  java
  • 渐进式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

  • 相关阅读:
    ueditor内容带格式回显(html字符串回显)
    thymleaf th:text="|第${user.courseSort}课|" 对于不知道的真的是解渴了
    Thymleaf 从某处(不包含某处)开始截取字符串到末尾
    layUI 实现自定义弹窗
    layUI实现可选项 弹框
    layUI弹出框提示
    点击编辑table变为可编辑状态
    POI导出数据以Excel的方式录入,下载
    基于BootStrap的initupload()实现Excel上传和获取excel中的数据
    下载导入模板
  • 原文地址:https://www.cnblogs.com/blueball/p/13268160.html
Copyright © 2011-2022 走看看