编写 Service Worker 脚本

此 Codelab 是由 Google Developers 培训团队开发的“渐进式 Web 应用开发”培训课程的一部分。如果您按顺序学习本 Codelab,将收获最多的课程价值。

如需了解本课程的完整详情,请参阅开发渐进式 Web 应用概览

简介

本实验将引导您创建简单的 Service Worker,并介绍 Service Worker 的生命周期。

学习内容

  • 创建基本的 Service Worker 脚本,安装该脚本,并执行简单的调试

注意事项

  • 基本 JavaScript 和 HTML
  • ES2015 Promise 的概念和基本语法
  • 如何启用开发者控制台

准备工作

从 GitHub 下载或克隆 pwa-training-labs 代码库,并安装 Node.js 的 LTS 版本(如果需要)。

转到 service-worker-lab/app/ 目录并启动本地开发服务器:

cd service-worker-lab/app
npm install
node server.js

您可以随时使用 Ctrl-c 终止服务器。

打开浏览器并导航到 localhost:8081/

注意:请取消注册任何 Service Worker 并清除 localhost 的所有 Service Worker 缓存,以免它们干扰实验室。在 Chrome DevTools 中,您可以点击 Application 标签页的 Clear storage 部分中的清除网站数据来执行此操作。

在您的首选文本编辑器中打开 service-worker-lab/app/ 文件夹。您将在其中构建实验的 app/ 文件夹。

此文件夹包含:

  • below/another.htmljs/another.jsjs/other.jsother.html 是用于测试 Service Worker 范围的示例资源
  • styles/ 文件夹包含此实验的级联样式表
  • test/ 文件夹包含用于测试进度的文件
  • index.html 是我们的示例网站/应用的主 HTML 页面
  • service-worker.js 是用于创建 Service Worker 的 JavaScript 文件
  • package.jsonpackage-lock.json 会跟踪此项目中使用的节点软件包
  • server.js 是我们用来托管应用的简单 Express 服务器

在文本编辑器中打开 service-worker.js。请注意,该文件为空。我们尚未添加任何要在 Service Worker 中运行的代码。

在文本编辑器中打开 index.html

<script> 标记内,添加以下代码来注册 Service Worker:

if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('service-worker.js')
    .then(registration => {
      console.log('Service Worker is registered', registration);
    })
    .catch(err => {
      console.error('Registration failed:', err);
    });
  });
}

保存脚本并刷新页面。控制台应返回一条消息,说明已注册 Service Worker。在 Chrome 中,依次打开 DevTools(在 Windows 和 Linux 上,按 Ctrl + Shift + I,或在 Mac 上按 ⌘ + alt + I)并点击 Application 标签页,然后点击 Service Workers 选项,检查是否已注册 Service Worker。您应该会看到类似下图的内容:

可选:在不受支持的浏览器上打开网站,验证支持条件检查是否有效。

说明

以上代码会将 service-worker.js 文件注册为 Service Worker。它首先检查浏览器是否支持 Service Worker。某些浏览器可能不支持 Service Worker,因此每次注册 Service Worker 时都应执行此操作。然后,代码会使用包含在窗口的 Navigator 接口中的 ServiceWorkerContainer APIregister 方法注册 Service Worker。

成功注册 Service Worker 后,navigator.serviceWorker.register(...) 会返回一个使用 registration 对象进行解析的 promise。如果注册失败,promise 将拒绝。

Service Worker 的状态变化会在 Service Worker 中触发事件。

添加事件监听器

在文本编辑器中打开 service-worker.js

将以下事件监听器添加到 Service Worker:

self.addEventListener('install', event => {
  console.log('Service worker installing...');
  // Add a call to skipWaiting here
});

self.addEventListener('activate', event => {
  console.log('Service worker activating...');
});

保存文件。

手动取消注册 Service Worker,然后刷新页面以安装和激活更新后的 Service Worker。控制台日志应指明新的 Service Worker 已注册、安装并激活。

注意:注册日志可能会与其他日志一起显示(安装和激活)。Service Worker 与页面并发运行,因此我们无法保证日志的顺序(注册日志来自页面,而安装和激活日志则来自 Service Worker)。不过,安装、激活和其他 Service Worker 事件在 Service Worker 内按指定顺序进行,并且应始终按预期顺序显示。

说明

Service Worker 在注册结束时发出 install 事件。在上面的代码中,消息会记录在 install 事件监听器中,但在实际应用中,这非常适合缓存静态资源。

注册 Service Worker 后,浏览器会检测 Service Worker 是否为新 Service Worker(原因是它与之前安装的 Service Worker 不同,或者没有为此网站注册 Service Worker)。如果 Service Worker 是新的(如本例中所示),则浏览器会安装它。

Service Worker 在控制页面时发出 activate 事件。以上代码记录了一条消息,但此事件通常用于更新缓存。

在指定范围中,一次只能有一个 Service Worker 处于活动状态(请参阅“探索 Service Worker 范围”),因此在现有 Service Worker 不再处于使用状态之前,新安装的 Service Worker 不会激活。因此,在新的 Service Worker 可以接管之前,必须关闭由 Service Worker 控制的所有页面。由于我们取消注册了现有 Service Worker,因此新的 Service Worker 会立即激活。

注意:仅仅刷新网页还不能将控制权转移给新的 Service Worker,因为系统将在当前页面卸载之前请求新页面,并且不会有旧的 Service Worker 处于未使用状态。

注意:您还可以使用某些浏览器和以编程方式使用 skipWaiting() 手动激活新的 Service Worker,我们将在第 3.4 节中对此进行介绍。

更新 Service Worker

service-worker.js 中的任意位置添加以下注释:

// I'm a new service worker

保存文件并刷新页面。查看控制台中的日志;请注意,新 Service Worker 已安装但并未激活。在 Chrome 中,您可以在开发者工具的 Application 标签页中看到等待的 Service Worker。

关闭与 Service Worker 关联的所有页面。然后,重新打开 localhost:8081/。控制台日志应指明新的 Service Worker 现已激活。

注意:如果收到意外结果,请确保您的开发者工具已停用 HTTP 缓存。

说明

浏览器检测到新 Service Worker 文件和现有 Service Worker 文件之间的字节差异(因为添加了注释),因此新 Service Worker 会被安装。因为对于给定的范围,一次只能有一个 Service Worker 处于活动状态,所以即使安装了新的 Service Worker,在现有的 Service Worker 不再使用之前,它也不会激活。通过关闭旧 Service Worker 控制下的所有页面,我们能够激活新的 Service Worker。

跳过等待阶段

通过跳过等待阶段,即使现有 Service Worker 存在,新 Service Worker 也可以立即激活。

service-worker.js 中,在 install 事件监听器中添加对 skipWaiting 的调用:

self.skipWaiting();

保存文件并刷新页面。请注意,即使之前的 Service Worker 拥有控制权,新 Service Worker 也会立即安装并激活。

说明

skipWaiting() 方法允许 Service Worker 在安装完成后立即激活。安装事件监听器是放置 skipWaiting() 调用的常见位置,但可以在等待阶段的任何位置或之前进行调用。如需详细了解何时以及如何使用 skipWaiting(),请参阅此文档。在此实验的其余部分,我们现在可以测试新的 Service Worker 代码,而无需手动取消注册 Service Worker。

更多信息

Service Worker 可以充当您的 Web 应用与网络之间的代理。

我们来添加一个提取监听器,以拦截来自我们网域的请求。

将以下代码添加到 service-worker.js

self.addEventListener('fetch', event => {
  console.log('Fetching:', event.request.url);
});

保存脚本并刷新页面,以安装并激活更新后的 Service Worker。

检查控制台,并观察到未记录哪些提取事件。请刷新页面,然后再次检查控制台。这次,您应该会看到相应网页及其资产(例如 CSS)的提取事件。

点击指向其他页面其他页面返回的链接。

您会在控制台中看到每个网页及其资源的提取事件。所有日志是否有意义?

注意:如果您访问某个网页,但未停用 HTTP 缓存,那么 CSS 和 JavaScript 资源可能会缓存在本地。如果发生这种情况,您将看不到这些资源的提取事件。

说明

Service Worker 会在其范围内针对浏览器发出的每个 HTTP 请求接收提取事件。fetch event 对象包含相应请求。在 Service Worker 中监听提取事件与在 DOM 中监听点击事件类似。在我们的代码中,当发生提取事件时,我们会将请求的网址记录到控制台(实际上,我们还可以使用任意资源创建和返回我们自己的自定义响应)。

为什么没有任何刷新事件记录第一次刷新?默认情况下,从网页提取事件不会经由 Service Worker,除非页面请求本身通过 Service Worker。这样可以确保网站中的一致性;如果页面在没有 Service Worker 的情况下加载,则相应的子资源也应如此。

更多信息

解决方案代码

如需获取该工作代码的副本,请转到 04-intercepting-network-requests/ 文件夹。

Service Worker 具有范围。Service Worker 的范围决定了该 Service Worker 从哪些路径拦截请求。

查找范围

更新 index.html 中的注册代码,改用以下代码:

if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('service-worker.js')
    .then(registration => {
      console.log('SW registered with scope:', registration.scope);
    })
    .catch(err => {
      console.error('Registration failed:', err);
    });
  });
}

刷新浏览器。请注意,控制台会显示 Service Worker 的范围(在本例中为 http://localhost:8081/)。

说明

register() 返回的 promise 可解析为包含 Service Worker 范围的 registration 对象

默认范围是 Service Worker 文件的路径,并扩展到所有下级目录。因此,应用根目录中的 Service Worker 可控制来自该应用中所有文件的请求。

移动 Service Worker

service-worker.js 移到 below/ 目录中,并更新 index.html 中注册代码中的 Service Worker 网址。

在浏览器中取消注册当前 Service Worker,然后刷新页面。

控制台显示 Service Worker 的范围现在为 http://localhost:8081/below/。在 Chrome 中,您还可以在开发者工具的应用标签页中查看 Service Worker 作用域:

返回主页面,依次点击其他页面其他页面返回。正在记录哪些提取请求?下列哪项不是?

说明

Service Worker 的默认范围是 Service Worker 文件的路径。因为 Service Worker 文件现在位于 below/ 中,所以这是其作用域。控制台现在只会记录 another.htmlanother.cssanother.js 的提取事件,因为这些资源是 Service Worker 范围中的唯一资源。

设置任意范围

将 Service Worker 移回项目根目录 (app/),并在 index.html 的注册代码中更新 Service Worker 网址。

通过 MDN 参考,使用 register() 中的可选参数将 Service Worker 的范围设置为 below/ 目录。

取消注册 Service Worker 并刷新页面。点击其他页面其他页面返回

同样,控制台会显示 Service Worker 的范围现在是 http://localhost:8081/below/,并且仅记录 another.htmlanother.cssanother.js 的提取事件。

说明

您可以通过在注册时传入其他参数来设置任意范围,例如:

navigator.serviceWorker.register('/service-worker.js', {
  scope: '/kitten/'
});

在上面的示例中,Service Worker 的范围设置为 /kitten/。Service Worker 拦截来自 /kitten//kitten/lower/ 中的页面的请求,但不会拦截来自 /kitten/ 等页面的请求。

注意:您无法设置位于 Service Worker 实际位置上方的任意范围。但是,如果使用 Service-Worker-Allowed 标头提供的客户端上存在工作器,您可以在 Service Worker 的位置上方指定 Service Worker 的最大范围。

更多信息

解决方案代码

如需获取该工作代码的副本,请转到 solution/ 文件夹。

现在,您已启动并运行了一个简单的 Service Worker,并了解 Service Worker 的生命周期。

更多信息

Service Worker 生命周期

如需查看 PWA 培训课程中的所有 Codelab,请参阅课程的欢迎 Codelab