Service Worker初探

JS_Even_JS

一、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内容为空都可以,因为注册的时候,浏览器会发起屏幕快照 2020-04-10 上午10.50.54.png请求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,一个是当前已激活的和一个最新的未激活的
屏幕快照 2020-04-10 上午11.28.16.png

新安装的#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来模拟断网环境,如:
屏幕快照 2020-04-10 下午2.52.32.png

要实现离线应用,即断网后即使刷新页面也还能访问,就必须将上一次的数据存储到缓存中具体的就是存储到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立即生效
});

屏幕快照 2020-04-10 下午3.20.58.png

可以看到指定的资源已经缓存到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缓存。
屏幕快照 2020-04-10 下午3.22.45.png

之前的缓存还在,由于缓存空间是有限的,并且我们并不需要之前的缓存了,所以我们应该删除旧的缓存,如:

// 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

屏幕快照 2020-04-10 下午4.34.02.png

虽然请求的资源失败了,但是页面可以正常显示,即缓存成功了。

六、Service Worker问题

当我们在https协议下使用Service Worker的时候,如果我们使用的是自己充当CA角色签名的证书并且提示如下错误,
屏幕快照 2020-04-10 下午4.44.21.png

因为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
阅读 279

前海拾贝
徜徉前端的海洋,拾取晶莹的贝壳。

前端工程师

2.1k 声望
330 粉丝
0 条评论

前端工程师

2.1k 声望
330 粉丝
宣传栏