「前置知识点」,只是做一个概念的介绍,不会做深度解释。因为,这些概念在下面文章中会有出现,为了让行文更加的顺畅,所以将本该在文内的概念解释放到前面来。「如果大家对这些概念熟悉,可以直接忽略」同时,由于阅读我文章的群体有很多,所以有些知识点可能「我视之若珍宝,尔视只如草芥,弃之如敝履」。以下知识点,请「酌情使用」。
创新互联是一家专注于成都网站建设、成都做网站与策划设计,孝昌网站建设哪家好?创新互联做网站,专注于网站建设十年,网设计领域的专业建站公司;建站业务涵盖:孝昌等地区。孝昌做网站价格咨询:18982081108
要查看正在运行的Service workers列表,我们可以在Chrome/Chromium中地址栏中输入chrome://serviceworker-internals/。
图片
chrome://xx 包含了很多内置的功能,这块也是有很大的说道的。后期,会单独有一个专题来讲。(已经在筹划准备中....)
Cache API为缓存的 Request / Response 对象对提供存储机制。例如,作为ServiceWorker 生命周期的一部分
Cache API像 workers 一样,是暴露在 window 作用域下的。尽管它被定义在 service worker 的标准中,但是它不必一定要配合 service worker 使用。
「一个域可以有多个命名 Cache 对象」。我们需要在脚本 (例如,在 ServiceWorker 中) 中处理缓存更新的方式。
缓存配额使用估算值,可以使用 StorageEstimate API 获得。
浏览器尽其所能去管理磁盘空间,但它有可能删除一个域下的缓存数据。
浏览器要么自动删除特定域的全部缓存,要么全部保留。
一些围绕service worker缓存的重要 API 方法包括:
Cache.put, Cache.add和Cache.addAll只能在GET请求下使用。
更多详情可以参考MDN-Cache[1]
如果我们以前没有使用过Cache接口,可能会认为它与 HTTP 缓存相同,或者至少与 HTTP 缓存相关。但实际情况并非如此。
可以将浏览器缓存看作是「分层的」。
Service workers是JavaScript层面的 API,「充当 Web 浏览器和 Web 服务器之间的代理」。它们的目标是通过提供离线访问以及提升页面性能来提高可靠性。
Service workers是对现有网站的增强。这意味着如果使用Service workers的网站的用户使用不支持Service workers的浏览器访问网站,基本功能不会受到破坏。它是向下兼容的。
Service workers通过类似于桌面应用程序的生命周期逐渐增强网站。想象一下当从应用商城安装APP时会发生流程:
Service worker也采用类似的生命周期,但采用「渐进增强」的方法。
Service worker技术中不可或缺的一部分是Cache API,这是一种「完全独立于 HTTP 缓存的缓存机制」。Cache API可以在Service worker作用域内和「主线程」作用域内访问。该特性为用户操作与 Cache 实例的交互提供了许多可能性。
这意味着可以根据网站的特有的逻辑来缓存网络请求的响应。例如:
这些都是缓存策略的应用方向。缓存策略使离线体验成为可能,并「通过绕过 HTTP 缓存触发的高延迟重新验证检查提供更好的性能」。
在「网络上传输数据本质上是异步的」。请求资产、服务器响应请求以及下载响应都需要时间。所涉及的时间是多样且不确定的。Service workers通过「事件驱动」的 API 来适应这种异步性,「使用回调处理事件」,例如:
都可以使用addEventListener API 注册事件。所有这些事件都可以与Cache API进行交互。特别是在网络请求是离散的,运行回调的能力对于「提供所期望的可靠性和速度」至关重要。
在JavaScript中进行异步工作涉及使用Promises。因为Promises也支持async和await,这些JavaScript特性也可用于简化Service worker代码,从而提供更好的开发者体验。
Service worker与Cache实例之间的交互涉及两个不同的缓存概念:
预缓存是需要提前缓存资源的过程,通常在Service worker「安装期间」进行。通过预缓存,「关键的静态资产和离线访问所需的材料可以被下载并存储在 Cache 实例中」。这种类型的缓存还可以提高需要预缓存资源的后续页面的页面速度。
运行时缓存是指在运行时从网络请求资源时应用缓存策略。这种类型的缓存非常有用,因为它保证了用户已经访问过的页面和资源的离线访问。
当在Service worker中使用这些方法时,可以为用户体验提供巨大的好处,并为普通的网页提供类似应用程序的行为。
Service workers与Web workers类似,它们的「所有工作都在自己的线程上进行」。这意味着Service workers的任务不会与主线程上的其他任务竞争。
我们就以Web Worker为例子,做一个简单的演示 在JavaScript中创建Web Worker并不是一项复杂的任务。
创建一个新的JavaScript文件,其中包含我们希望在工作线程中运行的代码。此文件不应包含对DOM的任何引用,因为它将无法访问DOM。
在我们的主JavaScript文件中,使用Worker构造函数创建一个新的Worker对象。此构造函数接受一个参数,即我们在第1步中创建的JavaScript文件的URL。
const worker = new Worker('worker.js');
为Worker对象添加事件侦听器,以处理主线程和工作线程之间发送的消息。onmessage事件处理程序用于处理从工作线程发送的消息,而postMessage方法用于向工作线程发送消息。
worker.onmessage = function(event) {
console.log('Worker said: ' + event.data);
};
worker.postMessage('Hello, worker!');
在我们的工作线程JavaScript文件中,添加一个事件侦听器,以处理从主线程发送的消息,使用self对象的onmessage属性。我们可以使用event.data属性访问消息中发送的数据。
self.onmessage = function(event) {
console.log('Main thread said: ' + event.data);
self.postMessage('Hello, main thread!');
};
现在让我们运行Web应用程序并测试Worker。我们应该在控制台中看到打印的消息,指示主线程和工作线程之间已发送和接收消息。
图片
在深入了解service worker的生命周期之前,我们先来了解一下与生命周期运作相关的「术语」(黑话)
了解service worker运作方式的关键在于理解「控制」(control)。
一个service worker的作用域由其「在 Web 服务器上的位置确定」。如果一个service worker在位于/A/index.html的页面上运行,并且位于/A/sw.js上,那么该service worker的作用域就是/A/。
作用域限制了service worker控制的页面。在上面的例子中,这意味着从/subdir/sw.js加载的service worker只能「控制位于/subdir/或其子页面中」。
控制页面的service worker仍然可以「拦截任何网络请求」,包括跨域资源的请求。作用域限制了由service worker控制的页面。
上述是默认情况下作用域工作的方式,但可以通过设置Service-Worker-Allowed响应头,以及通过向register方法传递作用域选项来进行覆盖。
除非有很好的理由将service worker的作用域限制为origin的子集,否则应「从 Web 服务器的根目录加载service worker,以便其作用域尽可能广泛」,不必担心Service-Worker-Allowed头部。
当说一个service worker正在控制一个页面时,实际上「是在控制一个客户端」。客户端是指URL位于该service worker作用域内的「任何打开的页面」。具体来说,这些是WindowClient的实例。
图片
为了使service worker能够控制页面,首先必须将其部署。
让我们看看一个没有service worker的网站到部署全新service worker时,中间发生了啥?
注册是service worker生命周期的「初始步骤」:
此代码在「主线程」上运行,并执行以下操作:
还有一些关键要点:
一旦注册完成,「安装」就开始了。
service worker在注册后触发其install事件。install「只会在每个service worker中调用一次,直到它被更新才会再次触发」。可以使用addEventListener在worker的作用域内注册install事件的回调:
// /sw.js
self.addEventListener("install", (event) => {
const cacheKey = "前端柒八九_v1";
event.waitUntil(
caches.open(cacheKey).then((cache) => {
// 将数组中的所有资产添加到'前端柒八九_v1'的`Cache`实例中以供以后使用。
return cache.addAll([
"/css/global.bc7b80b7.css",
"/css/home.fe5d0b23.css",
"/js/home.d3cc4ba4.js",
"/js/A.43ca4933.js",
]);
})
);
});
这会创建一个新的Cache实例并对资产进行「预缓存」。其中有一个event.waitUntil。event.waitUntil接受一个Promise,并等待该Promise被解决。
在这个示例中,这个Promise执行两个异步操作:
如果传递给event.waitUntil的Promise被「拒绝,安装将失败」。如果发生这种情况,service worker将被「丢弃」。
如果Promise被解决,安装成功,service worker的状态将更改为installed,然后进入「激活」阶段。
如果注册和安装成功,service worker将被「激活」,其状态将变为activating。在service worker的activate事件中可以进行激活期间的工作。在此事件中的一个典型任务是「清理旧缓存」,但对于「全新 service worker」,目前还不相关。
对于新的service worker,「安装成功后,激活会立即触发」。一旦激活完成,service worker的状态将变为activated。
默认情况下,新的service worker直到「下一次导航或页面刷新之前才会开始控制页面」。
一旦部署了第一个service worker,它很可能需要在以后进行更新。例如,如果请求处理或预缓存逻辑发生了变化,就可能需要进行更新。
浏览器会在以下情况下检查service worker的更新:
了解浏览器何时更新service worker很重要,但“如何”也很重要。假设service worker的URL或作用域未更改,「只有在其内容发生变化时,当前安装的service worker才会更新到新版本」。
浏览器以几种方式检测变化:
为确保浏览器能够可靠地检测service worker内容的变化,「不要使用 HTTP 缓存保留它,也不要更改其文件名」。当导航到service worker作用域内的新页面时,浏览器会自动执行更新检查。
关于更新,注册逻辑通常不应更改。然而,一个例外情况可能是「网站上的会话持续时间很长」。这可能在「单页应用程序」中发生,因为导航请求通常很少,应用程序通常在应用程序生命周期的开始遇到一个导航请求。在这种情况下,可以在「主线程上手动触发更新」:
navigator.serviceWorker.ready.then((registration) => {
registration.update();
});
对于传统的网站,或者在用户会话不持续很长时间的任何情况下,手动更新可能不是必要的。
当使用打包工具生成「静态资源」时,这些资源的「名称中会包含哈希值」,例如framework.3defa9d2.js。假设其中一些资源被预缓存以供以后离线访问,这将需要对service worker进行更新以预缓存新的资源:
self.addEventListener("install", (event) => {
const cacheKey = "前端柒八九_v2";
event.waitUntil(
caches.open(cacheKey).then((cache) => {
// 将数组中的所有资产添加到'前端柒八九_v2'的`Cache`实例中以供以后使用。
return cache.addAll([
"/css/global.ced4aef2.css",
"/css/home.cbe409ad.css",
"/js/home.109defa4.js",
"/js/A.38caf32d.js",
]);
})
);
});
与之前的install事件示例有两个方面不同:
更新后的service worker会与先前的service worker并存。这意味着旧的service worker仍然控制着任何打开的页面。刚才安装的新的service worker进入等待状态,直到被激活。
默认情况下,新的service worker将在「没有任何客户端由旧的service worker控制时激活」。这发生在相关网站的所有打开标签都关闭时。
当安装了新的service worker并结束了等待阶段时,它会被激活,并丢弃旧的service worker。在更新后的service worker的activate事件中执行的常见任务是「清理旧缓存」。通过使用caches.keys获取所有打开的 Cache 实例的key,并使用caches.delete删除不在允许列表中的所有旧缓存:
self.addEventListener("activate", (event) => {
// 指定允许的缓存密钥
const cacheAllowList = ["前端柒八九_v2"];
// 获取当前活动的所有`Cache`实例。
event.waitUntil(
caches.keys().then((keys) => {
// 删除不在允许列表中的所有缓存:
return Promise.all(
keys.map((key) => {
if (!cacheAllowList.includes(key)) {
return caches.delete(key);
}
})
);
})
);
});
旧的缓存不会自动清理。我们需要自己来做,否则可能会超过存储配额。
由于第一个service worker中的前端柒八九_v1已经过时,缓存允许列表已更新为指定前端柒八九_v2,这将删除具有不同名称的缓存。
「激活事件在旧缓存被删除后完成」。此时,新的service worker将控制页面,最终替代旧的service worker!
要有效使用service worker,有必要采用一个或多个缓存策略,这需要对Cache API有一定的了解。
缓存策略是service worker的fetch事件与Cache API之间的交互。如何编写缓存策略取决于不同情况。
缓存策略的另一个重要的用途就是与service worker的fetch事件配合使用。我们已经听说过一些关于「拦截网络请求」的内容,而service worker内部的fetch事件就是处理这种情况的:
// 建立缓存名称
const cacheName = "前端柒八九_v1";
self.addEventListener("install", (event) => {
event.waitUntil(caches.open(cacheName));
});
self.addEventListener("fetch", async (event) => {
// 这是一个图片请求
if (event.request.destination === "image") {
// 打开缓存
event.respondWith(
caches.open(cacheName).then((cache) => {
// 从缓存中响应图片,如果缓存中没有,就从网络获取图片
return cache.match(event.request).then((cachedResponse) => {
return (
cachedResponse ||
fetch(event.request.url).then((fetchedResponse) => {
// 将网络响应添加到缓存以供将来访问。
// 注意:我们需要复制响应以保存在缓存中,同时使用原始响应作为请求的响应。
cache.put(event.request, fetchedResponse.clone());
// 返回网络响应
return fetchedResponse;
})
);
});
})
);
} else {
return;
}
});
上面的代码执行以下操作:
fetch事件的事件对象包含一个request属性,其中包含一些有用的信息,可帮助我们识别每个请求的类型:
「异步操作是关键」。我们还记得install事件提供了一个event.waitUntil方法,它接受一个promise,并在激活之前等待其解析。fetch事件提供了类似的event.respondWith方法,我们可以使用它来返回异步fetch请求的结果或Cache接口的match方法返回的响应。
展示了从页面到service worker到缓存的流程。
「仅缓存」运作方式:当service worker控制页面时,「匹配的请求只会进入缓存」。这意味着为了使该模式有效,「任何缓存的资源都需要在安装时进行预缓存」,而「这些资源在service worker更新之前将不会在缓存中进行更新」。
// 建立缓存名称
const cacheName = "前端柒八九_v1";
// 要预缓存的资产
const preCachedAssets = ["/A.jpg", "/B.jpg", "/C.jpg", "/D.jpg"];
self.addEventListener("install", (event) => {
// 在安装时预缓存资产
event.waitUntil(
caches.open(cacheName).then((cache) => {
return cache.addAll(preCachedAssets);
})
);
});
self.addEventListener("fetch", (event) => {
const url = new URL(event.request.url);
const isPrecachedRequest = preCachedAssets.includes(url.pathname);
if (isPrecachedRequest) {
// 从缓存中获取预缓存的资产
event.respondWith(
caches.open(cacheName).then((cache) => {
return cache.match(event.request.url);
})
);
} else {
// 转到网络
return;
}
});
在上面的示例中,数组中的资产在安装时被预缓存。当service worker处理fetch请求时,我们「检查fetch事件处理的请求 URL 是否在预缓存资产的数组中」。
图片
「仅网络」的策略与「仅缓存」相反,它将请求通过service worker传递到网络,而「不与 service worker 缓存进行任何交互」。这是一种「确保内容新鲜度」的好策略,但其权衡是「当用户离线时将无法正常工作」。
要确保请求直接通过到网络,只需「不对匹配的请求调用 event.respondWith」。如果我们想更明确,可以在要传递到网络的请求的fetch事件回调中加入一个空的return;。这就是「仅缓存」策略演示中对于未经预缓存的请求所发生的情况。
图片
对于「匹配的请求」,流程如下:
// 建立缓存名称
const cacheName = "前端柒八九_v1";
self.addEventListener("fetch", (event) => {
// 检查这是否是一个图像请求
if (event.request.destination === "image") {
event.respondWith(
caches.open(cacheName).then((cache) => {
// 首先从缓存中获取
return cache.match(event.request.url).then((cachedResponse) => {
// 如果我们有缓存的响应,则返回缓存的响应
if (cachedResponse) {
return cachedResponse;
}
// 否则,访问网络
return fetch(event.request).then((fetchedResponse) => {
// 将网络响应添加到缓存以供以后访问
cache.put(event.request, fetchedResponse.clone());
// 返回网络响应
return fetchedResponse;
});
});
})
);
} else {
return;
}
});
尽管这个示例只涵盖了图像,但这是一个很好的范例,「适用于所有静态资产」(如CSS、JavaScript、图像和字体),「尤其是哈希版本的资产」。它「通过跳过 HTTP 缓存可能启动的任何与服务器的内容新鲜度检查,为不可变资产提供了速度提升」。更重要的是,「任何缓存的资产都将在离线时可用」。
它的含义就是:
这种策略对于HTML或 API 请求非常有用,当在线时,我们希望获取资源的最新版本,但希望在离线时能够访问最新可用的版本。
// 建立缓存名称
const cacheName = "前端柒八九_v1";
self.addEventListener("fetch", (event) => {
// 检查这是否是导航请求
if (event.request.mode === "navigate") {
// 打开缓存
event.respondWith(
caches.open(cacheName).then((cache) => {
// 首先通过网络请求
return fetch(event.request.url)
.then((fetchedResponse) => {
cache.put(event.request, fetchedResponse.clone());
return fetchedResponse;
})
.catch(() => {
// 如果网络不可用,从缓存中获取
return cache.match(event.request.url);
});
})
);
} else {
return;
}
});
在需要重视离线功能,但又需要平衡该功能与获取一些标记或 API 数据的最新版本的情况下,「网络优先,备用缓存」是一种实现这一目标的可靠策略。
图片
「陈旧时重新验证」策略是其中最复杂的。该策略的过程「优先考虑了资源的访问速度」,同时在后台保持其更新。该策略的工作流程如下:
这是一个适用于「需要保持更新但不是绝对必要的资源」的策略,比如网站的头像。它们会在用户愿意更新时进行更新,但不一定需要在每次请求时获取最新版本。
// 建立缓存名称
const cacheName = "前端柒八九_v1";
self.addEventListener("fetch", (event) => {
if (event.request.destination === "image") {
event.respondWith(
caches.open(cacheName).then((cache) => {
return cache.match(event.request).then((cachedResponse) => {
const fetchedResponse = fetch(event.request).then(
(networkResponse) => {
cache.put(event.request, networkResponse.clone());
return networkResponse;
}
);
return cachedResponse || fetchedResponse;
});
})
);
} else {
return;
}
});
如果将预缓存「应用于太多的资产」,或者如果Service Worker在页面「完成加载关键资产之前」就注册了,那么可能会遇到问题。
当Service Worker在「安装期间预缓存资产时,将同时发起一个或多个网络请求」。如果时机不合适,这可能会对用户体验产生问题。即使时机刚刚好,如果未对预缓存资产的「数量进行限制」,仍可能会浪费数据。
如果Service Worker预缓存任何内容,那么它的注册时机很重要。Service Worker通常使用内联的