Service Worker
Service Worker🐳
Web Worker
大家已经很熟悉了吧,那我今天来讲讲它的好兄弟Service Worker
。哥们也是最近两天才知道这个玩意,这几天玩下来感觉这玩意要比
Web Worker
实用一万倍
1. 啥是Service Worker
?👹
以下内容来自
MDN
:
- Service worker 本质上充当 Web 应用程序、浏览器与网络(可用时)之间的代理服务器。这个 API 旨在创建有效的离线体验,它会 **拦截网络请求 **并根据网络是否可用来采取适当的动作、更新来自服务器的的资源。它还提供入口以推送通知和访问后台同步 API。
说了这么一大堆,个人觉得最核心的还是: 拦截网络请求
😶🌫️。
如果大家用过axios
的interceptor
,那会感觉这俩玩意是出奇的相似🙊,(而且好像真能用ServiceWorker
模拟interceptor
?虽然很邪门就是了)
2.会拦截网络请求,有什么用?🦄
- 请求封装:首先,正如上面所说的,它可以像
interceptor
一样工作,那就可以给咱们每个请求都自动的加上token
之类的,虽然其他方法也能做,但这样真的很酷😎,当然,这也不是人家的主要业务 - 拦截网络请求:当没有网或者网络很差的时候,可以选择不发送网路请求,直接采用缓存内容,保证用户看到的不是一片惨白或者是只小恐龙🦖
- 导航预加载:当用户选择跳转页面的时候,也可以利用它率先
preLoad
下一个页面,或者直接采用预设好的页面,这有个响亮的名字: 导航预加载 - 推送通知:
Service Worker
能够接收来自服务器的推送通知,并在应用不在前台运行时向用户显示这些通知。这对于保持用户参与度和及时传递信息非常有用。(有api
) - 网络请求代理:
Service Worker
可以代理网络请求,实现如数据压缩、图片优化、安全策略加强等功能。
3. Service Worker工作流程👻
- 获取 service worker 代码,然后使用
serviceWorkerContainer.register()
来注册。如果成功,service worker 将在ServiceWorkerGlobalScope
中执行;这本质上是一种特殊的上下文,在主脚本执行线程之外运行,没有访问 DOM 的权限。Service Worker 现在已为处理事件做好准备。 - 安装完成。
install
事件始终是发送给 service worker 的第一个事件(这可用于启动填充 IndexedDB 和缓存站点资源的过程)。在此步骤期间,应用程序正在为离线可用做准备。 - 当
install
程序处理完成时,service worker 被视为已安装。此时,service worker 的先前版本可能处于激活的状态并控制着打开的页面。由于我们不希望同一 service worker 的两个不同版本同时运行,因此新版本尚未激活。 - 一旦 service worker 的旧版本控制的页面都已关闭,就可以安全地停用旧版本,并且新安装的 service worker 将收到
activate
事件。activate
的主要用途是去清理 service worker 之前版本使用的资源。新的 service worker 可以调用skipWaiting()
要求立即激活,而无需要求打开的页面关闭。然后,新的 service worker 将立即收到activate
事件,并将接管任何打开的页面。 - 激活后,service worker 将立即控制页面,但是只会控制那些在
register()
成功后打开的页面。换句话说,文档必须重新加载才能真正的受到控制,因为文档在有或者没有 service worker 的情况下开始存在,并在其生命周期内维护它。为了覆盖次默认行为并在页面打开的情况下,service worker 可以调用clients.claim()
方法。 - 每当获取新版本的 service worker 时,都会再次发生此循环,并在新版本的激活期间清理上一个版本的残留。
同一个worker的不同版本:
这不能说是
Service Worker
的特性,反而是Cache
的特性。 下面会讲到,Cache
是Service Worker
的好哥们,Cache
可以用Open(version)
来选择使用哪个版本的缓存,而Service Worker
也会根据这个衍生出不同版本,而我们不想要多个版本的 worker 同时存在,因此worker
的生命周期也比较特别 🤯
4. 来看看如何用它提升用户体验吧🤣
其实主要也就一点:
- 当缓存里有数据时,不应该再次请求,而是直接使用缓存内容,加快页面响应速度
而提到缓存,大家首先想到的是什么?可能是indexDB
,也可能是localStorage
,但是Service Worker
用的是另一种叫做cache
的缓存
Cache
:
cache
是web api
的一部分,虽然也可以在Service Worker
外使用,但是它的主要优势还是缓存重要资源,比如图片、文件,因此经常和Service Worker
成对出现,或者说,就是为Service Worker
量身打造的api。关于
cache
,这里不再过多介绍,可以去:cache详解 看看
好,话不多说,咱们开干!⛽
哥们的文件目录(忽略webworker):
跟
Web Worker
一样,都得在主js
中注册worker
,同样的,它也有messageChannel
,用于和主线程通信1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20const registerServiceWorker = async () => {
if("serviceWorker" in navigator) {
try {
const serviceWorkerRegistration = await navigator.serviceWorker.register("../serviceWorker.js", {
scope: "/webworker/public/"
});
const statusGroup = ["installing", "waiting", "active"];
console.group("status")
statusGroup.forEach((item) => {
serviceWorkerRegistration[item] && console.log(item);
})
console.groupEnd()
} catch (err) {
console.error(`registration failed: ${err}`);
}
} else {
console.error("service worker is forbidden")
}
}
registerServiceWorker();哦对了,它和
Web Worker
很大的不同就是它没得自己的构造函数,它只是一个挂载在navigation
下面的小特性🤐而且只有https
或者localhost
才能使用它,因为它实在强的有点离谱.因此,可以看到我们用try
catch
包裹注册事件,保证即使不是https
协议也可以正常访问网站之所以不用
http
是因为,万一哪天这玩意被劫持了,后果不堪设想🙈根据你上面填的路径,搞个
serviceWorker.js
出来😇😇我喜欢把这个
serviceWorker.js
当成纯纯的入口文件,当然,你们要全写在这一个文件里也不是不行,但是那样我觉得不清晰。以下是我的serviceWorker.js
,里面主要是监听事件:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27importScripts("./workers/installServiceWorker.js")
importScripts("./workers/cacheServiceWorker.js")
self.addEventListener("install", (event) => {
event.waitUntil(
Promise.all([
addResourcesToCache([
"/webworker/public/html/index.html",
"/webworker/public/css/index.css",
"/webworker/public/js/serviceWorker.js",
"/webworker/public/workers/installServiceWorker.js",
"/webworker/public/workers/cacheServiceWorker.js",
"/webworker/public/assets/3.jpg",
]),
self.registration.navigationPreload.enable()
])
);
});
self.addEventListener("fetch", (event) => {
event.respondWith(
cacheFirstWithFallback({
request: event.request,
preloadResponsePromise: event.preloadResponse,
fallbackUrl: "/webworker/public/assets/3.jpg",
}),
);
});大家有空也可以看看 Martin Fowler 的 重构:改善既有代码的设计(超级好看🤪),哥们的这种方式也是他的设计理念
Service Worker
也是有管理范围的!可以通过
scope
指定范围,但是它的范围只能是跟自己同级或者下级的文件,自己的上级文件就搞不到了,因此我们把serviceWorker.js
放在根目录里,也就是为什么第一点的路径是”../serviceWorker.js”然后创建一个
installServiceWorker.js
,用于存放跟install
相关的函数和配置,大致如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17const addResourcesToCache = async (resources) => {
const cache = await caches.open("v1");
await cache.addAll(resources)
}
self.addEventListener("install", (event) => {
event.waitUntil(
addResourcesToCache([
"/webworker/public/html/index.html",
"/webworker/public/css/index.css",
"/webworker/public/js/serviceWorker.js",
"/webworker/public/workers/installServiceWorker.js",
"/webworker/public/workers/cacheServiceWorker.js",
"/webworker/public/workers/3.jpg",
]),
);
});其中
cache.addAll
是Cache
的一个方法,接受一个数组参数,通常是url数组
,并把它们全部存储到cache
的v1
中waitUntil
的作用是Service Worker
必须在该函数的Promise执行完之后才能继续,接下来会详细介绍,霸道!!路径一定要写全!🙃
到现在,咱们已经完成了
Service Worker
的挂载和缓存的下载了,咱们可以点开控制台,看看application
里的缓存空间
或者存储
,有没有缓存🤩如果顺利的话,应该可以看到如下条目:
存储搞定了,那接下来就是如何运用存储了,在这里,我在
workers
中新建了cacheServiceWorker.js
来处理利用缓存相关的逻辑:1
2
3
4
5
6
7
8
9
10
11
12
13
14const cacheFirst = async (request) => {
const cachedResponse = await caches.match(request);
if(cachedResponse) {
return cachedResponse
}
return fetch(request);
}
self.addEventListener("fetch", (event) => {
console.log(event)
event.respondWith(
cacheFirst(event.request),
);
});逻辑很简单对吧,就是首先在
caches
中寻找有无缓存过的条目,如果有,就直接返回缓存,如果没有,就返回fetch(request)
的结果顺便提一嘴,
cacheFirst
的request
是Request
类的实例,也就是咱们平时发请求带的对象。fetch
会自动把我们传的参数转换成这个对象,也可以直接传一个request
给fetch
,因此这里可以直接fetch(request)
👽而下面
eventListener
中的event
属于FetchEvent
对象,Service Worker
对象专属的api
,继承于Event
(如果对
Event
不是很了解可以去看我的上一篇:https://konodioda727.github.io/2023/11/26/addEventListener1/)它只在监听
fetch
事件时会产生,它有以下三大特性(包括我们上面讲的waitUntil
):- event.request:这是一个
Request
对象,包含了被拦截的网络请求的详细信息,如请求的 URL、请求头、请求方法等。 - **event.respondWith()**:这个方法允许你提供一个用于生成响应的 Promise。这是控制网络响应行为的关键方法。通过这个方法,你可以返回缓存中的响应、生成一个新的响应或者直接调用
fetch
来获取网络上的资源。 - **event.waitUntil()**:这个方法用于延长事件的生命周期。它对于执行一些额外的异步处理(如缓存更新)很有用。
那来试试看是什么效果嘞,刷新页面,打开控制台->网络,看到如下红框中的
(Service Worker)
就算成功啦!- event.request:这是一个
这样已经初具人形啦🤫,但是实际中还有许多异步加载的场景,我们要异步加载也能缓存下来,那就得使用更复杂的策略,我们不仅可以从网络中请求资源,还可以将其保存到缓存中,以便稍后对该资源的请求也可以离线检索🙀,做法其实也很简单,就在咱们刚才的基础上让经过请求得到的资源也存入
Cache
中即可,为此,我新加了putInCache
函数,并对cacheFirst
函数稍加修改:1
2
3
4
5
6
7
8
9
10
11
12
13const putInCache = async (request, response) => {
const cache = await caches.open("v1");
await cache.put(request, response);
};
const cacheFirst = async (request) => {
const cachedResponse = await caches.match(request);
if(cachedResponse) {
return cachedResponse
}
const NetworkResponse = await fetch(request);
await putInCache(request, NetworkResponse.clone());
return NetworkResponse;
}现在已经非常不错啦👾,但还需要思考一个问题: 没网怎么办?,初听可能会觉得很恼火😡,浏览器没网怎么玩?但是这也是
Service Worker
存在的原因之一,为了能让用户在没网的时候也有点东西看,我们就只能再次打磨cacheFirst
函数,让它在请求失败的时候,也能有一个fallback
可以使用:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38const cacheFirst = async ({ request, fallbackUrl }) => {
// 首先,尝试从缓存中获取资源
const responseFromCache = await caches.match(request);
if (responseFromCache) {
return responseFromCache;
}
// 然后尝试从网络中获取资源
try {
const responseFromNetwork = await fetch(request);
// 响应可能会被使用
// 我们需要将它的拷贝放入缓存
// 然后再返回该响应
putInCache(request, responseFromNetwork.clone());
return responseFromNetwork;
} catch (error) {
const fallbackResponse = await caches.match(fallbackUrl);
if (fallbackResponse) {
return fallbackResponse;
}
// 当回落的响应也不可用时,
// 我们便无能为力了,但我们始终需要
// 返回 Response 对象
return new Response("Network error happened", {
status: 408,
headers: { "Content-Type": "text/plain" },
});
}
};
self.addEventListener("fetch", (event) => {
event.respondWith(
cacheFirst({
request: event.request,
fallbackUrl: "/webworker/public/assets/3.jpg",
}),
);
});好了,这下彻底圆满了🤣,当我们打开控制台,关闭网络时,现在也不会有太多的变化,我们的目的也就达到了,当然,实际过程中肯定还要为不同情况搞不同的
fallback
,但这里意思意思就差不多了💩
5. 导航预加载🥸
如果启用了导航预加载功能,其将在发出 fetch 请求后,立即开始下载资源,并同时激活 service worker。这确保了在导航到一个页面时,立即开始下载,而不是等到 service worker 被激活。
当用户导航到一个新页面时,浏览器可以立即开始加载页面资源,而不需要等待
Service Worker
启动和激活。正常情况下,如果一个Service Worker
控制着网站,当用户点击链接或进行页面跳转时,浏览器首先需要启动并激活Service Worker
,然后Service Worker
才会处理页面的网络请求。这可能会引入一些延迟
这种延迟发生的次数相对较少,但是一旦发生就不可避免,而且可能很重要。
首先,必须在 service worker 激活期间使用 registration.navigationPreload.enable()
来启用该功能:
1 |
|
MDN
上说必须在activate
里激活,但是哥们实测install
也可以,毕竟install
是第一个事件,理论上activate
中干的在它里边一样可以.因此,我在
installServiceWorker.js
中写了如下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
self.addEventListener("install", (event) => {
event.waitUntil(
Promise.all([
addResourcesToCache([
"/webworker/public/html/index.html",
"/webworker/public/css/index.css",
"/webworker/public/js/serviceWorker.js",
"/webworker/public/workers/installServiceWorker.js",
"/webworker/public/workers/cacheServiceWorker.js",
"/webworker/public/assets/3.jpg",
]),
self.registration.navigationPreload.enable()
])
);
});只是增加了
Promise.all()
,跟原来的变化不大
然后使用 event.preloadResponse
等待预加载的资源在 fetch
事件处理程序中完成下载。
继续前几节的示例,我们插入代码,以便在缓存检查后等待预加载的资源,如果失败,则再从网络中获取。
新流程是:
- 检查缓存
- 等待
event.preloadResponse
,它作为preloadResponsePromise
传递给cacheFirst()
函数。如果返回结果,则缓存它。 - 如果两者均没有结果,那么我们就通过
Service Worker
从网络中获取。
preload
主要处理的是导航请求,其过程完全由浏览器操纵,不必担心,原理就是当遇到导航请求时,会不等待Service Worker
,绕过Service Worker
发送请求,接收到请求就返回到preloadResponse
中
再对着cacheFirstWithFallback
缝缝补补,加上preload
的逻辑
1 |
|
6. 更新Service Worker
🥳
之前提到过,浏览器不允许**不同版本的相同Service Worker
**同时运行,为此,Service Worker
的生命周期就显得很奇怪:
- 如果你的
service worker
已经被安装,但是刷新页面时有一个新版本的可用,新版的service worker
会在后台安装,但是仍然不会被激活。当不再有任何已加载的页面在使用旧版的service worker
的时候,新版本才会激活。一旦再也没有这样的已加载的页面,新的service worker
就会被激活。
如果我把cache的版本改成v2
,那么这个service worker
跟之前的就不一样了,这时,在install
中后台会默默下载这个v2
的worker,当没有页面在使用之前的版本的时候,这个新的 service worker 就会激活并开始响应请求。🫨
当你更新 service worker 到一个新的版本,你将在它的 install
事件处理程序中创建一个新的缓存。在仍有由上一个 worker 的版本控制的打开的页面,你就需要同时保留这两个版本的缓存,因为之前的版本需要它缓存的版本。你可以使用 activate
事件从之前的缓存中移除数据。
传给 waitUntil()
的 promise 会阻塞其他的事件,直到它完成,因此你可以放心,当你在新的 service worker 中得到你的第一个 fetch
事件时,你的清理操作已经完成。
1 |
|
7.如何处理过期资源?😓
遗憾的是,Service Worker
并没有检测过期资源的api
,那就只能靠咱们自己手搓了,我的想法就是给cache
加一条timeStamp
,或者maxAge
,
如果缓存时间大于maxAge
就清除资源并重新请求缓存(这不就http吗?)
还是老规矩,再对cacheServiceWorker
缝缝补补,这次修改的日志:
- 新增了一个用于删除过期资源的
deleteCache
函数 - 新增了一个用于判断是否过期的
isResourceExpired
函数 - 对
putInCache
以及cacheServiceWorker
添加了删除过期缓存的逻辑
修改完毕后大概长这样🤔:
1 |
|
8.终于差不多了🥳
到这里,Service Worker
的基本内容已经结束了,但还有些worker
共有的属性或者方法,比如: messageChannel
(超级好用,还可以用来组件间通信)因为太懒就没有讲,之后可能还会有Web Worker
的文章?可能到那时候再讲讲好了🦖。
能写这么多哥们也要累死了😓