js13kGames:使用 Service Worker 让 PWA 实现离线工作
既然我们已经了解了 js13kPWA 的结构,并且看到了基本框架的运行,现在我们来看看如何使用 Service Worker 实现离线功能。在本文中,我们将探讨它如何在我们的 js13kPWA 示例(也可以查看源代码)中使用。我们将研究如何添加离线功能。
Service Worker 详解
Service Worker 是浏览器和网络之间的虚拟代理。它们使得正确缓存网站资源并使其在用户设备离线时可用成为可能。
它们在与我们页面主 JavaScript 代码不同的线程上运行,并且无法访问 DOM 结构。这引入了一种与传统 Web 编程不同的方法——API 是非阻塞的,并且可以在不同上下文之间发送和接收通信。您可以使用基于 Promise 的方法,让 Service Worker 处理一些工作,并在结果准备好后接收它。
Service Worker 不仅能提供离线功能,还可以处理通知或执行复杂的计算。Service Worker 非常强大,因为它们可以控制网络请求,修改它们,提供从缓存中检索的自定义响应,或者完全合成响应。
要了解更多关于 Service Worker 的信息,请参阅 离线和后台操作。
js13kPWA 应用中的 Service Worker
让我们来看看 js13kPWA 应用如何使用 Service Worker 来提供离线功能。
注册 Service Worker
我们将从查看在 app.js 文件中注册新 Service Worker 的代码开始。
let swRegistration = null;
if ("serviceWorker" in navigator) {
navigator.serviceWorker
.register("./pwa-examples/js13kpwa/sw.js")
.then((reg) => {
swRegistration = reg;
});
}
如果浏览器支持 Service Worker API,则使用 ServiceWorkerContainer.register() 方法将其注册到网站。其内容位于 sw.js 文件中,并在注册成功后执行。这是 app.js 文件中唯一包含 Service Worker 代码的部分;所有其他特定于 Service Worker 的代码都写在 sw.js 文件本身中。
Service Worker 的生命周期
注册完成后,sw.js 文件会自动下载、安装,然后激活。
安装
该 API 允许我们为感兴趣的关键事件添加事件监听器——第一个是 install 事件。
self.addEventListener("install", (e) => {
console.log("[Service Worker] Install");
});
在 install 监听器中,我们可以初始化缓存并将文件添加到其中以供离线使用。我们的 js13kPWA 应用正是这样做的。
首先,创建一个用于存储缓存名称的变量,并将应用外壳文件列在一个数组中。
const cacheName = "js13kPWA-v1";
const appShellFiles = [
"/pwa-examples/js13kpwa/",
"/pwa-examples/js13kpwa/index.html",
"/pwa-examples/js13kpwa/app.js",
"/pwa-examples/js13kpwa/style.css",
"/pwa-examples/js13kpwa/fonts/graduate.eot",
"/pwa-examples/js13kpwa/fonts/graduate.ttf",
"/pwa-examples/js13kpwa/fonts/graduate.woff",
"/pwa-examples/js13kpwa/favicon.ico",
"/pwa-examples/js13kpwa/img/js13kgames.png",
"/pwa-examples/js13kpwa/img/bg.png",
"/pwa-examples/js13kpwa/icons/icon-32.png",
"/pwa-examples/js13kpwa/icons/icon-64.png",
"/pwa-examples/js13kpwa/icons/icon-96.png",
"/pwa-examples/js13kpwa/icons/icon-128.png",
"/pwa-examples/js13kpwa/icons/icon-168.png",
"/pwa-examples/js13kpwa/icons/icon-192.png",
"/pwa-examples/js13kpwa/icons/icon-256.png",
"/pwa-examples/js13kpwa/icons/icon-512.png",
];
接下来,在第二个数组中生成要与 data/games.js 文件中的内容一起加载的图片的链接。然后,使用 Array.prototype.concat() 函数将这两个数组合并。
const gamesImages = [];
for (const game of games) {
gamesImages.push(`data/img/${game.slug}.jpg`);
}
const contentToCache = appShellFiles.concat(gamesImages);
然后我们可以管理 install 事件本身。
self.addEventListener("install", (e) => {
console.log("[Service Worker] Install");
e.waitUntil(
(async () => {
const cache = await caches.open(cacheName);
console.log("[Service Worker] Caching all: app shell and content");
await cache.addAll(contentToCache);
})(),
);
});
这里有两点需要解释:ExtendableEvent.waitUntil 的作用,以及 caches 对象是什么。
Service Worker 不会安装,直到 waitUntil 中的代码执行完毕。它返回一个 Promise——这种方法是必需的,因为安装可能需要一些时间,所以我们必须等待它完成。
caches 是在给定 Service Worker 的作用域中可用的特殊 CacheStorage 对象,用于保存数据——保存到 Web Storage 将不起作用,因为 Web Storage 是同步的。使用 Service Worker,我们改用 Cache API。
在这里,我们用给定的名称打开一个缓存,然后将应用使用的所有文件添加到缓存中,以便下次加载时可用。资源通过其请求 URL 标识,该 URL 相对于 Worker 的 位置。
您可能会注意到我们没有缓存 game.js。这是包含我们在显示游戏时使用的数据的文件。实际上,这些数据很可能来自 API 端点或数据库,缓存数据意味着在有网络连接时定期更新它。我们在此处不深入探讨,但 Periodic Background Sync API 是关于此主题的进一步阅读的好材料。
激活
还有一个 activate 事件,其用法与 install 相同。此事件通常用于删除任何不再需要的文件并清理应用。我们在应用中不需要这样做,所以我们跳过它。
响应请求
我们还有一个可用的 fetch 事件,它会在我们的应用发出每个 HTTP 请求时触发。这非常有用,因为它允许我们拦截请求并用自定义响应来响应它们。例如:
self.addEventListener("fetch", (e) => {
console.log(`[Service Worker] Fetched resource ${e.request.url}`);
});
响应可以是任何我们想要的东西:请求的文件、其缓存的副本,或者一段将执行特定操作的 JavaScript 代码——可能性是无限的。
在我们的示例应用中,只要资源实际存在于缓存中,我们就从缓存而不是网络提供内容。无论应用是在线还是离线,我们都这样做。如果文件不在缓存中,应用会先将其添加进去,然后再提供。
self.addEventListener("fetch", (e) => {
e.respondWith(
(async () => {
const r = await caches.match(e.request);
console.log(`[Service Worker] Fetching resource: ${e.request.url}`);
if (r) {
return r;
}
const response = await fetch(e.request);
const cache = await caches.open(cacheName);
console.log(`[Service Worker] Caching new resource: ${e.request.url}`);
cache.put(e.request, response.clone());
return response;
})(),
);
});
在这里,我们用一个函数响应 fetch 事件,该函数尝试在缓存中查找资源并返回响应(如果存在)。如果不存在,我们使用另一个 fetch 请求从网络获取它,然后将响应存储在缓存中,以便下次请求时可用。
FetchEvent.respondWith 方法接管了控制——这部分充当了应用和网络之间的代理服务器。这使我们能够用任何我们想要的响应来响应每一个请求:由 Service Worker 准备,从缓存中获取,如果需要则进行修改。
就这样!我们的应用在安装时缓存其资源,并通过 fetch 从缓存中提供它们,因此即使在用户离线时也能工作。它还在添加新内容时缓存新内容。
更新
还有一点需要说明:当有新版本应用包含新资产可用时,如何升级 Service Worker?缓存名称中的版本号是关键。
const cacheName = "js13kPWA-v1";
当这更新到 v2 时,我们可以将所有文件(包括我们的新文件)添加到新的缓存中。
contentToCache.push("/pwa-examples/js13kpwa/icons/icon-32.png");
// …
self.addEventListener("install", (e) => {
e.waitUntil(
(async () => {
const cache = await caches.open(cacheName);
await cache.addAll(contentToCache);
})(),
);
});
一个新的 Service Worker 在后台安装,前一个 (v1) 正确运行,直到没有页面在使用它——然后新的 Service Worker 被激活,并从旧的 Service Worker 接管页面的管理。
清除缓存
还记得我们跳过的 activate 事件吗?它可以用来清除我们不再需要的旧缓存。
self.addEventListener("activate", (e) => {
e.waitUntil(
caches.keys().then((keyList) =>
Promise.all(
keyList.map((key) => {
if (key === cacheName) {
return undefined;
}
return caches.delete(key);
}),
),
),
);
});
这确保了我们缓存中只有我们需要的文件,因此我们不会留下任何垃圾;浏览器中的可用缓存空间是有限的,所以清理自己是一个好主意。
其他用例
从缓存提供文件并不是 Service Worker 提供的唯一功能。如果您有复杂的计算需要执行,您可以将其从主线程卸载并在 Worker 中执行,并在结果可用时立即接收它们。从性能角度来看,您可以预取当前不需要但将来可能需要的资源,这样当您实际需要这些资源时,应用程序会更快。
总结
在本文中,我们简单地探讨了如何使用 Service Worker 让您的 PWA 实现离线工作。如果您想了解更多关于 Service Worker API 背后的概念以及如何更详细地使用它,请务必查阅我们的进一步文档。
在处理 推送通知时也会使用 Service Worker——这将在后续文章中进行解释。