一、Service Worker简介
Service workers 本质上充当Web应用程序与浏览器之间的代理服务器,比如拦截客户端的请求、向客户端发送消息、向服务器发起请求等等,其中最重要的作用之一就是离线资源缓存,先将网络请求进行拦截,然后判断网络是否可用,如果网络不可用,那么读取浏览器缓存并返回,从而实现离线应用。
Service worker运行在worker上下文中,因此它不能访问DOM,同理Service Worker中运行的代码不会被阻塞,也不会阻塞其他页面的JS文件中的代码。
Service Worker 是一个浏览器中的进程而不是浏览器内核下的线程,因此它在被注册安装之后,能够被在多个页面中使用,也不会因为页面的关闭而被销毁。
出于对安全问题的考虑,Service Worker 只能被使用在 https 或者本地的 localhost 环境下。
二、Service Worker的基本使用
- 注册
Service Worker需要先通过serviceWorker对象注册,serviceWorker对象存在于navigator对象下,调用其register()方法即可,可以接收两个参数,第一个参数为Service Worker的执行脚本路径,第二个参数为一个配置对象,比如配置Service Worker的作用域scope范围,其会返回一个Promise对象,如:
// index.html
// 注册一个Service Worker,注册成功后Service Worker会执行sw.js脚本
navigator.serviceWorker.register("./sw.js", {scope: "./"}).then((reg) => {
console.log("注册成功");
}).catch((err) => {
console.log("注册失败");
console.log(err);
});
需要注意的是,只有sw.js存在才能注册成功,即使这个sw.js内容为空都可以,因为注册的时候,浏览器会发起请求sw.js脚本,如果请求失败则会注册失败,如:
- 下载并安装
Service Worker注册成功后,浏览器就会开始下载并安装。我们可以在Service Worker运行脚本中监听其install事件,如:
// sw.js
// 监听Service Worker install安装事件
this.addEventListener("install", (e) => {
console.log("Service Worker start install.");
});
当Service Worker安装成功后,再次刷新页面,Service Worker并不会重新注册、安装,但是register()返回的是一个Promise对象,所以刷新页面后,由于之前已经注册成功了,所以其then()方法还是会执行的。
- 激活
Service Worker安装成功后会被激活,我们可以在Service Worker运行脚本中监听其activate事件,如:
// sw.js
// 监听Service Worker activate激活事件
this.addEventListener("activate", (e) => {
console.log("Service Worker start activate.");
});
需要注意的是,当Service Worker执行脚本发生变化后,此时刷新页面,Service Worker是会被重新注册和安装的,但是新安装的Service Worker是不会被激活的,虽然每次修改Service Worker执行脚本刷新页面后,Service Worker都会被重新注册和安装,但其不会以队列的形式保存所有安装的Service Worker,而是用最新的覆盖之前未激活的。即你只会看到两个Service Worker,一个是当前已激活的和一个最新的未激活的。
新安装的#2940并没有被激活,而是在等待激活,如果继续修改Service Worker执行脚本,那么#2941会覆盖掉之前的#2940。
因为在默认情况下,Service Worker会每24小时被下载一次,如果下载的执行脚本文件发生了变化,那么它就会被重新注册和安装,但不会被激活,当不再有页面使用旧的 Service Worker 的时候,它才会被激活。即如果把全部页面关闭后,再次打开该页面,那么之前未激活的Service Worker才会被激活。
当然我们也可以在install事件回调函数中,直接调用skipWaiting()方法来让Service Worker立即生效。
// sw.js
this.addEventListener("install", (e) => {
console.log("Service Worker start install.");
this.skipWaiting(); // 调用skipWaiting让Service Worker立即生效
});
三、Service Worker与页面信息通信
- 页面向Service Worker发送数据
由于Service Worker是运行在Worker上下文中的,所以其可以和Worker一样通过postMessage方法进行信息的发送,关键是要获取到Service Worker实例对象。获取Service Worker实例有两种方式:
① 通过serviceWorker的controller属性获取
② 通过register()方法返回的注册对象的active属性获取
即当调用register()方法后会resolve出一个注册对象,然后通过这个注册对象的active属性拿到Service Worker实例。
需要注意的是,Service Worker注册完成后,是无法立即拿到Service Worker实例的,因为只有被激活后才能拿到Service Worker实例,而激活是一个异步的过程。
// index.html
navigator.serviceWorker.register("./sw.js", {scope: "./"}).then((reg) => {
console.log("注册成功");
console.log(navigator.serviceWorker.controller); // 刚注册立即获取为null
navigator.serviceWorker.controller && navigator.serviceWorker.controller.postMessage("this message from Page.");
});
// index.html
navigator.serviceWorker.register("./sw.js", {scope: "./"}).then((reg) => {
console.log("注册成功");
console.log(reg.active); // 刚注册立即获取为null
reg.active && reg.active.postMessage("this message from Page.");
})
Service Worker执行脚本中直接监听message事件即可拿到页面发送过来的数据了,如:
// sw.js
// 监听message事件,获取页面发送过来的数据
this.addEventListener("message", (e) => {
console.log(e.data);
});
* Service Worker向页面发送数据
Service Worker监听到message事件后,可以通过事件对象的source属性拿到发送消息的WindowClient对象,通过这个WindowClient对象即可向页面发送数据了,页面中通过navigator.serviceWorker监听message事件即可,如:
// index.html
// 监听来自Service Worker发送的数据
navigator.serviceWorker.addEventListener('message', function (e) {
console.log(e.data);
});
// sw.js
// 通过事件对象的source获取WindowClient对象向页面发送数据
this.addEventListener("message", (e) => {
console.log(e.data);
console.log(e.source); // 获取WindowClient对象
e.source.postMessage("this message from Service Worker.");
});
四、使用 MessageChannel 来通信
MessageChannel会创建一个通信的管道,这个管道有两个端口,每个端口都可以通过postMessage发送数据,而一个端口只要绑定了onmessage回调方法,就可以接收从另一个端口传过来的数据。
// index.html
navigator.serviceWorker.register("./sw.js", {scope: "./"}).then((reg) => {
console.log("注册成功");
const messageChannel = new MessageChannel();
messageChannel.port1.onmessage = (e) => { // 页面上通过port1监听message事件
console.log(e.data);
}
// 将port2传给Service Worker,然后Service Worker就可以用port2发送数据了
reg.active && reg.active.postMessage("this message from Page.", [messageChannel.port2]);
})
// sw.js
this.addEventListener("message", (e) => {
console.log(e.data);
// 获取传递过来的port2发送数据,页面通过port1就能收到来自Service Worker发送过来的数据了
e.ports[0].postMessage("this message from Service Worker.");
});
五、实现一个简单的离线应用
前面说过,Service Worker只能在localhost和https下使用。对于localhost,直接断开网络也是可以正常访问的,但是我们可以通过打开浏览器调试窗口,然后在NetWork下选择offline来模拟断网环境,如:
要实现离线应用,即断网后即使刷新页面也还能访问,就必须将上一次的数据存储到缓存中,具体的就是存储到cacheStorage中。
- 资源缓存
index.html内容如下,其中引入了一个index.css文件,如:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" type="text/css" href="index.css" />
</head>
<body>
</body>
</html>
index.css内容如下:
body {
background: red;
}
我们首先在Service Worker安装的时候,打开缓存并且将需要缓存的资源添加到缓存中,如:
const cacheName = "index-cache-v1"; // 指定缓存的名称
this.addEventListener("install", async (e) => {
console.log("Service Worker start install.");
const cache = await caches.open(cacheName); // 打开缓存
cache.addAll([ // 缓存index.html和index.css两个文件
"./index.css",
"./index.html"
]);
this.skipWaiting(); // 让Service Worker立即生效
});
可以看到指定的资源已经缓存到cache Storage中了,但是此时如果断开网络刷新页面,缓存资源会立即被清空,页面会显示无网络。因为我们只是将资源缓存了起来,并没有在断网的时候使用这些缓存。
- 更新缓存
当你修改了缓存资源,那么我们就要进行缓存的更新,我们可以通过修改缓存名称来更新缓存,如:
修改index.css,如下:
body {
background: blue;
}
修改缓存名称,如下:
const cacheName = "index-cache-v2"; // 更新缓存名称
this.addEventListener("install", async (e) => {
console.log("Service Worker start install.");
const cache = await caches.open(cacheName); // 打开缓存
cache.addAll([ // 缓存index.html和index.css两个文件
"./index.css",
"./index.html"
]);
this.skipWaiting(); // 让Service Worker立即生效
});
再次刷新页面,可以看到多了一个index-cache-v2缓存。
之前的缓存还在,由于缓存空间是有限的,并且我们并不需要之前的缓存了,所以我们应该删除旧的缓存,如:
// Service Worker激活后清空旧的缓存
this.addEventListener("activate", async (e) => {
console.log("Service Worker start activate.");
const keys = await caches.keys(); // 获取所有缓存的名称
keys.forEach((key) => { // 遍历缓存,如果与当前最新缓存名称不一致则删除
if (key !== cacheName) {
caches.delete(key);
}
});
});
- 断网的时候使用缓存
要想在断网的时候使用缓存,那么我们必须先拦截用户的请求,然后对网络进行判断,如果有网则请求最新的资源,如果没有网络则读取缓存资源并返回,可以通过监听fetch事件对所有请求进行拦截。
// 监听fetch事件拦截所有请求
this.addEventListener("fetch", (e) => {
console.log("interceptor user fetch");
const req = e.request; // 获取请求对象
e.respondWith(getResponse(req));
// 获取response
async function getResponse(req) {
let response = null;
try {
response = await fetch(req); // 如果网络正常那么发起请求获取最新的资源
console.log("net ok");
} catch(err) { // 如果网络异常
console.log("net fail.");
const cache = await caches.open(cacheName); // 打开缓存
response = await cache.match(req); // 传入请求对象找到匹配的缓存
}
return response;
}
});
需要注意的是,测试的时候,访问的url地址要带上index.html,否则缓存会匹配失败,即使用http://localhost:8080/index.html
而不是http://localhost:8080
虽然请求的资源失败了,但是页面可以正常显示,即缓存成功了。
六、Service Worker问题
当我们在https协议下使用Service Worker的时候,如果我们使用的是自己充当CA角色签名的证书并且提示如下错误,
因为SSL certificate存在问题,我们必须通过命令启动我们的谷歌浏览器,忽略掉证书错误,这样Service Worker才能在https协议下正常使用。
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --user-data-dir=./tmp --ignore-certificate-errors --unsafely-treat-insecure-origin-as-secure=https://localhost:8080
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。