使用 Service Worker 实现 PWA 离线功能
现在我们已经了解了 js13kPWA 的结构,并且看到了基本框架的运行,让我们来看看如何实现使用 Service Worker 的离线功能。在本文中,我们将探讨它在我们js13kPWA 示例(也可以查看源代码)中的使用方式。我们将研究如何添加离线功能。
Service Worker 解释
Service Worker 是浏览器和网络之间的虚拟代理。它们可以正确缓存网站的资源,并在用户设备离线时提供这些资源。
它们在与页面主要 JavaScript 代码分开的线程上运行,并且无法访问 DOM 结构。这引入了与传统 Web 编程不同的方法——API 是非阻塞的,并且可以在不同的上下文之间发送和接收通信。你可以为 Service Worker 提供一些工作,并使用基于Promise的方法在它准备就绪时接收结果。
Service Worker 不仅可以提供离线功能,还可以处理通知或执行繁重的计算。Service Worker 非常强大,因为它们可以控制网络请求,修改它们,提供从缓存中检索的自定义响应,或完全合成响应。
要详细了解 Service Worker,请参阅离线和后台操作。
js13kPWA 应用中的 Service Worker
让我们看看 js13kPWA 应用如何使用 Service Worker 提供离线功能。
注册 Service Worker
我们将从查看注册新 Service Worker 的代码开始,该代码位于 app.js 文件中
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("./pwa-examples/js13kpwa/sw.js");
}
如果浏览器支持 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 (let i = 0; i < games.length; i++) {
gamesImages.push(`data/img/${games[i].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
对象是什么。
在waitUntil
内部的代码执行之前,Service Worker 不会安装。它返回一个 Promise——需要这种方法是因为安装可能需要一些时间,因此我们必须等待它完成。
caches
是在给定 Service Worker 的作用域中可用的特殊CacheStorage
对象,用于启用数据保存——保存到Web 存储不起作用,因为 Web 存储是同步的。使用 Service Worker,我们改为使用 Cache API。
在这里,我们使用给定的名称打开一个缓存,然后将应用使用的所有文件添加到缓存中,以便下次加载时可以使用。资源由其请求 URL 标识,该 URL 相对于 worker 的位置。
你可能会注意到我们没有缓存game.js
。这是包含我们在显示游戏时使用的数据的文件。实际上,这些数据很可能来自 API 端点或数据库,缓存数据意味着在网络连接时定期更新它。我们在这里不讨论这个问题,但周期性后台同步 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 在后台安装,并且之前的 Service Worker(v1)会一直正常工作,直到没有页面使用它为止——然后新的 Service Worker 被激活并接管旧 Service Worker 对页面的管理。
清除缓存
还记得我们跳过的activate
事件吗?它可以用来清除我们不再需要的旧缓存
self.addEventListener("activate", (e) => {
e.waitUntil(
caches.keys().then((keyList) => {
return Promise.all(
keyList.map((key) => {
if (key === cacheName) {
return;
}
return caches.delete(key);
}),
);
}),
);
});
这确保了缓存中只有我们需要的文件,因此我们不会留下任何垃圾;浏览器中可用的缓存空间是有限的,因此清理是我们自己的好习惯。
其他用例
从缓存中提供文件不是 Service Worker 提供的唯一功能。如果你需要执行繁重的计算,你可以将其从主线程中卸载并在 worker 中执行,并在结果可用时立即接收结果。在性能方面,你可以预取当前不需要但将来可能需要的资源,以便在实际需要这些资源时应用速度更快。
总结
在本文中,我们简单地介绍了如何使用 Service Worker 使 PWA 离线工作。如果你想详细了解Service Worker API背后的概念以及如何更详细地使用它,请务必查看我们的其他文档。
Service Workers 也用于处理推送通知——这将在后续文章中解释。