前言
service worker是PWA(Progressive Web App,是提升 Web App 的体验的一种新方法,能给用户原生应用的体验。)的重要组成部分,W3C 组织早在 2014 年 5 月就提出过 Service Worker 这样的一个 HTML5 API ,主要用来做持久的离线缓存,也是Web Worker的升级版。
科普 Service Worker
Service Worker 是 PWA 中重要的一部分,它是一个网站安插在用户浏览器中的大脑。Service Worker 是这样被注册在页面上的:
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
}
为什么说 SW(下文将 Service Worker 简称为 SW)是网站的大脑?举个例子,如果在 www.example.com 的根路径下注册了一个 SW,那么这个 SW 将可以控制所有该浏览器向 www.example.com 站点发起的请求。只需要监听 fetch 事件,你就可以任意的操纵请求,可以返回从 CacheStorage 中读的数据,也可以通过 Fetch API 发起新的请求,甚至可以 new 一个 Response,返回给页面。
// 一段糟糕的 SW 代码,在这个 SW 注册好以后,整个 SW 控制站点的所有请求返回的都将是字符串 "bad",包括页面的 HTML
self.addEventListener('fetch', function(event) {
event.respondWith(
new Response('bad')
);
});
就是因为 SW 权利太大了,写起来才会如履薄冰,一不小心有些页面资源就不能及时正确的更新了。
前提条件
Service Worker 出于安全性和其实现原理,在使用的时候有一定的前提条件。
-
由于 Service Worker 要求 HTTPS 的环境,我们通常可以借助于 github page 进行学习调试。当然一般浏览器允许调试 Service Worker 的时候 host 为
localhost
或者127.0.0.1
也是 ok 的。 -
Service Worker 的缓存机制是依赖 Cache API 实现的
-
依赖 Promise 实现
注册
要安装 Service Worker, 我们需要通过在 js 主线程(常规的页面里的 js )注册 Service Worker 来启动安装,这个过程将会通知浏览器我们的 Service Worker 线程的 javaScript 文件在什么地方呆着。
先来感受一段代码
if ('serviceWorker' in navigator) {
window.addEventListener('load', function () {
navigator.serviceWorker.register('/sw.js', {scope: '/'})
.then(function (registration) {
// 注册成功
console.log('ServiceWorker registration successful with scope: ', registration.scope);
})
.catch(function (err) {
// 注册失败:(
console.log('ServiceWorker registration failed: ', err);
});
});
}
-
这段代码首先是要判断 Service Worker API 的可用情况,支持的话咱们才继续谈实现,否则免谈了。
-
如果支持的话,在页面
onload
的时候注册位于/sw.js
的 Service Worker。 -
每次页面加载成功后,就会调用
register()
方法,浏览器将会判断 Service Worker 线程是否已注册并做出相应的处理。 -
register 方法的 scope 参数是可选的,用于指定你想让 Service Worker 控制的内容的子目录。本 demo 中服务工作线程文件位于根网域, 这意味着服务工作线程的作用域将是整个来源。
关于
register
方法的 scope 参数,需要说明一下:Service Worker 线程将接收 scope 指定网域目录上所有事项的 fetch 事件,如果我们的 Service Worker 的 javaScript 文件在/a/b/sw.js
, 不传 scope 值的情况下, scope 的值就是/a/b
。scope 的值的意义在于,如果 scope 的值为
/a/b
, 那么 Service Worker 线程只能捕获到 path 为/a/b
开头的(/a/b/page1
,/a/b/page2
,...)页面的 fetch 事件。通过 scope 的意义我们也能看出 Service Worker 不是服务单个页面的,所以在 Service Worker 的 js 逻辑中全局变量需要慎用。 -
then()
函数链式调用我们的 promise,当 promise resolve 的时候,里面的代码就会执行。 -
最后面我们链了一个
catch()
函数,当 promise rejected 才会执行。
代码执行完成之后,我们这就注册了一个 Service Worker,它工作在 worker context,所以没有访问 DOM 的权限。在正常的页面之外运行 Service Worker 的代码来控制它们的加载。
安装
在你的 Service Worker 注册成功之后呢,我们的浏览器中已经有了一个属于你自己 web App 的 worker context 啦, 在此时,浏览器就会马不停蹄的尝试为你的站点里面的页面安装并激活它,并且在这里可以把静态资源的缓存给办了。
install 事件我们会绑定在 Service Worker 文件中,在 Service Worker 安装成功后,install 事件被触发。
install 事件一般是被用来填充你的浏览器的离线缓存能力。为了达成这个目的,我们使用了 Service Worker 新的标志性的存储 cache API — 一个 Service Worker 上的全局对象,它使我们可以存储网络响应发来的资源,并且根据它们的请求来生成key。这个 API 和浏览器的标准的缓存工作原理很相似,但是是只对应你的站点的域的。它会一直持久存在,直到你告诉它不再存储,你拥有全部的控制权。
localStorage 的用法和 Service Worker cache 的用法很相似,但是由于 localStorage 是同步的用法,所以不允许在 Service Worker 中使用。 IndexedDB 也可以在 Service Worker 内做数据存储。
// 监听 service worker 的 install 事件
this.addEventListener('install', function (event) {
// 如果监听到了 service worker 已经安装成功的话,就会调用 event.waitUntil 回调函数
event.waitUntil(
// 安装成功后操作 CacheStorage 缓存,使用之前需要先通过 caches.open() 打开对应缓存空间。
caches.open('my-test-cache-v1').then(function (cache) {
// 通过 cache 缓存对象的 addAll 方法添加 precache 缓存
return cache.addAll([
'/',
'/index.html',
'/main.css',
'/main.js',
'/image.jpg'
]);
})
);
});
-
这里我们 新增了一个 install 事件监听器,接着在事件上接了一个
ExtendableEvent.waitUntil()
方法——这会确保 Service Worker 不会在 waitUntil() 里面的代码执行完毕之前安装完成。 -
在
waitUntil()
内,我们使用了caches.open()
方法来创建了一个叫做 v1 的新的缓存,将会是我们的站点资源缓存的第一个版本。它返回了一个创建缓存的 promise,当它 resolved 的时候,我们接着会调用在创建的缓存实例(Cache API)上的一个方法addAll()
,这个方法的参数是一个由一组相对于 origin 的 URL 组成的数组,这些 URL 就是你想缓存的资源的列表。 -
如果 promise 被 rejected,安装就会失败,这个 worker 不会做任何事情。这也是可以的,因为你可以修复你的代码,在下次注册发生的时候,又可以进行尝试。
-
当安装成功完成之后,Service Worker 就会激活。在第一次你的 Service Worker 注册/激活时,这并不会有什么不同。但是当 Service Worker 更新的时候 ,就不太一样了。
参考文献:https://lavas.baidu.com/pwa/offline-and-cache-loading/service-worker/how-to-use-service-worker