作者:田八
https://juejin.cn/post/7165893682132959245
ServiceWorker
是一个运行在浏览器背后的独立线程,它拥有访问网络的能力,可以用来实现缓存、消息推送、后台自动更新等功能,甚至可以用来实现一个完整的 Web 服务器。
因为ServiceWorker
运行在浏览器背后,因为这个特性,它可以实现一些不需要服务器参与的功能,比如消息推送、后台自动更新等。
什么是 ServiceWorker
ServiceWorker
提供了一个一对一的代理服务器,它可以拦截浏览器的请求,然后根据自己的逻辑来处理这些请求,比如可以直接返回缓存的资源,或者从网络上获取资源,然后将资源缓存起来,再返回给浏览器。
既然作为一个服务器,那么它就拥有着对应的生命周期,它没有传统的服务器那么复杂,它只有两个生命周期,分别是安装和激活,这个状态可以通过ServiceWorker.state
来获取。
相信大家都不喜欢干巴巴的文字,下面我们来看一下ServiceWorker
是怎么使用的,然后看一下它的生命周期,慢慢介绍它的功能。
ServiceWorker 的使用
注册 ServiceWorker
ServiceWorker
的注册是通过navigator.serviceWorker.register
来完成的;
它接受两个参数:
第一个参数是
ServiceWorker
的脚本地址第二个参数是一个配置对象,目前只有一个属性
scope
,用来指定ServiceWorker
的作用域,它的默认值是ServiceWorker
脚本所在目录。
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<ServiceWorkerRegistration>}
*/
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
到底能拦截多少种类型的请求:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="index.css">
</head>
<body>
<!-- 加载外部js,axios -->
<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
;
缓存更新
上面我们已经缓存了我们需要资源,但是我们的资源是不会更新的,现在你可以修改一下index.css
,然后刷新页面,不管怎么刷新,你的页面都不会更新,这是因为我们的资源是缓存的,所以我们需要更新我们的缓存;
通常情况下,我们会在activate
事件中删除旧的缓存,然后在install
事件中缓存新的资源;
self.addEventListener('activate', function (event) {
event.waitUntil(
caches.keys().then(function (cacheNames) {
return Promise.all(
cacheNames.map(function (cacheName) {
if (cacheName !== 'my-cache') {
return caches.delete(cacheName);
}
})
);
})
);
});
如果你想看缓存在哪里,可以在Application
中的Cache Storage
中查看:
现在你可以修改index.css
,然后按ctrl + F5
刷新页面,然后再看看Cache Storage
中的my-cache
,你会发现index.css
已经更新了;
我的开发者面板是中文的,新版的谷歌浏览器已经支持中文了,你可以在开发者面板中,通过右上角
齿轮
图标进入修改。
实战
现在我们已经知道了如何缓存资源,上面也提到了通过fetch/xhr
等网络请求也可以被ServiceWorker
拦截,那么我还是���于之前文章的百万级数据渲染的例子来实现一下缓存,这样我们就可以实现离线访问了;
首先我们需要在install
事件中缓存我们的数据:
self.addEventListener('install', function (event) {
event.waitUntil(
caches.open('my-cache').then(function (cache) {
return cache.addAll([
'/',
'/index.html',
'/index.css',
'/index.js',
'/getData'
]);
})
);
});
我们在之前的基础上,添加了/getData
的一个地址,这里我使用了node.js
的express
框架,然后在/getData
中返回我们的数据,下面是我的node.js
代码:
import express from 'express';
import path from "path"
const app = express();
app.use(express.json())
app.use(express.urlencoded({extended: false}))
const __dirname = path.resolve();
app.use('/', express.static(__dirname + '/public'));
app.listen(1701, async () => {
console.log('服务启动成功:http://localhost:1701');
})
app.all('/getData', (req, res) => {
// 百万级数据
const data = [];
for (var i = 0; i < 1000000; i++) {
data.push({
name: 'name' + i,
age: i
});
}
res.send(data);
})
process.on('uncaughtException', (e)=>{
console.error(e); // Error: uncaughtException
});
如果你不想使用
node.js
,那么可以使用一个静态的json
文件来代替也是一样的。
然后请求这个接口,正好我也引入了axios
,所以我就直接使用axios
来请求了:
axios.get('/getData').then(res => {
console.log(res.data);
})
当我们刷新页面后,我们的数据就已经成功缓存了,然后我们将浏览器设置为离线模式,然后刷新页面,我们就可以看到我们的数据了,这样我们就实现了离线访问了;
总结
ServiceWorker
是一个非常强大的功能,它可以帮助我们实现很多功能,比如缓存、离线访问、消息推送等等;
本章我们主要介绍了ServiceWorker
的基本使用,以及如何缓存资源,最后我们实现了一个离线访问的功能;
ServiceWorker
还有很多其他的功能都等着我们去探索,比如消息推送、后台同步等等;