CycleTracker:Service Worker
到目前为止,我们已经为 CycleTracker 编写了 HTML、CSS 和 JavaScript。我们添加了一个清单文件,其中定义了颜色、图标、URL 和其他应用功能。我们已经有了一个可用的 PWA!但它还不能离线工作。在本节中,我们将编写所需的 JavaScript,将我们功能完备的 Web 应用转换为一个可以作为独立应用分发并能无缝离线工作的 PWA。
如果您尚未完成,请复制 HTML、CSS、JavaScript 和 清单 JSON 文件。将它们分别保存为 index.html、style.css、app.js 和 cycletracker.json。
在本节中,我们将创建 sw.js,即 Service Worker 脚本,它将把我们的 Web 应用转换为 PWA。我们已经有一个 JavaScript 文件;HTML 文件的最后一行调用了 app.js。这个 JavaScript 提供了标准 Web 应用功能的所有功能。我们不会像使用 <script> 的 src 属性调用 app.js 文件那样调用 sw.js 文件,而是通过注册 Service Worker 在 Web 应用及其 Service Worker 之间建立关系。
在本课程结束时,您将拥有一个功能完备的 PWA;一个渐进增强的 Web 应用,可以完全安装,甚至在用户离线时也能正常工作。
Service Worker 的职责
Service Worker 是使应用离线工作同时确保应用始终保持最新状态的关键。为了实现这一点,Service Worker 应包括以下内容:
- 版本号(或其他标识符)。
- 要缓存的资源列表。
- 缓存版本名称。
Service Worker 还负责:
- 在安装应用时安装缓存。
- 根据需要更新自身和其他应用文件。
- 删除不再使用的缓存文件。
我们通过响应三个 Service Worker 事件来完成这些任务,包括:
版本号
一旦 PWA 安装在用户的机器上,唯一能通知浏览器有更新文件需要检索的方法是 Service Worker 发生变化。如果对任何其他 PWA 资源进行了更改 — 如果 HTML 更新了,CSS 中修复了 bug,app.js 中添加了函数,图像被压缩以减小文件大小等等 — 已安装 PWA 的 Service Worker 将不知道它需要下载更新的资源。只有当 Service Worker 以任何方式被修改时,PWA 才会知道可能需要更新缓存;这是 Service Worker 的任务。
虽然从技术上讲,更改任何字符可能就足够了,但 PWA 的最佳实践是创建一个版本号常量,并按顺序更新该常量以指示文件更新。更新版本号(或日期)可以为 Service Worker 提供正式的编辑,即使 Service Worker 本身没有更改其他任何内容,并为开发人员提供了一种识别应用版本的方法。
任务
通过包含版本号来启动 JavaScript 文件
const VERSION = "v1";
将文件保存为 sw.js
离线资源列表
为了获得良好的离线体验,缓存文件列表应包含 PWA 离线体验中使用的所有资源。虽然清单文件可能列出了各种大小的众多图标,但应用缓存只需要包含应用在离线模式下使用的资产。
const APP_STATIC_RESOURCES = [
"/",
"/index.html",
"/style.css",
"/app.js",
"/icon-512x512.png",
];
您无需在列表中包含所有不同操作系统和设备使用的各种图标。但请务必包含应用中使用的任何图像,包括可能在应用加载缓慢时可见的启动页面中使用的资产,或在任何“您需要连接互联网才能获得完整体验”类型的页面中使用的资产。
不要在要缓存的资源列表中包含 Service Worker 文件。
任务
将 CycleTracker PWA 要缓存的资源列表添加到 sw.js。
解决方案示例
我们包含了本教程其他部分创建的 CycleTracker 离线运行时所需的静态资源。我们当前的 sw.js 文件是:
const VERSION = "v1";
const APP_STATIC_RESOURCES = [
"/",
"/index.html",
"/style.css",
"/app.js",
"/cycletracker.json",
"/icons/wheel.svg",
];
我们包含了 wheel.svg 图标,尽管我们当前的应用没有使用它,以防您正在增强 PWA UI,例如在没有周期数据时显示徽标。
应用缓存名称
我们有一个版本号和需要缓存的文件。在缓存文件之前,我们需要创建一个缓存名称,用于存储应用的静态资源。这个缓存名称应该进行版本控制,以确保在应用更新时,将创建一个新缓存并删除旧缓存。
任务
使用 VERSION 号来创建一个带版本号的 CACHE_NAME,并将其作为常量添加到 sw.js。
解决方案示例
我们将缓存命名为 period-tracker-,并附加当前的 VERSION。由于常量声明在一行上,为了更好的可读性,我们将其放在资源常量数组之前。
const VERSION = "v1";
const CACHE_NAME = `period-tracker-${VERSION}`;
const APP_STATIC_RESOURCES = [
// …
];
我们已成功声明了常量;一个唯一的标识符,作为数组的离线资源列表,以及每次标识符更新时都会更改的应用缓存名称。现在让我们专注于安装、更新和删除未使用的缓存资源。
PWA 安装时保存缓存
当用户安装或仅仅访问带有 Service Worker 的网站时,Service Worker 作用域中会触发一个 install 事件。我们希望监听此事件,在安装时用 PWA 的静态资源填充缓存。每次 Service Worker 版本更新时,浏览器都会安装新的 Service Worker,并发生安装事件。
install 事件在应用首次使用时发生,或者当浏览器检测到新版本的 Service Worker 时发生。当旧的 Service Worker 被新的替换时,旧的 Service Worker 将作为 PWA 的 Service Worker 使用,直到新的 Service Worker 被激活。
仅在安全上下文中可用,WorkerGlobalScope.caches 属性返回一个与当前上下文关联的 CacheStorage 对象。CacheStorage.open() 方法返回一个 Promise,该 Promise 解析为与作为参数传入的缓存名称匹配的 Cache 对象。
Cache.addAll() 方法将一个 URL 数组作为参数,检索它们,然后将响应添加到给定的缓存中。ExtendableEvent.waitUntil() 方法告诉浏览器工作正在进行,直到 Promise 解决,并且如果它希望该工作完成,则不应终止 Service Worker。虽然浏览器负责在必要时执行和终止 Service Worker,但 waitUntil 方法是向浏览器发出的请求,要求在执行任务时不要终止 Service Worker。
self.addEventListener("install", (e) => {
e.waitUntil(
(async () => {
const cache = await caches.open("cacheName_identifier");
cache.addAll(["/", "/index.html", "/style.css", "/app.js"]);
})(),
);
});
任务
添加一个 install 事件监听器,该监听器将 APP_STATIC_RESOURCES 中列出的文件检索并存储到名为 CACHE_NAME 的缓存中。
解决方案示例
self.addEventListener("install", (event) => {
event.waitUntil(
(async () => {
const cache = await caches.open(CACHE_NAME);
cache.addAll(APP_STATIC_RESOURCES);
})(),
);
});
更新 PWA 并删除旧缓存
如前所述,当现有 Service Worker 被新的 Service Worker 替换时,现有 Service Worker 将作为 PWA 的 Service Worker 使用,直到新的 Service Worker 激活。我们使用 activate 事件删除旧缓存以避免空间不足。我们遍历命名的 Cache 对象,删除除了当前缓存之外的所有缓存,然后将 Service Worker 设置为 PWA 的 controller。
我们监听当前 Service Worker 的全局作用域 activate 事件。
我们获取现有命名缓存的名称。我们使用 CacheStorage.keys() 方法(再次通过 WorkerGlobalScope.caches 属性访问 CacheStorage),该方法返回一个 Promise,该 Promise 解析为一个数组,其中包含所有命名 Cache 对象的字符串,按其创建顺序排列。
我们使用 Promise.all() 方法遍历命名缓存 Promise 列表。all() 方法将可迭代 Promise 列表作为输入,并返回一个单一的 Promise。对于命名缓存列表中的每个名称,检查缓存是否是当前活动的缓存。如果不是,则使用 Cache 的 delete() 方法将其删除。
最后一行,await clients.claim() 使用 Clients 接口的 claim() 方法,使我们的 Service Worker 能够将自身设置为我们客户端的控制器;“客户端”指的是 PWA 的运行实例。claim() 方法使 Service Worker 能够“声明控制”其作用域内的所有客户端。这样,在同一作用域内加载的客户端无需重新加载。
self.addEventListener("activate", (event) => {
event.waitUntil(
(async () => {
const names = await caches.keys();
await Promise.all(
names.map((name) => {
if (name !== CACHE_NAME) {
return caches.delete(name);
}
return undefined;
}),
);
await clients.claim();
})(),
);
});
任务
将上述 activate 事件监听器添加到您的 sw.js 文件中。
fetch 事件
我们可以利用 fetch 事件,以防止已安装的 PWA 在用户在线时发出请求。监听 fetch 事件使得拦截所有请求并用缓存响应代替网络请求成为可能。大多数应用程序不需要这种行为。事实上,许多商业模式希望用户定期发出服务器请求以进行跟踪和营销。因此,尽管拦截请求可能对某些人来说是一种反模式,但为了提高我们的 CycleTracker 应用程序的隐私性,我们不希望应用程序发出不必要的服务器请求。
由于我们的 PWA 由一个单一页面组成,对于页面导航请求,我们返回 index.html 主页。没有其他页面,我们也不希望访问服务器。如果 Fetch API 的 Request 只读 mode 属性是 navigate,这意味着它正在寻找网页,我们使用 FetchEvent 的 respondWith() 方法来阻止浏览器的默认 fetch 处理,通过使用 caches.match() 方法提供我们自己的响应 Promise。
对于所有其他请求模式,我们像 安装事件响应 中那样打开缓存,而是将事件请求传递给相同的 match() 方法。它检查请求是否是存储的 Response 的键。如果是,它返回缓存的响应。如果不是,我们返回 404 状态 作为响应。
使用 Response() 构造函数传递一个 null 主体和 status: 404 作为选项,并不意味着我们的 PWA 中存在错误。相反,我们需要的一切都应该已经在缓存中,如果不在,我们也不会去服务器解决这个非问题。
self.addEventListener("fetch", (event) => {
// when seeking an HTML page
if (event.request.mode === "navigate") {
// Return to the index.html page
event.respondWith(caches.match("/"));
return;
}
// For every other request type
event.respondWith(
(async () => {
const cache = await caches.open(CACHE_NAME);
const cachedResponse = await cache.match(event.request.url);
if (cachedResponse) {
// Return the cached response if it's available.
return cachedResponse;
}
// Respond with a HTTP 404 response status.
return new Response(null, { status: 404 });
})(),
);
});
完整的 Service Worker 文件
您的 sw.js 文件应与以下 JavaScript 类似。请注意,当更新 APP_STATIC_RESOURCES 数组中列出的任何资源时,此 Service Worker 中唯一必须更新的常量或函数是 VERSION 的值。
// The version of the cache.
const VERSION = "v1";
// The name of the cache
const CACHE_NAME = `period-tracker-${VERSION}`;
// The static resources that the app needs to function.
const APP_STATIC_RESOURCES = [
"/",
"/index.html",
"/app.js",
"/style.css",
"/icons/wheel.svg",
];
// On install, cache the static resources
self.addEventListener("install", (event) => {
event.waitUntil(
(async () => {
const cache = await caches.open(CACHE_NAME);
cache.addAll(APP_STATIC_RESOURCES);
})(),
);
});
// delete old caches on activate
self.addEventListener("activate", (event) => {
event.waitUntil(
(async () => {
const names = await caches.keys();
await Promise.all(
names.map((name) => {
if (name !== CACHE_NAME) {
return caches.delete(name);
}
return undefined;
}),
);
await clients.claim();
})(),
);
});
// On fetch, intercept server requests
// and respond with cached responses instead of going to network
self.addEventListener("fetch", (event) => {
// As a single page app, direct app to always go to cached home page.
if (event.request.mode === "navigate") {
event.respondWith(caches.match("/"));
return;
}
// For all other requests, go to the cache first, and then the network.
event.respondWith(
(async () => {
const cache = await caches.open(CACHE_NAME);
const cachedResponse = await cache.match(event.request.url);
if (cachedResponse) {
// Return the cached response if it's available.
return cachedResponse;
}
// If resource isn't in the cache, return a 404.
return new Response(null, { status: 404 });
})(),
);
});
更新 Service Worker 时,VERSION 常量不需要更新,因为 Service Worker 脚本内容本身的任何更改都会触发浏览器安装新的 Service Worker。但是,更新版本号是一个好习惯,因为它使开发人员(包括您自己)更容易查看当前浏览器中运行的 Service Worker 版本,通过 在“应用”工具中检查缓存名称(或“源”工具)。
注意:当对任何应用程序资源(包括 CSS、HTML 和 JS 代码以及图像资源)进行更改时,更新 VERSION 至关重要。版本号,或 Service Worker 文件的任何更改,是强制为用户更新应用程序的唯一方法。
注册 Service Worker
现在我们的 Service Worker 脚本已完成,我们需要注册 Service Worker。
我们首先通过使用 特性检测 全局 navigator 对象上是否存在 serviceWorker 属性来检查浏览器是否支持 Service Worker API。
// Does "serviceWorker" exist
if ("serviceWorker" in navigator) {
// If yes, we register the service worker
}
如果支持该属性,我们就可以使用 Service Worker API 的 ServiceWorkerContainer 接口的 register() 方法。
if ("serviceWorker" in navigator) {
// Register the app's service worker
// Passing the filename where that worker is defined.
navigator.serviceWorker.register("sw.js");
}
虽然上述内容足以满足 CycleTracker 应用的需求,但 register() 方法确实返回一个 Promise,该 Promise 解析为 ServiceWorkerRegistration 对象。对于更健壮的应用程序,请检查注册错误:
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("sw.js").then(
(registration) => {
console.log("Service worker registration successful:", registration);
},
(error) => {
console.error(`Service worker registration failed: ${error}`);
},
);
} else {
console.error("Service workers are not supported.");
}
任务
打开 index.html 并在包含 app.js 的脚本之后、闭合 </body> 标签之前添加以下 <script>。
<!-- Register the app's service worker. -->
<script>
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("sw.js");
}
</script>
您可以尝试功能齐全的 CycleTracker 经期跟踪 Web 应用,并在 GitHub 上查看 Web 应用源代码。是的,它确实有效,现在,它正式成为一个 PWA!
调试 Service Worker
由于我们设置 Service Worker 的方式,一旦它被注册,每个请求都将从缓存中获取,而不是加载新内容。在开发时,您将频繁编辑代码。您可能希望定期在浏览器中测试您的编辑;很可能是每次保存后。
通过更新版本号并进行硬重置
要获取新缓存,您可以更改 版本号,然后进行硬浏览器刷新。进行硬刷新的方式取决于浏览器和操作系统:
- 在 Windows 上:Ctrl+F5、Shift+F5 或 Ctrl+Shift+R。
- 在 macOS 上:Shift+Command+R。
- macOS 上的 Safari:Option+Command+E 清空缓存,然后 Option+Command+R。
- 在移动设备上:转到浏览器(Android)或操作系统(三星、iOS)设置,在高级设置下找到浏览器(iOS)或网站数据(Android、三星)网站设置,删除 CycleTracker 的数据,然后重新加载页面。
使用开发者工具
您可能不希望每次保存都更新版本号。在您准备将新版本的 PWA 发布到生产环境并向所有人提供新版本 PWA 之前,您可以注销 Service Worker,而不是在保存时更改版本号。
您可以通过单击浏览器开发人员工具中的unregister按钮来注销 Service Worker。硬刷新页面将重新注册 Service Worker 并创建新缓存。
![]()
在某些开发人员工具中,您可以手动注销 Service Worker,或者您可以选择 Service Worker 的“重新加载时更新”选项,该选项会使开发人员工具在每次重新加载时重置并重新激活 Service Worker,只要开发人员工具是打开的。还有一个选项可以绕过 Service Worker 并从网络加载资源。此面板包括本教程未涵盖的功能,但随着您创建更高级的 PWA(包括同步和推送,这两个都包含在离线和后台操作指南中),这些功能将很有帮助。
![]()
DevTools 应用程序面板中的 Service Worker 窗口提供了一个链接,可以访问一个弹出窗口,其中包含浏览器中所有已注册 Service Worker 的列表;而不仅仅是当前选项卡中打开的应用程序的 Service Worker。每个 Service Worker 工作人员列表都有按钮来停止、启动或注销该单个 Service Worker。
![]()
换句话说,当您正在开发 PWA 时,您不必为每个应用视图更新版本号。但请记住,当您完成所有更改后,在分发更新版本的 PWA 之前,请更新 Service Worker 的 VERSION 值。如果您忘记了,所有已经安装您的应用或甚至在未安装的情况下访问您的在线 PWA 的用户,都将无法看到您的更改!
我们完成了!
PWA 的核心是一个可以安装的 Web 应用,并且通过渐进增强使其能够离线工作。我们创建了一个功能完备的 Web 应用。然后,我们添加了将它转换为 PWA 所需的两个功能——清单文件和 Service Worker。如果您想与他人分享您的应用,请通过安全连接使其可用。或者,如果您只想自己使用周期追踪器,请创建本地开发环境,安装 PWA,然后享受吧!一旦安装,您就不再需要运行 localhost。
恭喜!