Service Worker —这应该是一个挺全面的整理

前段日子有空,粗略学习了一下 Service Worker ;最近从新复习了下,而且把内容整理后写在这里,但愿对你们有所帮助。css

同时,若是文章中有错误或者描述不当的地方,欢迎你们可以帮我指正,谢谢。react

PS:文章很长,含有大量示例代码。你们能够慢慢看:)json


介绍

做为一个比较新的技术,你们能够把 Service Worker 理解为一个介于客户端和服务器之间的一个代理服务器。在 Service Worker 中咱们能够作不少事情,好比拦截客户端的请求、向客户端发送消息、向服务器发起请求等等,其中最重要的做用之一就是离线资源缓存。浏览器

首先,做为一个新技术,咱们须要关注的是它在不一样浏览器的兼容性,下面是来自于 caniuse.com 的一张图: 缓存

各大浏览器对 Service Worker 的 兼容性
从这张图中,咱们能够看到 IE 和 Opera Mini 全面扑街,而主流浏览器中 Edge 17如下不支持,Safair 和 IOS Safair 刚刚开始支持,而火狐和 Chrome 支持良好。因此你们能够放心的使用,不过最好仍是作一下判断。

对于 Service Worker ,了解过 Web Worker 的同窗可能会比较好理解。它和 Web Worker 相比,有相同的点,也有不一样的地方。安全

相同:bash

  1. Service Worker 工做在 worker context 中,是没有访问 DOM 的权限的,因此咱们没法在 Service Worker 中获取 DOM 节点,也没法在其中操做 DOM 元素;
  2. 咱们能够经过 postMessage 接口把数据传递给其余 JS 文件;
  3. Service Worker 中运行的代码不会被阻塞,也不会阻塞其余页面的 JS 文件中的代码;

不一样的地方在于,Service Worker 是一个浏览器中的进程而不是浏览器内核下的线程,所以它在被注册安装以后,可以被在多个页面中使用,也不会由于页面的关闭而被销毁。所以,Service Worker 很适合被用与多个页面须要使用的复杂数据的计算——购买一次,全家“收益”。服务器

另外有一点须要注意的是,出于对安全问题的考虑,Service Worker 只能被使用在 https 或者本地的 localhost 环境下并发

注册安装

下面就让咱们来使用 Service Worker 。异步

若是当前使用的浏览器支持 Service Worker ,则在 window.navigator 下会存在 serviceWorker 对象,咱们可使用这个对象的 register 方法来注册一个 Service Worker。

这里须要注意的一点是,Service Worker 在使用的过程当中存在大量的 Promise ,对于 Promise 不是很了解的同窗能够先去看一下相关文档。 Service Worker 的注册方法返回的也是一个 Promise 。

// index.js
if ('serviceWorker' in window.navigator) {
  navigator.serviceWorker.register('./sw.js', { scope: './' })
    .then(function (reg) {
      console.log('success', reg);
    })
    .catch(function (err) {
      console.log('fail', err);
    });
}
复制代码

在这段代码中,咱们先使用 if 判断下浏览器是否支持 Service Worker ,避免因为浏览器不兼容致使的 bug 。

register 方法接受两个参数,第一个是 service worker 文件的路径,请注意:这个文件路径是相对于 Origin ,而不是当前 JS 文件的目录的;第二个参数是 Serivce Worker 的配置项,可选填,其中比较重要的是 scope 属性。按照文档上描述,它是 Service Worker 控制的内容的子目录,这个属性所表示的路径不能在 service worker 文件的路径之上,默认是 Serivce Worker 文件所在的目录;关于这个属性,文档中讲的不是很清楚,我也有不少疑问,会在接下来的内容中提出。但愿有知道的同窗能帮我解惑。

register 方法返回一个 Promise 。若是注册失败,能够经过 catch 来捕获错误信息;若是注册成功,可使用 then 来获取一个 ServiceWorkerRegistration 的实例,有兴趣的同窗能够去翻阅文档。

注册完 Service Worker 以后,浏览器会为咱们自动安装它,所以咱们就能够在 service worker 文件中监听它的 install 事件了。

// sw.js
this.addEventListener('install', function (event) {
  console.log('Service Worker install');
});
复制代码

一样的,Service Worker 在安装完成后会被激活,因此咱们也可监听 activate 事件。

// sw.js
this.addEventListener('activate', function (event) {
  console.log('Service Worker activate');
});
复制代码

这时,咱们能够在 Chorme 的开发者工具中看到咱们注册的 Service Worker。

Chrome 开发者工具
在默认状况下,Service Worker 一定会 每24小时被下载一次,若是下载的文件是最新文件,那么它就会被从新注册和安装,但不会被激活,当再也不有页面使用旧的 Service Worker 的时候,它就会被激活。
Service Worker 等到被激活
这对于咱们开发来讲是很不方便的,所以在这里我勾选了一个名为 Update on reload 的单选框,选中它以后,咱们每次刷新页面都可以使用最新的 service worker 文件。

在同一个 Origin 下,咱们能够注册多个 Service Worker。可是请注意,这些 Service Worker 所使用的 scope 必须是不相同的

if ('serviceWorker' in window.navigator) {
  navigator.serviceWorker.register('./sw/sw.js', { scope: './sw' })
    .then(function (reg) {
      console.log('success', reg);
    })
  navigator.serviceWorker.register('./sw2/sw2.js', { scope: './sw2' })
    .then(function (reg) {
      console.log('success', reg);
    })
}
复制代码

信息通信

以前说过,使用 postMessage 方法能够进行 Service Worker 和页面之间的通信,下面就让咱们来试一下。

从页面到 Service Worker

首先是从页面发送信息到 Serivce Worker 。

// index.js
if ('serviceWorker' in window.navigator) {
  navigator.serviceWorker.register('./sw.js', { scope: './' })
    .then(function (reg) {
      console.log('success', reg);
      navigator.serviceWorker.controller && navigator.serviceWorker.controller.postMessage("this message is from page");
    });
}
复制代码

为了保证 Service Worker 可以接收到信息,咱们在它被注册完成以后再发送信息,和普通的 window.postMessage 的使用方法不一样,为了向 Service Worker 发送信息,咱们要在 ServiceWorker 实例上调用 postMessage 方法,这里咱们使用到的是 navigator.serviceWorker.controller

// sw.js
this.addEventListener('message', function (event) {
  console.log(event.data); // this message is from page
});
复制代码

在 service worker 文件中咱们能够直接在 this 上绑定 message 事件,这样就可以接收到页面发来的信息了。

对于不一样 scope 的多个 Service Worker ,我么也能够给指定的 Service Worker 发送信息。

// index.js
if ('serviceWorker' in window.navigator) {
  navigator.serviceWorker.register('./sw.js', { scope: './sw' })
    .then(function (reg) {
      console.log('success', reg);
      reg.active.postMessage("this message is from page, to sw");
    })
  navigator.serviceWorker.register('./sw2.js', { scope: './sw2' })
    .then(function (reg) {
      console.log('success', reg);
      reg.active.postMessage("this message is from page, to sw 2");
    })
}

// sw.js
this.addEventListener('message', function (event) {
  console.log(event.data); // this message is from page, to sw
});

// sw2.js
this.addEventListener('message', function (event) {
  console.log(event.data); // this message is from page, to sw 2
});
复制代码

请注意,当咱们在注册 Service Worker 时,若是使用的 scope 不是 Origin ,那么navigator.serviceWorker.controller 会为 null。这种状况下,咱们可使用 reg.active 这个对象下的 postMessage 方法,reg.active 就是被注册后激活 Serivce Worker 实例。可是,因为 Service Worker 的激活是异步的,所以第一次注册 Service Worker 的时候,Service Worker 不会被马上激活, reg.active 为 null,系统会报错。我采用的方式是返回一个 Promise ,在 Promise 内部进行轮询,若是 Service Worker 已经被激活,则 resolve 。

// index.js
navigator.serviceWorker.register('./sw/sw.js')
    .then(function (reg) {
      return new Promise((resolve, reject) => {
        const interval = setInterval(function () {
          if (reg.active) {
            clearInterval(interval);
            resolve(reg.active);
          }
        }, 100)
      })
    }).then(sw => {
      sw.postMessage("this message is from page, to sw");
    })

  navigator.serviceWorker.register('./sw2/sw2.js')
    .then(function (reg) {
      return new Promise((resolve, reject) => {
        const interval = setInterval(function () {
          if (reg.active) {
            clearInterval(interval);
            resolve(reg.active);
          }
        }, 100)
      })
    }).then(sw => {
      sw.postMessage("this message is from page, to sw2");
    })
复制代码

从 Service Worker 到页面

下一步就是从 Service Worker 发送信息到页面了,不一样于页面向 Service Worker 发送信息,咱们须要在 WindowClient 实例上调用 postMessage 方法才能达到目的。而在页面的JS文件中,监听 navigator.serviceWorker 的 message 事件便可收到信息。

而最简单的方法就是从页面发送过来的消息中获取 WindowClient 实例,使用的是 event.source ,不过这种方法只能向消息的来源页面发送信息。

// sw.js
this.addEventListener('message', function (event) {
  event.source.postMessage('this message is from sw.js, to page');
});

// index.js
navigator.serviceWorker.addEventListener('message', function (e) {
  console.log(e.data); // this message is from sw.js, to page
});
复制代码

若是不想受到这个限制,则能够在 serivce worker 文件中使用 this.clients 来获取其余的页面,并发送消息。

// sw.js
this.clients.matchAll().then(client => {
  client[0].postMessage('this message is from sw.js, to page');
})
复制代码

关于这个方法,我有一些没有解决的疑问的。在个人试验中,注册 Service Worker 时候设置的 scope 的值会对获取到的 client 产生影响。

若是在注册 Service Worker 的时候,把 scope 设置为非 origin 目录,那么在 service worker 文件中,我没法获取到 Origin 路径对应页面的 client。

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

// sw.js
this.clients.matchAll().then(client => {
  console.log(client); // []
})
复制代码

我查找了一些资料,可是没有找到关于 scope 和 client 之间的联系的明确说明文档。个人猜想是,Service Worker 是否只可以获取到 scope 路径下的子页面的 client ,可是我使用 react router 试验发现彷佛又不是,但愿有知道的同窗可以帮忙解答,谢谢!

使用 Message Channel 来通讯

另一种比较好用的通讯方式是使用 Message Channel 。

// index.js
navigator.serviceWorker.register('./sw.js', { scope: './' })
    .then(function (reg) {
      const messageChannel = new MessageChannel();
      messageChannel.port1.onmessage = e => {
        console.log(e.data); // this message is from sw.js, to page
      }
      reg.active.postMessage("this message is from page, to sw", [messageChannel.por2]);
    })

// sw.js
this.addEventListener('message', function (event) {
  console.log(event.data); // this message is from page, to sw
  event.ports[0].postMessage('this message is from sw.js, to page');
});
复制代码

使用这种方式可以使得通道两端之间能够相互通讯,而不是只能向消息源发送信息。举个例子,两个 Service Worker 之间的通讯。

// index.jsconst messageChannel = new MessageChannel();

  navigator.serviceWorker.register('./sw/sw.js')
    .then(function (reg) {
      console.log(reg)
      return new Promise((resolve, reject) => {
        const interval = setInterval(function () {
          if (reg.active) {
            clearInterval(interval);
            resolve(reg.active);
          }
        }, 100)
      })
    }).then(sw => {
      sw.postMessage("this message is from page, to sw", [messageChannel.port1]);
    })

  navigator.serviceWorker.register('./sw2/sw2.js')
    .then(function (reg) {
      return new Promise((resolve, reject) => {
        const interval = setInterval(function () {
          if (reg.active) {
            clearInterval(interval);
            resolve(reg.active);
          }
        }, 100)
      })
    }).then(sw => {
      sw.postMessage("this message is from page, to sw2", [messageChannel.port2]);
    })

// sw.js
this.addEventListener('message', function (event) {
  console.log(event.data); // this message is from page, to sw
  event.ports[0].onmessage = e => {
    console.log('sw:', e.data); // sw: this message is from sw2.js
  }
  event.ports[0].postMessage('this message is from sw.js');
});

// sw2.js
this.addEventListener('message', function (event) {
  console.log(event.data); // this message is from page, to sw2
  event.ports[0].onmessage = e => {
    console.log('sw2:', e.data); // sw2: this message is from sw.js
  }
  event.ports[0].postMessage('this message is from sw2.js');
});
复制代码

首先让页面给两个 Service Worker 发送信息,而且把信息通道的端口发送过去;而后在两个 service worker 文件中使用端口分别设置接受信息的回调函数,以后它们就可以互相发送信息并接收到来自通道对面的消息了。

静态资源缓存

下面要讲的就是重头戏,也是 Service Worker 可以实现的最主要的功能——静态资源缓存。

正常状况下,用户打开网页,浏览器会自动下载网页所须要的 JS 文件、图片等静态资源。咱们能够经过 Chrome 开发工具的 Network 选项来查看。

静态资源
可是若是用户在没有联网的状况下打开网页,浏览器就没法下载这些展现页面效果所必须的资源,页面也就没法正常的展现出来。

咱们可使用 Service Worker 配合 CacheStroage 来实现对静态资源的缓存。

缓存指定静态资源

// sw.js
this.addEventListener('install', function (event) {
  console.log('install');
  event.waitUntil(
    caches.open('sw_demo').then(function (cache) {
      return cache.addAll([
        '/style.css',
        '/panda.jpg',
        './main.js'
      ])
    }
    ));
});
复制代码

当 Service Worker 在被安装的时候,咱们可以对制定路径的资源进行缓存。CacheStroage 在浏览器中的接口名是 caches ,咱们使用 caches.open 方法新建或打开一个已存在的缓存;cache.addAll 方法的做用是请求指定连接的资源并把它们存储到以前打开的缓存中。因为资源的下载、缓存是异步行为,因此咱们要使用事件对象提供的 event.waitUntil 方法,它可以保证资源被缓存完成前 Service Worker 不会被安装完成,避免发生错误。

从 Chrome 开发工具中的 Application 的 Cache Strogae 中能够看到咱们缓存的资源。

Cache Stroage
这种方法只能缓存指定的资源,无疑是不实用的,因此咱们须要针对用户发起的每个请求进行缓存。

动态缓存静态资源

this.addEventListener('fetch', function (event) {
  console.log(event.request.url);
  event.respondWith(
    caches.match(event.request).then(res => {
      return res ||
        fetch(event.request)
          .then(responese => {
            const responeseClone = responese.clone();
            caches.open('sw_demo').then(cache => {
              cache.put(event.request, responeseClone);
            })
            return responese;
          })
          .catch(err => {
            console.log(err);
          });
    })
  )
});
复制代码

咱们须要监听 fetch 事件,每当用户向服务器发起请求的时候这个事件就会被触发。有一点须要注意,页面的路径不能大于 Service Worker 的 scope,否则 fetch 事件是没法被触发的。

在回掉函数中咱们使用事件对象提供的 respondWith 方法,它能够劫持用户发出的 http 请求,并把一个 Promise 做为响应结果返回给用户。而后咱们使用用户的请求对 Cache Stroage 进行匹配,若是匹配成功,则返回存储在缓存中的资源;若是匹配失败,则向服务器请求资源返回给用户,并使用 cache.put 方法把这些新的资源存储在缓存中。由于请求和响应流只能被读取一次,因此咱们要使用 clone 方法复制一份存储到缓存中,而原版则会被返回给用户

在这里有几点须要注意:

  1. 当用户第一次访问页面的时候,资源的请求是早于 Service Worker 的安装的,因此静态资源是没法缓存的;只有当 Service Worker 安装完毕,用户第二次访问页面的时候,这些资源才会被缓存起来;
  2. Cache Stroage 只能缓存静态资源,因此它只能缓存用户的 GET 请求;
  3. Cache Stroage 中的缓存不会过时,可是浏览器对它的大小是有限制的,因此须要咱们按期进行清理;

对于用户发起的 POST 请求,咱们也能够在拦截后,经过判断请求中携带的 body 的内容来进行有选择的返回。

if(event.request.method === 'POST') {
      event.respondWith(
        new Promise(resolve => {
          event.request.json().then(body => {
            console.log(body); // 用户请求携带的内容
          })
          resolve(new Response({ a: 2 })); // 返回的响应
        })
      )
    }
}
复制代码

咱们能够在 fetch 事件的回掉函数中对请求的 method 、url 等各项属性进行判断,选择不一样的操做。

对于静态资源的缓存,Cache Stroage 是个不错的选择;而对于数据,咱们可使用 IndexedDB 来存储,一样是拦截用户请求后,使用缓存在 IndexDB 中的数据做为响应返回,详细的内容我就不在这里讲了,有兴趣的同窗能够本身去了解下。

更新 Cache Stroage

前面提到过,当有新的 service worker 文件存在的时候,他会被注册和安装,等待使用旧版本的页面所有被关闭后,才会被激活。这时候,咱们就须要清理下咱们的 Cache Stroage 了,删除旧版本的 Cache Stroage 。

this.addEventListener('install', function (event) {
  console.log('install');
  event.waitUntil(
    caches.open('sw_demo_v2').then(function (cache) { // 更换 Cache Stroage
      return cache.addAll([
        '/style.css',
        '/panda.jpg',
        './main.js'
      ])
    }
    ))
});

const cacheNames = ['sw_demo_v2']; // Cahce Stroage 白名单

this.addEventListener('activate', function (event) {
  event.waitUntil(
    caches.keys().then(keys => {
      return Promise.all[keys.map(key => {
        if (!cacheNames.includes(key)) {
          console.log(key);
          return caches.delete(key); // 删除不在白名单中的 Cache Stroage
        }
      })]
    })
  )
});
复制代码

首先在安装 Service Worker 的时候,要换一个 Cache Stroage 来存储,而后设置一个白名单,当 Service Worker 被激活的时候,将不在白名单中的 Cache Stroage 删除,释放存储空间。一样使用 event.waitUntil ,在 Service Worker 被激活前执行完删除操做。

小结

Service Worker 做为一个新的技术,在静态资源缓存和处理多页面所需的复杂数据等方面都有很不错的应用前景。做为实现 PWA 不可或缺的一部分,我相信,不论是他的浏览器兼容性、功能的多样性以及文档的完整性,都会变的愈来愈好。

同时,确定还有不少我没有学到、讲到,或者是我忽略了 Service Worker 的内容存在,因此我但愿能够和你们一块儿学习,特别是 scope 这个属性,但愿有知道的同窗帮我答疑解惑,谢谢。


感谢阅读,未经容许,请勿转载:)

相关文章
相关标签/搜索