Get to know MDN better
此页面由社区从英文翻译而来。了解更多并加入 MDN Web Docs 社区。
本文是关于使用 service worker 的教程,包括讲解 service worker 的基本架构、怎么注册 service worker、新的 service worker 的安装以及激活的过程、怎么更新 service worker 还有它的缓存控制和自定义响应,这一切都在一个简单的离线的应用程序中。
有一个困扰 web 用户多年的难题——丢失网络连接。即使是世界上最好的 web app,如果下载不了它,也是非常糟糕的体验。如今虽然已经有各种尝试来创造技术去尝试着解决这个问题,并且其中一些问题已经被解决。但是,最重要的问题是,仍然没有一个好的统筹机制对资源缓存和自定义的网络请求进行控制。
Service worker 修复了这个问题。使用 service worker,你可以将 app 设置为首先使用缓存资源,从而即使在离线状态,也可以提供默认的体验,然后从网络获取更多数据(通常称为“离线优先”)。这已经在原生 app 中可用,这是经常选择原生 app,而不是选择 web app 的主要原因之一。
service worker 的功能类似于代理服务器,允许你去修改请求和响应,将其替换成来自其自身缓存的项目。
service worker 在现代浏览器中默认开启。要使用 service worker 运行代码,你需要通过 HTTPS 提供你的代码——出于安全原因,Service worker 仅限在 HTTPS 上运行。支持 HTTPS 的服务器是必要的。为了托管实验代码,你可以使用 Github、Netlify、Vercel 等服务。为了促进本地开发,浏览器也认为 localhost 是一个安全的来源。
通常遵循以下基本步骤来使用 service worker:
以下是可用的 service worker 事件的摘要:
为了展示注册和安装 service worker 的基础知识,我们已经创建了一个名为简单 service worker 的演示,这是一个乐高的星球大战图像库。它使用 promise 驱动的函数从 JSON 对象读取图像数据,并使用 fetch() 加载图像,然后将图像显示在页面的下一行。我们暂时让它保持不变。它同时也注册、安装和激活 service worker。
你也可以在 GitHub 上查看源代码以及简单 service worker 的在线演示。
在我们 app 的 JavaScript 文件(app.js)的第一个代码块中如下所示。这是我们使用 service worker 的入口点。
这就注册了一个 service worker,它工作在 worker 上下文,所以没有访问 DOM 的权限。
单个 service worker 可以控制很多页面。每个你的作用域(scope)里的页面加载完的时候,安装在页面的 service worker 就可以控制它。牢记你需要小心 service worker 脚本里的全局变量:每个页面不会有自己独有的 worker。
备注:关于 service worker 一个很棒的事情就是,如果你像我们上面做的那样使用特性检测,发现浏览器并不支持 service worker,但是它还是可以正常地以预期的方式在线使用你的 app。
可能是如下的原因:
在你的 service worker 注册之后,浏览器会尝试为你的页面或站点安装并激活它。
install 事件会在注册成功完成之后触发。install 事件通常会这样用,将离线运行 app 产生的资源放置在浏览器离线缓存的空间。为了实现这个,我们使用了 Service Worker 的存储 API——cache——一个 service worker 上的全局对象,它使我们可以存储网络响应发来的资源,并且根据它们的请求来生成 key。这个 API 和浏览器的标准的缓存工作原理很相似,但它特定于你的域的。直到你清理它们之前,这些内容都会持久存在。
以下是我们的 service worker 如何处理 install 事件:
备注:Web Storage API(localStorage)跟 service worker 的 cache 工作原理十分类似,但是它是同步的,所以不允许在 service worker 中使用。
备注:如果你需要的话,可以在 service worker 中使用 IndexedDB 来做数据存储。
现在你已经将你的站点资源缓存了,你需要告诉 service worker 让它用这些缓存内容来做点什么。有了 fetch 事件,这是很容易做到的。
每次获取 service worker 控制的资源时,都会触发 fetch 事件,这些资源包括了指定的作用域内的文档,和这些文档内引用的其他任何资源(比如 index.html 发起了一个跨源的请求来嵌入一个图片,这个也会通过 service worker)。
你可以给 service worker 添加一个 fetch 的事件监听器,接着调用 event 上的 respondWith() 方法来劫持我们的 HTTP 响应,然后你用可以用自己的方法来更新它们。
在任何情况下,我们会首先响应缓存的 URL 和网络请求的 URL 相匹配的资源:
caches.match(event.request) 允许我们对网络请求里的每个资源与缓存里可获取的等效资源进行匹配,查看缓存中是否有相应的资源。该匹配通过 URL 和各种标头进行,就像正常的 HTTP 请求一样。
在 service worker 的缓存中存在相匹配的资源时,caches.match(event.request) 是非常棒的。但是如果没有匹配资源呢?如果我们不提供任何错误处理,promise 就会兑现 undefined,因而我们不会得到任何内容。
在测试缓存的响应后,我们可以退回到常规网络请求:
如果资源不存在缓存中,它们则会从网络中进行请求。
使用更复杂的策略,我们不仅可以从网络中请求资源,还可以将其保存到缓存中,以便稍后对该资源的请求也可以离线检索。这意味着,如果将额外的图像添加到星球大战图库中,我们的 app 可以自动抓取并缓存它们。以下片段实现了这样的策略:
如果请求 URL 在缓存中不可用,我们将使用 await fetch(request) 从网络请求中请求资源。之后,我们将响应的克隆放入缓存。putInCache() 函数使用 caches.open('v1') 和 cache.put() 将资源增加到缓存中。它的原始响应会返回给浏览器以提供给调用它的页面。
克隆响应是必要的,因为请求和响应流仅可以读取一次。为了返回响应到浏览器,并将其放入缓存中,我们得克隆它。因此原始的资源会返回给浏览器,克隆的资源会发送到缓存。它们都只能被读取一次。
看起来有点奇怪的是,putInCache() 返回的 promise 并没有使用 await。但原因是,我们并不想要等到缓存被添加至缓存后再返回响应。
我们现在唯一的问题是当请求没有匹配到缓存中的任何资源,或网络不可用的时,我们的请求依然会失败。让我们提供一个默认的回退方案以便不管发生了什么,用户至少能得到些东西:
我们选择了回落的图片,因为唯一的更新是对新图片的,它可能会失败,因为其他的所有内容都依赖于我们之前看到的 install 事件监听器中的安装过程。
如果启用了导航预加载功能,其将在发出 fetch 请求后,立即开始下载资源,并同时激活 service worker。这确保了在导航到一个页面时,立即开始下载,而不是等到 service worker 被激活。这种延迟发生的次数相对较少,但是一旦发生就不可避免,而且可能很重要。
首先,必须在 service worker 激活期间使用 registration.navigationPreload.enable() 来启用该功能:
然后使用 event.preloadResponse 等待预加载的资源在 fetch 事件处理程序中完成下载。
继续前几节的示例,我们插入代码,以便在缓存检查后等待预加载的资源,如果失败,则再从网络中获取。
新流程是:
注意,在此示例中,无论资源是“正常”下载还是预加载,我们都会下载和缓存相同的数据。相反,你可以选择在预加载时下载和缓存其他资源。请参阅 NavigationPreloadManager > 自定义响应 以了解详情。
如果你的 service worker 已经被安装,但是刷新页面时有一个新版本的可用,新版的 service worker 会在后台安装,但是仍然不会被激活。当不再有任何已加载的页面在使用旧版的 service worker 的时候,新版本才会激活。一旦再也没有这样的已加载的页面,新的 service worker 就会被激活。
备注:可以通过使用 Clients.claim() 绕过这一点。
你想把你的新版的 service worker 里的 install 事件监听器改成下面这样(注意新的版本号):
当安装发生的时候,前一个版本依然在响应请求。新的版本正在后台安装。我们调用了一个新的缓存 v2,所以前一个 v1 版本的缓存不会被扰乱。
当没有页面在使用之前的版本的时候,这个新的 service worker 就会激活并开始响应请求。
正如我们在最后一节看到的那样,当你更新 service worker 到一个新的版本,你将在它的 install 事件处理程序中创建一个新的缓存。在仍有由上一个 worker 的版本控制的打开的页面,你就需要同时保留这两个版本的缓存,因为之前的版本需要它缓存的版本。你可以使用 activate 事件从之前的缓存中移除数据。
传给 waitUntil() 的 promise 会阻塞其他的事件,直到它完成,因此你可以放心,当你在新的 service worker 中得到你的第一个 fetch 事件时,你的清理操作已经完成。