「前置知识点」,只是做一个概念的介绍,不会做深度解释。因为,这些概念在下面文章中会有出现,为了让行文更加的顺畅,所以将本该在文内的概念解释放到前面来。「如果大家对这些概念熟悉,可以直接忽略」同时,由于阅读我文章的群体有很多,所以有些知识点可能「我视之若珍宝,尔视只如草芥,弃之如敝履」。以下知识点,请「酌情使用」。
要查看正在运行的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生命周期的「初始步骤」:
<script> // 直到页面完全加载后再注册service worker window.addEventListener("load", () => { // 检查service worker是否可用 if ("serviceWorker" in navigator) { navigator.serviceWorker .register("/sw.js") .then(() => { console.log("Service worker 注册成功!"); }) .catch((error) => { console.warn("注册service worker时发生错误:"); console.warn(error); }); } });</script>
此代码在「主线程」上运行,并执行以下操作:
还有一些关键要点:
一旦注册完成,「安装」就开始了。
service worker在注册后触发其install事件。install「只会在每个service worker中调用一次,直到它被更新才会再次触发」。可以使用addEventListener在worker的作用域内注册install事件的回调:
// /sw.jsself.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通常使用内联的<script>元素注册。这意味着 HTML 解析器可能在页面的关键资产加载完成之前就发现了Service Worker的注册代码。
这是一个问题。Service Worker在最坏的情况下应该对性能没有不利影响,而不是使性能变差。为用户着想,应该在「页面加载事件」触发时注册Service Worker。这减少了预缓存可能干扰加载页面的关键资产的机会,从而意味着页面可以更快地实现交互,而无需处理后来可能不需要的资产的网络请求。
if ("serviceWorker" in navigator) { window.addEventListener("load", function () { navigator.serviceWorker.register("/service-worker.js"); });}
无论时机如何,「预缓存都涉及发送网络请求」。如果不谨慎地选择要预缓存的资产清单,结果可能会浪费一些数据。
「浪费数据是预缓存的一个潜在代价」,但并非每个人都可以访问快速的互联网或无限的数据计划!「在预缓存时,应考虑删除特别大的资产,并依赖于运行时缓存来捕捉它们」,而不是进行假设用户都需要这些资源,从而全部都进行缓存。
虽然Service Worker生命周期确保了可预测的安装和更新过程,但它可能使本地开发与常规开发有些不同。
通常情况下,Service WorkerAPI 仅在通过 HTTPS 提供的页面上可用,但是我们平时开发中,经常是通过 localhost 提供的页面进行严重。
此时,我们可以通过 chrome://flags/#unsafely-treat-insecure-origin-as-secure,并指定要将不安全的起源视为安全起源。
迄今为止,测试Service Worker的最有效方法是依赖于无痕窗口,例如 Chrome 中的无痕窗口。每次打开无痕窗口时,我们都是从头开始的。没有活动的Service Worker,也没有打开的缓存实例。这种测试的常规流程如下:
通过这个过程,我们模拟了Service Worker的生命周期。
Chrome DevTools 应用程序面板中提供的其他测试工具也可以帮助,尽管它们可能在某些方面修改了Service Worker的生命周期。
图片
应用程序面板有一个名为Service Workers的面板,显示了当前页面的活动Service Worker。每个活动Service Worker都可以手动更新,甚至完全注销。面板顶部还有三个开关按钮,有助于开发。
这些开关非常有帮助,特别是Bypass for network,当我们正在开发一个具有活动Service Worker的项目时,同时还希望确保体验在没有Service Worker的情况下也能按预期工作。
当在本地开发中使用活动的Service Worker,而不需要更新后刷新或绕过网络功能时,按住 Shift 键并单击刷新按钮也非常有用。
这个操作的键盘变体涉及在 macOS 计算机上按住 Shift、Cmd 和 R 键。
这被称为「强制刷新」,它绕过 HTTP 缓存以获取网络数据。当Service Worker处于活动状态时,强制刷新也将完全绕过Service Worker。
如果不确定特定缓存策略是否按预期工作,或者希望从网络获取所有内容以比较有Service Worker和无Service Worker时的行为,这个功能非常有用。更好的是,这是一个规定的行为,因此所有支持Service Worker的浏览器都会观察到它。
如果无法检查缓存,就很难确定缓存策略是否按预期工作。Chrome DevTools 的应用程序面板提供了一个子面板,用于检查缓存实例的内容。
在DevTools中检查缓存
这个子面板通过提供以下功能来使Service Worker开发变得更容易:
这个图形用户界面使检查Service Worker缓存更容易,以查看项目是否已添加、更新或从Service Worker缓存中完全删除。
在拥有大量大型静态资产(如高分辨率图像)的网站中,可能会触及存储配额。当这种情况发生时,浏览器将从缓存中驱逐它认为过时或值得牺牲以腾出空间以容纳新资产的项目。
处理存储配额应该是Service Worker开发的一部分,而 Workbox 使这个过程比自行管理更简单。不管是否使用 Workbox,模拟自定义存储配额以测试缓存管理逻辑可能是一个不错的主意。
存储使用查看器
Chrome DevTools 的 Application 面板中的存储使用查看器。在这里,正在设置自定义存储配额。
Chrome DevTools 的 Application 面板有一个存储子面板,提供了有关页面使用的当前存储配额的信息。它还允许指定以兆字节为单位的自定义配额。一旦生效,Chrome 将执行自定义存储配额以进行测试。
这个子面板还包含一个清除站点数据按钮以及一整套相关的复选框,用于在单击按钮时清除哪些内容。其中包括任何打开的缓存实例,以及注销控制页面的任何活动Service Worker的能力。
本文链接://www.dmpip.com//www.dmpip.com/showinfo-26-16137-0.htmlWorkBox 之底层逻辑Service Worker
声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com
上一篇: JVM类加载机制分析