WorkBox 之底层逻辑Service Worker(二)

简介: WorkBox 之底层逻辑Service Worker(二)

3. 激活(Activation)

如果注册安装成功,service worker将被激活,其状态将变为activating。在service workeractivate事件中可以进行激活期间的工作。在此事件中的一个典型任务是清理旧缓存,但对于全新 service worker,目前还不相关。

对于新的service worker安装成功后,激活会立即触发。一旦激活完成,service worker的状态将变为activated

默认情况下,新的service worker直到下一次导航或页面刷新之前才会开始控制页面


3.2 处理 service worker 的更新

一旦部署了第一个service worker,它很可能需要在以后进行更新。例如,如果请求处理或预缓存逻辑发生了变化,就可能需要进行更新。

更新发生的时机

浏览器会在以下情况下检查service worker的更新:

  1. 用户导航到service worker作用域内的页面。
  2. 调用navigator.serviceWorker.register()传入与当前安装的 service worker 不同的 URL
  3. 调用navigator.serviceWorker.register()传入与已安装的 service worker 相同的 URL,但具有不同的作用域

更新的方式

了解浏览器何时更新service worker很重要,但“如何”也很重要。假设service workerURL作用域未更改,只有在其内容发生变化时,当前安装的service worker才会更新到新版本

浏览器以几种方式检测变化

  1. importScripts请求的脚本的字节级更改
  2. service worker顶级代码的任何更改,这会影响浏览器生成的指纹。

为确保浏览器能够可靠地检测service worker内容的变化,不要使用 HTTP 缓存保留它,也不要更改其文件名。当导航到service worker作用域内的新页面时,浏览器会自动执行更新检查。

手动触发更新检查

关于更新,注册逻辑通常不应更改。然而,一个例外情况可能是网站上的会话持续时间很长。这可能在单页应用程序中发生,因为导航请求通常很少,应用程序通常在应用程序生命周期的开始遇到一个导航请求。在这种情况下,可以在主线程上手动触发更新

navigator.serviceWorker.ready.then((registration) => {
  registration.update();
});

对于传统的网站,或者在用户会话不持续很长时间的任何情况下,手动更新可能不是必要的。

安装(Installation)

当使用打包工具生成静态资源时,这些资源的名称中会包含哈希值,例如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事件示例有两个方面不同:

  1. 创建了一个具有 key前端柒八九_v2新 Cache 实例
  2. 预缓存资源的名称已更改。(/css/global.bc7b80b7.css变为/css/global.ced4aef2.css)

更新后的service worker会与先前的service worker并存。这意味着旧的service worker仍然控制着任何打开的页面。刚才安装的新的service worker进入等待状态,直到被激活。

默认情况下,新的service worker将在没有任何客户端由旧的service worker控制时激活。这发生在相关网站的所有打开标签都关闭时。

激活(Activation)

当安装了新的service worker并结束了等待阶段时,它会被激活,并丢弃旧的service worker。在更新后的service workeractivate事件中执行的常见任务是清理旧缓存。通过使用caches.keys获取所有打开的 Cache 实例的key,并使用caches.delete删除不在允许列表中的所有旧缓存:

// 建立缓存名称
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;
  }
});

旧的缓存不会自动清理。我们需要自己来做,否则可能会超过存储配额。

由于第一个service worker中的前端柒八九_v1已经过时,缓存允许列表已更新为指定前端柒八九_v2,这将删除具有不同名称的缓存。

激活事件在旧缓存被删除后完成。此时,新的service worker将控制页面,最终替代旧的service worker


4. Service worker 缓存策略

要有效使用service worker,有必要采用一个或多个缓存策略,这需要对Cache API有一定的了解。

缓存策略service workerfetch事件Cache API之间的交互。如何编写缓存策略取决于不同情况。

普通的 Fetch 事件

缓存策略的另一个重要的用途就是与service workerfetch事件配合使用。我们已经听说过一些关于拦截网络请求的内容,而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;
  }
});

上面的代码执行以下操作:

  • 检查请求的destination属性,以查看是否是图像请求。
  • 如果图像在service worker缓存中,则从缓存中提供它。如果没有,从网络获取图像,将响应存储在缓存中,并返回网络响应。
  • 所有其他请求都会通过service worker,不与缓存互动。

fetch事件事件对象包含一个request属性,其中包含一些有用的信息,可帮助我们识别每个请求的类型:

  • url,表示当前由 fetch 事件处理的网络请求的 URL
  • method,表示请求方法(例如GETPOST)。
  • mode,描述请求的模式。通常使用值navigate来区分对 HTML 文档的请求与其他请求。
  • destination,以一种避免使用所请求资产的文件扩展名的方式描述所请求内容的类型。

异步操作是关键。我们还记得install事件提供了一个event.waitUntil方法,它接受一个promise,并在激活之前等待其解析。fetch事件提供了类似的event.respondWith方法,我们可以使用它来返回异步fetch请求的结果或Cache接口match方法返回的响应。


缓存策略

1. 仅缓存(Cache only)

image.png

仅缓存运作方式:当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 是否在预缓存资产的数组中

  • 如果是,我们从缓存中获取资源,并跳过网络。
  • 其他请求将通过网络传递,只经过网络。

2. 仅网络(Network only)

image.png

仅网络的策略与仅缓存相反,它将请求通过service worker传递到网络,而不与 service worker 缓存进行任何交互。这是一种确保内容新鲜度的好策略,但其权衡是当用户离线时将无法正常工作

要确保请求直接通过到网络,只需不对匹配的请求调用 event.respondWith。如果我们想更明确,可以在要传递到网络的请求的fetch事件回调中加入一个空的return;。这就是仅缓存策略演示中对于未经预缓存的请求所发生的情况。

3. 缓存优先,备用网络(Cache first, falling back to network)

image.png

对于匹配的请求,流程如下:

  1. 请求到达缓存。如果资产在缓存中,就从缓存中提供。
  2. 如果请求不在缓存中,去访问网络。
  3. 一旦网络请求完成,将其添加到缓存,然后返回网络响应。
// 建立缓存名称
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;
  }
});

尽管这个示例只涵盖了图像,但这是一个很好的范例,适用于所有静态资产(如CSSJavaScript、图像和字体),尤其是哈希版本的资产。它通过跳过 HTTP 缓存可能启动的任何与服务器的内容新鲜度检查,为不可变资产提供了速度提升。更重要的是,任何缓存的资产都将在离线时可用


4. 网络优先,备用缓存(Network first, falling back to cache)

image.png

它的含义就是:

  1. 首先通过网络请求资源,然后将响应放入缓存。
  2. 如果以后离线了,就回退到缓存中的最新版本的响应

这种策略对于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;
  }
});
  • 首先,访问页面。可能需要在将 HTML 响应放入缓存之前重新加载。
  • 然后在开发者工具中,模拟离线连接,然后重新加载。
  • 最后一个可用版本将立即从缓存中提供。

在需要重视离线功能,但又需要平衡该功能与获取一些标记或 API 数据的最新版本的情况下,网络优先,备用缓存是一种实现这一目标的可靠策略。


5. 陈旧时重新验(Stale-while-revalidate)

image.png

陈旧时重新验证策略是其中最复杂的。该策略的过程优先考虑了资源的访问速度,同时在后台保持其更新。该策略的工作流程如下:

  1. 对于首次请求的资源,从网络获取,将其放入缓存,并返回网络响应。
  2. 对于后续请求,首先从缓存中提供资源,然后在后台重新从网络请求并更新资源的缓存条目。
  3. 对于以后的请求,我们将收到从网络获取并在前一步放入缓存的最新版本。

这是一个适用于需要保持更新但不是绝对必要的资源的策略,比如网站的头像。它们会在用户愿意更新时进行更新,但不一定需要在每次请求时获取最新版本。

// 建立缓存名称
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;
  }
});

5. Service Worker 预缓存的陷阱

如果将预缓存应用于太多的资产,或者如果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");
  });
}

考虑数据使用

无论时机如何,预缓存都涉及发送网络请求。如果不谨慎地选择要预缓存的资产清单,结果可能会浪费一些数据。

浪费数据是预缓存的一个潜在代价,但并非每个人都可以访问快速的互联网或无限的数据计划!在预缓存时,应考虑删除特别大的资产,并依赖于运行时缓存来捕捉它们,而不是进行假设用户都需要这些资源,从而全部都进行缓存。


6. 改进Service Worker开发体验

虽然Service Worker生命周期确保了可预测的安装和更新过程,但它可能使本地开发与常规开发有些不同。

本地开发的异常情况

通常情况下,Service WorkerAPI 仅在通过 HTTPS 提供的页面上可用,但是我们平时开发中,经常是通过 localhost 提供的页面进行严重。

此时,我们可以通过 chrome://flags/#unsafely-treat-insecure-origin-as-secure,并指定要将不安全的起源视为安全起源。

Service Worker开发辅助工具

迄今为止,测试Service Worker的最有效方法是依赖于无痕窗口,例如 Chrome 中的无痕窗口。每次打开无痕窗口时,我们都是从头开始的。没有活动Service Worker,也没有打开的缓存实例。这种测试的常规流程如下:

  1. 打开一个无痕浏览窗口。
  2. 转到注册了Service Worker的页面。
  3. 验证Service Worker是否按我们的预期工作。
  4. 关闭无痕窗口。
  5. 重复。

通过这个过程,我们模拟了Service Worker的生命周期。

Chrome DevTools 应用程序面板中提供的其他测试工具也可以帮助,尽管它们可能在某些方面修改了Service Worker的生命周期。

image.png

应用程序面板有一个名为Service Workers的面板,显示了当前页面的活动Service Worker。每个活动Service Worker都可以手动更新,甚至完全注销。面板顶部还有三个开关按钮,有助于开发。

  • Offline(离线):模拟离线条件。这有助于测试当前是否有活动Service Worker提供脱机内容。
  • Update on reload(重新加载时更新):当切换开启时,每次重新加载页面时都会重新获取并替换当前的Service Worker
  • Bypass for network(绕过网络):切换开启时,会绕过Service Worker的 fetch 事件中的任何代码,并始终从网络获取内容。

这些开关非常有帮助,特别是Bypass for network,当我们正在开发一个具有活动Service Worker的项目时,同时还希望确保体验在没有Service Worker的情况下也能按预期工作。


强制刷新

当在本地开发中使用活动的Service Worker,而不需要更新后刷新绕过网络功能时,按住 Shift 键并单击刷新按钮也非常有用。

这个操作的键盘变体涉及在 macOS 计算机上按住 ShiftCmdR 键。

这被称为强制刷新,它绕过 HTTP 缓存以获取网络数据。当Service Worker处于活动状态时,强制刷新也将完全绕过Service Worker

如果不确定特定缓存策略是否按预期工作,或者希望从网络获取所有内容以比较有Service Worker和无Service Worker时的行为,这个功能非常有用。更好的是,这是一个规定的行为,因此所有支持Service Worker的浏览器都会观察到它。

检查缓存内容

如果无法检查缓存,就很难确定缓存策略是否按预期工作。Chrome DevTools 的应用程序面板提供了一个子面板,用于检查缓存实例的内容。

image.png

这个子面板通过提供以下功能来使Service Worker开发变得更容易:

  • 查看缓存实例的名称。
  • 检查缓存资产的响应正文以及它们关联的响应标头。
  • 从缓存中清除一个或多个项目,甚至删除整个缓存实例。

这个图形用户界面使检查Service Worker缓存更容易,以查看项目是否已添加、更新或从Service Worker缓存中完全删除。


模拟存储配额

在拥有大量大型静态资产(如高分辨率图像)的网站中,可能会触及存储配额。当这种情况发生时,浏览器将从缓存中驱逐它认为过时或值得牺牲以腾出空间以容纳新资产的项目。

处理存储配额应该是Service Worker开发的一部分,而 Workbox 使这个过程比自行管理更简单。不管是否使用 Workbox,模拟自定义存储配额以测试缓存管理逻辑可能是一个不错的主意。

image.png

Chrome DevToolsApplication 面板中的存储使用查看器。在这里,正在设置自定义存储配额。

Chrome DevToolsApplication 面板有一个存储子面板,提供了有关页面使用的当前存储配额的信息。它还允许指定以兆字节为单位的自定义配额。一旦生效,Chrome 将执行自定义存储配额以进行测试。

这个子面板还包含一个清除站点数据按钮以及一整套相关的复选框,用于在单击按钮时清除哪些内容。其中包括任何打开的缓存实例,以及注销控制页面的任何活动Service Worker的能力。


后记

分享是一种态度

全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。

相关文章
|
3月前
|
存储 前端开发 Java
一篇文章带你搞懂Controller、Service等各层的功能与作用
本文将深入探讨这些controller.service等层的作用与功能,帮助读者更好地理解它们在软件开发中的重要性和运作原理。
548 0
|
8月前
|
存储 Web App开发 Android开发
Service Worker 在 PWA 中的应用
Service Worker 在 PWA 中的应用
61 0
|
2月前
|
存储 缓存 算法
关于 Service Worker 和 Web 应用对应关系的讨论
关于 Service Worker 和 Web 应用对应关系的讨论
18 0
|
5月前
|
存储 缓存 前端开发
WorkBox 之底层逻辑Service Worker(一)
WorkBox 之底层逻辑Service Worker(一)
|
8月前
|
缓存 JSON 自然语言处理
PWA 应用 Service Worker 缓存的一些可选策略和使用场景
PWA 应用 Service Worker 缓存的一些可选策略和使用场景
67 0
|
8月前
|
缓存 JavaScript 前端开发
在项目中使用Service Worker 与 PWA
在项目中使用Service Worker 与 PWA
49 1
|
12月前
|
存储 负载均衡 Kubernetes
简单说说K8S的Service底层,总感觉还是说不清楚。
简单说说K8S的Service底层,总感觉还是说不清楚。
151 0
|
域名解析 Kubernetes 负载均衡
k8s service 概念和原理
详细讲解k8s的概念和原理
718 0
k8s service 概念和原理
|
Android开发
Service和线程的区别
Service和线程都没有UI界面,都是运行于后台的服务程序,google为什么要为Android系统创建Service这个组件呢? 今天我就把自己的理解分享给大家
155 0
【Binder 机制】AIDL 分析 ( 创建 Service 服务 | 绑定 Service 远程服务 )
【Binder 机制】AIDL 分析 ( 创建 Service 服务 | 绑定 Service 远程服务 )
144 0