3. 激活(Activation
)
如果注册
和安装
成功,service worker
将被激活,其状态将变为activating
。在service worker
的activate
事件中可以进行激活期间的工作。在此事件中的一个典型任务是清理旧缓存,但对于全新 service worker,目前还不相关。
对于新的service worker
,安装成功后,激活会立即触发。一旦激活完成,service worker
的状态将变为activated
。
默认情况下,新的
service worker
直到下一次导航或页面刷新之前才会开始控制页面。
3.2 处理 service worker 的更新
一旦部署了第一个service worker
,它很可能需要在以后进行更新。例如,如果请求处理或预缓存逻辑发生了变化,就可能需要进行更新。
更新发生的时机
浏览器会在以下情况下检查service worker
的更新:
- 用户导航到
service worker
作用域内的页面。 - 调用
navigator.serviceWorker.register()
并传入与当前安装的 service worker 不同的 URL - 调用
navigator.serviceWorker.register()
并传入与已安装的 service worker 相同的 URL,但具有不同的作用域。
更新的方式
了解浏览器何时更新service worker
很重要,但“如何”也很重要。假设service worker
的URL
或作用域
未更改,只有在其内容发生变化时,当前安装的service worker
才会更新到新版本。
浏览器以几种方式检测变化
:
importScripts
请求的脚本的字节级更改。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
事件示例有两个方面不同:
- 创建了一个具有
key
为前端柒八九_v2
的新 Cache 实例。 - 预缓存资源的名称已更改。(
/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 worker
的activate事件
中执行的常见任务是清理旧缓存。通过使用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 worker
的fetch事件
与Cache API
之间的交互。如何编写缓存策略取决于不同情况。
普通的 Fetch 事件
缓存策略的另一个重要的用途就是与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; } });
上面的代码执行以下操作:
- 检查请求的
destination
属性,以查看是否是图像请求。 - 如果图像在
service worker
缓存中,则从缓存中提供它。如果没有,从网络获取图像,将响应存储在缓存中,并返回网络响应。 - 所有其他请求都会通过
service worker
,不与缓存互动。
fetch事件
的事件对象
包含一个request属性
,其中包含一些有用的信息,可帮助我们识别每个请求的类型:
url
,表示当前由fetch
事件处理的网络请求的URL
。method
,表示请求方法(例如GET
或POST
)。mode
,描述请求的模式。通常使用值navigate
来区分对HTML
文档的请求与其他请求。destination
,以一种避免使用所请求资产的文件扩展名的方式描述所请求内容的类型。
异步操作是关键。我们还记得install事件
提供了一个event.waitUntil
方法,它接受一个promise
,并在激活之前等待其解析。fetch事件
提供了类似的event.respondWith
方法,我们可以使用它来返回异步fetch请求
的结果或Cache接口
的match方法
返回的响应。
缓存策略
1. 仅缓存(Cache only
)
仅缓存运作方式:当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
)
仅网络的策略与仅缓存相反,它将请求通过service worker
传递到网络,而不与 service worker 缓存进行任何交互。这是一种确保内容新鲜度的好策略,但其权衡是当用户离线时将无法正常工作。
要确保请求直接通过到网络,只需不对匹配的请求调用 event.respondWith。如果我们想更明确,可以在要传递到网络的请求的fetch事件
回调中加入一个空的return
;。这就是仅缓存策略演示中对于未经预缓存的请求所发生的情况。
3. 缓存优先,备用网络(Cache first, falling back to network
)
对于匹配的请求,流程如下:
- 请求到达缓存。如果资产在缓存中,就从缓存中提供。
- 如果请求不在缓存中,去访问网络。
- 一旦网络请求完成,将其添加到缓存,然后返回网络响应。
// 建立缓存名称 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 缓存可能启动的任何与服务器的内容新鲜度检查,为不可变资产提供了速度提升。更重要的是,任何缓存的资产都将在离线时可用。
4. 网络优先,备用缓存(Network first, falling back to cache
)
它的含义就是:
- 首先通过网络请求资源,然后将响应放入缓存。
- 如果以后离线了,就回退到缓存中的最新版本的响应。
这种策略对于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
)
陈旧时重新验证策略是其中最复杂的。该策略的过程优先考虑了资源的访问速度,同时在后台保持其更新。该策略的工作流程如下:
- 对于
首次请求
的资源,从网络获取,将其放入缓存,并返回网络响应。 - 对于
后续请求
,首先从缓存中提供资源,然后在后台重新从网络请求并更新资源的缓存条目。 - 对于以后的请求,我们将收到从网络获取并在前一步放入缓存的最新版本。
这是一个适用于需要保持更新但不是绝对必要的资源的策略,比如网站的头像。它们会在用户愿意更新时进行更新,但不一定需要在每次请求时获取最新版本。
// 建立缓存名称 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 Worker
API 仅在通过 HTTPS
提供的页面上可用,但是我们平时开发中,经常是通过 localhost
提供的页面进行严重。
此时,我们可以通过 chrome://flags/#unsafely-treat-insecure-origin-as-secure
,并指定要将不安全的起源视为安全起源。
Service Worker
开发辅助工具
迄今为止,测试Service Worker
的最有效方法是依赖于无痕窗口
,例如 Chrome
中的无痕窗口。每次打开无痕窗口时,我们都是从头开始的。没有活动
的Service Worker
,也没有打开的缓存实例。这种测试的常规流程如下:
- 打开一个无痕浏览窗口。
- 转到注册了
Service Worker
的页面。 - 验证
Service Worker
是否按我们的预期工作。 - 关闭无痕窗口。
- 重复。
通过这个过程,我们模拟了Service Worker
的生命周期。
Chrome DevTools
应用程序面板中提供的其他测试工具也可以帮助,尽管它们可能在某些方面修改了Service Worker
的生命周期。
应用程序面板有一个名为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
计算机上按住 Shift
、Cmd
和 R
键。
这被称为强制刷新,它绕过 HTTP 缓存以获取网络数据。当Service Worker
处于活动状态时,强制刷新也将完全绕过Service Worker
。
如果不确定特定缓存策略是否按预期工作,或者希望从网络获取所有内容以比较有Service Worker
和无Service Worker
时的行为,这个功能非常有用。更好的是,这是一个规定的行为,因此所有支持Service Worker
的浏览器都会观察到它。
检查缓存内容
如果无法检查缓存,就很难确定缓存策略是否按预期工作。Chrome DevTools
的应用程序面板提供了一个子面板,用于检查缓存实例的内容。
这个子面板通过提供以下功能来使Service Worker
开发变得更容易:
- 查看缓存实例的名称。
- 检查缓存资产的响应正文以及它们关联的响应标头。
- 从缓存中清除一个或多个项目,甚至删除整个缓存实例。
这个图形用户界面使检查Service Worker
缓存更容易,以查看项目是否已添加、更新或从Service Worker
缓存中完全删除。
模拟存储配额
在拥有大量大型静态资产(如高分辨率图像)的网站中,可能会触及存储配额。当这种情况发生时,浏览器将从缓存中驱逐它认为过时或值得牺牲以腾出空间以容纳新资产的项目。
处理存储配额应该是Service Worker
开发的一部分,而 Workbox
使这个过程比自行管理更简单。不管是否使用 Workbox,模拟自定义存储配额以测试缓存管理逻辑可能是一个不错的主意。
Chrome DevTools
的 Application
面板中的存储使用查看器。在这里,正在设置自定义存储配额。
Chrome DevTools
的 Application
面板有一个存储子面板,提供了有关页面使用的当前存储配额的信息。它还允许指定以兆字节为单位的自定义配额。一旦生效,Chrome
将执行自定义存储配额以进行测试。
这个子面板还包含一个清除站点数据
按钮以及一整套相关的复选框,用于在单击按钮时清除哪些内容。其中包括任何打开的缓存实例,以及注销控制页面的任何活动Service Worker
的能力。
后记
分享是一种态度。
全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。