ServiceWorker
是一个运行在浏览器背后的独立线程,它拥有访问网络的能力,可以用来实现缓存、消息推送、后台自动更新等功能,甚至可以用来实现一个完整的 Web 服务器。
因为
ServiceWorker
运行在浏览器背后,因为这个特性,它可以实现一些不需要服务器参与的功能,比如消息推送、后台自动更新等。
什么是 ServiceWorker
ServiceWorker
提供了一个一对一的代理服务器,它可以拦截浏览器的请求,然后根据自己的逻辑来处理这些请求,比如可以直接返回缓存的资源,或者从网络上获取资源,然后将资源缓存起来,再返回给浏览器。
既然作为一个服务器,那么它就拥有着对应的生命周期,它没有传统的服务器那么复杂,它只有两个生命周期,分别是安装和激活,这个状态可以通过
ServiceWorker.state
来获取。
相信大家都不喜欢干巴巴的文字,下面我们来看一下
ServiceWorker
是怎么使用的,然后看一下它的生命周期,慢慢介绍它的功能。
ServiceWorker 的使用
注册 ServiceWorker
ServiceWorker
的注册是通过
navigator.serviceWorker.register
来完成的;
它接受两个参数:
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js', {
scope: '/'
}).then(function (registration) {
// 注册成功
console.log('ServiceWorker registration successful with scope: ', registration.scope);
}).catch(function (err) {
// 注册失败 :(
console.log('ServiceWorker registration failed: ', err);
});
}
上面的代码我们分四个部分讲解:
-
第一部分是判断浏览器是否支持
ServiceWorker
,如果不支持,那么就提示或者做其他的处理。
-
第二部分是注册
ServiceWorker
,调用
navigator.serviceWorker.register
方法,它会返回一个
Promise
对象。
-
第三部分是
register
方法的第一个参数,它是
ServiceWorker
的脚本地址,这个地址是相对于当前页面的地址的。
-
第四部分是
register
方法的第二个参数,它是一个配置对象,目前只有一个属性
scope
,用来指定
ServiceWorker
的作用域,它的默认值是
ServiceWorker
脚本所在目录。
这里需要注意的就是第三部分和第四部分,我们先来看一下
register
的函数签名,再来讲注意的地方。
/**
* 注册 ServiceWorker
* @param {string} scriptURL ServiceWorker 脚本地址
* @param {Object} options 配置项
* @param {string} options.scope ServiceWorker 作用域
* @returns {Promise}
*/
register(scriptURL, options)
函数签名看着很简单,但是我们需要注意的是
scriptURL
和
scope
的值,它们的值是相对于当前页面的地址的,而不是相对于
ServiceWorker
脚本的地址的。
scriptURL
其实也没什么好说的,同之前讲的
Worker
一样,就是我们的脚本地址;
scope
的值是用来指定
ServiceWorker
的作用域的,它的默认值是
ServiceWorker
脚本所在目录,也就是
scriptURL
的值,但是我们可以通过
scope
来指定它的作用域,它的作用域是一个目录,它的值是相对于当前页面的地址的,也就是说,它的值是相对于
scriptURL
的值的。
上面说的有点绕,我们直接上代码,上面已经有了注册的代码了,我们现在补充
service-worker.js
的代码,看一下
scope
的值是怎么指定的。
// service-worker.js
self.addEventListener('install', function (event) {
console.log('install');
});
self.addEventListener('activate', function (event) {
console.log('activate');
});
self.addEventListener('fetch', function (event) {
console.log('fetch');
});
上面的代码我都写好了之后,我们将它们放到服务器上,然后访问你托管的地址,打开控制台,你会看到如下的输出:
可以看到上面有三个输出,首先我们看到的是
ServiceWorker
的生命周期,经过了安装和激活,然后看到了注册成功的提示;
将页面刷新再看看控制台:
可以看到并没有进行安装和激活,这是因为我们的
ServiceWorker
已经注册成功了,它会一直存在,除非我们手动的注销它,否则它不会再次进行安装和激活。
注意:我这里出现了 4 次
fetch
,这是因为我有插件的原因,插件请求了一些资源,所以会触发
fetch
事件,
fetch
事件会在后面讲到。
ServiceWorker 生命周期
上面我们已经成功的注册了
ServiceWorker
,那么它的生命周期我们肯定是需要关注一下的,它的生命周期有三个阶段,分别是安装、激活和运行。
安装
安装阶段是在
ServiceWorker
注册成功之后,浏览器开始下载
ServiceWorker
脚本的阶段;
这个阶段是一个异步的过程,我们可以在
install
事件中监听它,它的回调函数会接收到一个
event
对象;
我们可以通过
event.waitUntil
来监听它的完成状态,当它完成之后,我们需要调用
event.waitUntil
的参数,这个参数是一个
Promise
对象,当这个
Promise
对象完成之后,浏览器才会进入下一个阶段。
self.addEventListener('install', function (event) {
console.log('install');
event.waitUntil(
// 这里可以做一些缓存的操作
);
});
注意:
event.waitUntil
不要乱用,它会阻塞浏览器的安装,如果你的
Promise
对象一直没有完成,那么浏览器就会一直处于安装的状态,这样会影响到浏览器的正常使用。
激活
激活阶段是在安装完成之后,浏览器开始激活
ServiceWorker
的阶段;
这个阶段也是一个异步的过程,我们可以在
activate
事件中监听它,它的回调函数会接收到一个
event
对象;
self.addEventListener('activate'
, function (event) {
console.log('activate');
event.waitUntil(
// 这里可以做一些清理缓存的操作
);
});
不同于安装阶段,激活阶段不需要等待
event.waitUntil
的传递的
Permise
对象完成,它会立即进入下一个阶段。
但是永远不要传递一个可能一直处于
pending
状态的
Promise
对象,否则会导致
ServiceWorker
一直处在某一个状态而无法响应,导致浏览器卡死。
运行
运行阶段是在激活完成之后,
ServiceWorker
开始运行的阶段;
这个阶段是一个长期存在的过程,我们可以在
fetch
事件中监听它,它的回调函数会接收到一个
event
对象;
self.addEventListener('fetch', function (event) {
console.log('fetch');
});
任何请求拦截都是在这个阶段进行的,我们可以在这个阶段中对请求进行拦截,然后返回我们自己的响应。
ServiceWorker 请求拦截
上面我们已经成功的注册了
ServiceWorker
,并且它已经进入了运行阶段,那么我们就可以在这个阶段中对请求进行拦截了。
在上面我贴的图可以看到,
ServiceWorker
连插件的请求都拦截了,这是因为
ServiceWorker
的优先级是最高的,它会拦截所有的请求,包括插件的请求。
我的插件请求了是一些
css
文件,也就是说
ServiceWorker
拦截了这些请求,然后返回了自己的响应,这个响应就是我们在
ServiceWorker
中缓存的资源。
插件的请求咱们不用管,现在来看看我们的
ServiceWorker
到底能拦截多少种类型的请求:
html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
<link rel="stylesheet" href="index.css">
head>
<body>
<script src="axios.js">script>
<script>
// 注册service worker
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js', {
scope: '/'
}).then(function (registration) {
// 注册成功
console.log('ServiceWorker registration successful with scope: ', registration.scope);
}).catch(function (err) {
// 注册失败 :(
console.log('ServiceWorker registration failed: ', err);
});
}
// 使用axios发送请求
axios.get('/').then(function (response) {
console.log('axios 成功');
});
// 使用XMLHttpRequest发送请求
const xhr = new XMLHttpRequest();
xhr.open('GET', '/');
xhr.send();
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
console.log('XMLHttpRequest 成功');
}
}
// 使用fetch发送请求
fetch('/').then(function (response) {
console.log('fetch 成功');
});
script>
body>
html>
上面的代码中我发送了五个请求,分别是请求
axios.js
,
axios
发送请求,
XMLHttpRequest
发送请求,
fetch
发送请求,最头部还有一个
css
请求;
css
的内容自己随意发挥,我这里就不贴了。
可以看到,
ServiceWorker
只进入了7次
fetch
事件,也就是说只拦截了7次请求,我们可以通过
event.request.url
来查看请求的地址。
self.addEventListener('fetch', function (event) {
console.log('fetch', event.request.url);
});
通过打印请求地址,发现
axios.js
没有进入
fetch
事件,但是并不影响我们的结果。
关于静态资源为什么没有进入
fetch
事件,我这里没有查到相关资料,但是其实确实是进入了
fetch
事件。
ServiceWorker 监听事件
上面因为我们只监听了
fetch
事件,所以只有
fetch
请求被拦截了,那么我们可以监听哪些事件呢?
从最开始的生命周期的两个事件,
install
和
activate
,到后面的
fetch
网络请求的,还有其他什么事件呢?
现在就来看看
ServiceWorker
的事件列表:
-
install
:安装事件,当
ServiceWorker
安装成功后,就会触发这个事件,这个事件只会触发一次。
-
activate
:激活事件,当
ServiceWorker
激活成功后,就会触发这个事件,这个事件只会触发一次。
-
fetch
:网络请求事件,当页面发起网络请求时,就会触发这个事件。
-
push
:推送事件,当页面发起推送请求时,就会触发这个事件。
-
sync
:同步事件,当页面发起同步请求时,就会触发这个事件。
-
message
:消息事件,当页面发起消息请求时,就会触发这个事件。
-
messageerror
:消息错误事件,当页面发起消息错误请求时,就会触发这个事件。
-
error
:错误事件,当页面发起错误请求时,就会触发这个事件。
可以看到最后三个是我们的老伙伴了,
message
,
messageerror
,
error
,它在这个基础上还增加了两个事件,
push
和
sync
。
翻了很多资料,
ServiceWorker
还可以监听
notification
事件,但是目前我还没有找到相关的资料,后续找到了我会单独写一篇文章来讲解。
message
,
messageerror
,
error
这三个事件,我们在上一篇文章中已经讲解过了,就是主线程和
Worker
之间的通信,文末有链接,可以去看看。
push
和
sync
这两个事件,今天这里不详解,后续我会单独写一篇文章来讲解。
ServiceWorker 缓存
缓存是我们日常开发中经常会用到的一个功能,
ServiceWorker
也提供了缓存的功能,我们可以通过
ServiceWorker
来缓存我们的静态资源,这样就可以离线访问我们的页面了。
ServiceWorker
的缓存是基于
CacheStorage
的,它是一个
Promise
对象,我们可以通过
caches
来获取它;
caches.open('my-cache').then(function (cache) {
// 这里可以做一些缓存的操作
});
CacheStorage
提供了一些方法,我们可以通过这些方法来对缓存进行操作;
添加缓存
我们可以通过
cache.put
来添加缓存,它接收两个参数,第一个参数是
Request
对象,第二个参数是
Response
对象;
caches.open('my-cache').then(function (cache) {
cache.put(new Request('/'), new Response('Hello World'));
});
获取缓存
我们可以通过
cache.match
来获取缓存,它接收一个参数,这个参数可以是
Request
对象,也可以是
URL
字符串;
caches.open('my-cache').then(function (cache) {
cache.match('/').then(function (response) {
console.log(response);
});
});
删除缓存
我们可以通过
cache.delete
来删除缓存,它接收一个参数,这个参数可以是
Request
对象,也可以是
URL
字符串;
caches.open('my-cache').then(function (cache) {
cache.delete('/').then(function () {
console.log('删除成功');
});
});
清空缓存
我们可以通过
cache.keys
来获取缓存的
key
,然后通过
cache.delete
来删除缓存;
caches.open('my-cache').then(function (cache) {
cache.keys().then(function (keys) {
keys.forEach(function (key) {
cache.delete(key);
});
});
});
ServiceWorker 缓存策略
ServiceWorker
的缓存策略是基于
fetch
事件的,我们可以在
fetch
事件中监听请求,然后对请求进行拦截,然后返回我们自己的响应;
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.match(event.request).then(function (response) {
if (response) {
return response;
}
return fetch(event.request);
})
);
});
上面的代码是一个最简单的缓存策略,它会先从缓存中获取请求,如果缓存中没有请求,那么就会从网络中获取请求;
缓存资源
文章开始我们介绍了
ServiceWorker
的生命周期,然后又详解了
fetch
事件,最后又讲了一堆缓存的东西,这些都是为我们接下来的内容做铺垫,接下来我们缓存一些静态资源,然后离线访问我们的页面;
还是上面
fetch
事件的例子,我们请求了 6 个资源,其中 1 是
index.html
,1 是
axios.js
,1 个是
index.css
,剩余的 3 个都是请求的'/',也是我们的
index.html
;
但是上面的例子中我们什么都没做,所以我们的页面是没有缓存的,我们可以通过
cache.addAll
来缓存一些资源;
通常我们会在
install
事件中缓存一些资源,因为
install
事件只会触发一次,并且会阻塞
activate
事件,所以我们可以在
install
事件中缓存一些资源,然后在
activate
事件中删除一些旧的资源;
self.addEventListener('install', function (event) {
event.waitUntil(
caches.open('my-cache').then(function (cache) {
return cache.addAll([
'/',
'/index.css',
'/axios.js',
'/index.html'
]);
})
);
});
上面的代码中我们缓存了刚才提到的所有资源,缓存了之后当然是使用缓存的资源了,所以我们可以在
fetch
事件中返回缓存的资源;
注意:上面缓存的所有资源一定都是确定的存在的,不能出现除状态码为 200 以外的其他状态码,否则缓存会失败;
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.match(event.request).then(function (response) {
if (response) {
return response;
}
return fetch(event.request);
})
);
});
上面的代码中我们使用
caches.match
来匹配请求,如果匹配到了,那么就返回缓存的资源,如果没有匹配到,那么就从网络中获取资源,这也就是我们刚才提到的缓存策略:
缓存优先
看看上面的图,当我们第一次访问页面的时候,我们的页面是没有缓存的,所以我们的页面是从网络中获取的,当我们刷新页面的时候,我们的页面是从缓存中获取的,可以看到来源是
ServiceWorker
;