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才能使用它,因为它实在强的有点离谱.因此,可以看到我们用trycatch包裹注册事件,保证即使不是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
15self.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的文章?可能到那时候再讲讲好了🦖。
能写这么多哥们也要累死了😓