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
到底能拦截多少种类型的请求:
html复制代码<!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
还有很多其他的功能都等着我们去探索,比如消息推送、后台同步等等,这些功能我们后面会陆续介绍;
转自:https://juejin.cn/post/7165893682132959245