js13kGames:使用通知和推送 API 让 PWA 重新吸引用户
缓存应用程序内容以离线工作是一项很棒的功能。允许用户在他们的设备上安装 Web 应用则更佳。但除了仅依赖用户操作之外,我们还可以做得更多,通过使用推送消息和通知,在有新内容可用时自动重新吸引用户并提供新内容。
两个 API,一个目标
推送 API 和 通知 API 是两个独立的 API,但当您想在您的应用程序中提供引人入胜的功能时,它们可以很好地协同工作。推送用于在没有任何客户端干预的情况下将新内容从服务器传递到应用程序,其操作由应用程序的服务工作线程处理。通知可以由服务工作线程用来向用户显示新信息,或者至少在有内容更新时提醒他们。
它们与服务工作线程一样,在浏览器窗口外部运行,因此即使应用程序页面未获得焦点或已关闭,也可以推送更新并显示通知。
通知
让我们先从通知开始——它们可以独立工作,但与推送结合后会更有用。我们先单独看看通知。
请求权限
要显示通知,我们必须先请求权限。但最佳实践表明,我们不应立即显示通知,而应在用户通过单击按钮请求时显示弹出窗口。
const button = document.getElementById("notifications");
button.addEventListener("click", () => {
Notification.requestPermission().then((result) => {
if (result === "granted") {
randomNotification();
}
});
});
这会显示一个使用操作系统自身通知服务的弹出窗口。

当用户确认接收通知时,应用程序就可以显示它们了。用户操作的结果可以是默认、允许或拒绝。当用户未做出选择时,将选择默认选项,而当用户分别单击是或否时,则分别设置其他两个选项。
接受后,权限对通知和推送都有效。
创建通知
示例应用程序根据可用数据创建通知——随机选择一个游戏,然后将选定的游戏内容用于通知:它将游戏名称设置为标题,在正文中提及作者,并将图像显示为图标。
function randomNotification() {
if (!swRegistration) return;
const randomItem = Math.floor(Math.random() * games.length);
const notifTitle = games[randomItem].name;
const notifBody = `Created by ${games[randomItem].author}.`;
const notifImg = `data/img/${games[randomItem].slug}.jpg`;
const options = {
body: notifBody,
icon: notifImg,
};
swRegistration.showNotification(notifTitle, options);
setTimeout(randomNotification, 30000);
}
每 30 秒创建一个新的随机通知,直到用户觉得太烦人而禁用它。(对于实际应用程序,通知的频率应该低得多,而且更有用。)通知 API 的优势在于它使用了操作系统的通知功能。这意味着即使在用户未查看 Web 应用程序时,也可以将通知显示给用户,并且通知看起来与原生应用程序显示的通知类似。
推送 (Push)
推送比通知更复杂——我们需要订阅一个服务器,然后该服务器会将数据发送回应用程序。应用程序的服务工作线程将从推送服务器接收数据,然后可以使用通知系统或其他所需机制来显示这些数据。
这项技术仍处于非常初级的阶段——一些工作示例使用了 Google Cloud Messaging 平台,但正在重写以支持 VAPID(自愿应用程序标识),它为您的应用程序提供了额外的安全层。您可以查看 Service Workers Cookbook 示例,尝试使用 Firebase 设置推送消息服务器,或构建自己的服务器(例如使用 Node.js)。
如前所述,要接收推送消息,您必须有一个服务工作线程,其基础知识已在 使用 Service Workers 让 PWA 离线工作 文章中进行了介绍。在服务工作线程内部,通过调用 PushManager 接口的 getSubscription() 方法来创建推送服务订阅机制。
navigator.serviceWorker
.register("service-worker.js")
.then((registration) => registration.pushManager.getSubscription())
.then(/* … */);
一旦用户订阅成功,他们就可以从服务器接收推送通知。
从服务器端来看,整个过程出于安全原因必须使用公钥和私钥进行加密——允许任何人不安全地使用您的应用程序发送推送消息将是一个糟糕的主意。有关保护服务器的详细信息,请参阅 Web Push 数据加密测试页面。服务器存储用户订阅时接收到的所有信息,以便以后需要时可以发送消息。
要接收推送消息,我们可以在服务工作线程文件中监听 push 事件。
self.addEventListener("push", (e) => {
/* ... */
});
可以检索数据,然后立即将其显示为通知给用户。例如,这可用于提醒用户某事,或让他们知道应用程序中提供的新内容。
推送示例
推送需要服务器部分才能工作,因此我们无法将其包含在托管在 GitHub Pages 上的 js13kPWA 示例中,因为它仅提供静态文件托管。所有内容都在 Service Worker Cookbook 中进行了说明——请参阅 Push Payload 演示。
此演示由三个文件组成:
index.js,其中包含我们的应用程序的源代码。server.js,其中包含服务器部分(使用 Node.js 编写)。service-worker.js,其中包含服务工作线程特有的代码。
让我们来探讨一下所有这些内容。
index.js
index.js 文件首先注册服务工作线程。
navigator.serviceWorker
.register("service-worker.js")
.then((registration) => registration.pushManager.getSubscription())
.then((subscription) => {
// subscription part
});
它比我们在 js13kPWA 演示 中看到的服务工作线程要复杂一些。在这种特定情况下,注册后,我们使用注册对象进行订阅,然后使用生成的订阅对象来完成整个过程。
在注册部分,代码如下所示:
async (subscription) => {
if (subscription) {
return subscription;
}
};
如果用户已订阅,我们将返回订阅对象并继续到订阅部分。如果未订阅,我们将初始化一个新订阅。
const response = await fetch("./vapidPublicKey");
const vapidPublicKey = await response.text();
const convertedVapidKey = urlBase64ToUint8Array(vapidPublicKey);
应用程序获取服务器的公钥并将响应转换为文本;然后需要将其转换为 Uint8Array(以支持 Chrome)。要了解有关 VAPID 密钥的更多信息,您可以阅读 通过 Mozilla 的推送服务发送 VAPID 标识的 WebPush 通知 博客文章。
现在应用程序可以使用 PushManager 来订阅新用户。传递给 PushManager.subscribe() 方法的有两个选项——第一个是 userVisibleOnly: true,这意味着发送给用户的所有通知都将对他们可见,第二个是 applicationServerKey,它包含我们已成功获取并转换的 VAPID 密钥。
registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: convertedVapidKey,
});
现在让我们转到订阅部分——应用程序首先使用 Fetch 将订阅详细信息作为 JSON 发送到服务器。
fetch("./register", {
method: "post",
headers: {
"Content-type": "application/json",
},
body: JSON.stringify({ subscription }),
});
然后定义“订阅”按钮的 onclick 函数:
document.getElementById("doIt").onclick = () => {
const payload = document.getElementById("notification-payload").value;
const delay = document.getElementById("notification-delay").value;
const ttl = document.getElementById("notification-ttl").value;
fetch("./sendNotification", {
method: "post",
headers: {
"Content-type": "application/json",
},
body: JSON.stringify({
subscription,
payload,
delay,
ttl,
}),
});
};
单击按钮时,fetch 会要求服务器发送带有给定参数的通知:payload 是要在通知中显示的文本,delay 定义通知将显示前的延迟(以秒为单位),而 ttl 是生存时间设置,它允许通知在服务器上保留指定的时间(也以秒为单位)。
现在,我们来看下一个 JavaScript 文件。
server.js
服务器部分是用 Node.js 编写的,需要托管在合适的位置,这是一个单独的文章主题。我们在此仅提供高层概述。
使用 web-push 模块 来设置 VAPID 密钥,并在尚未提供密钥时可选地生成它们。
const webPush = require("web-push");
if (!process.env.VAPID_PUBLIC_KEY || !process.env.VAPID_PRIVATE_KEY) {
console.log(
"You must set the VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEY " +
"environment variables. You can use the following ones:",
);
console.log(webPush.generateVAPIDKeys());
return;
}
webPush.setVapidDetails(
"https://example.com",
process.env.VAPID_PUBLIC_KEY,
process.env.VAPID_PRIVATE_KEY,
);
接下来,一个模块定义并导出了应用程序需要处理的所有路由:获取 VAPID 公钥、注册以及发送通知。您可以看到来自 index.js 文件的变量:payload、delay 和 ttl。
module.exports = (app, route) => {
app.get(`${route}vapidPublicKey`, (req, res) => {
res.send(process.env.VAPID_PUBLIC_KEY);
});
app.post(`${route}register`, (req, res) => {
res.sendStatus(201);
});
app.post(`${route}sendNotification`, (req, res) => {
const subscription = req.body.subscription;
const payload = req.body.payload;
const options = {
TTL: req.body.ttl,
};
setTimeout(() => {
webPush
.sendNotification(subscription, payload, options)
.then(() => {
res.sendStatus(201);
})
.catch((error) => {
console.log(error);
res.sendStatus(500);
});
}, req.body.delay * 1000);
});
};
service-worker.js
我们最后要看的文件是服务工作线程。
self.addEventListener("push", (event) => {
const payload = event.data?.text() ?? "no payload";
event.waitUntil(
self.registration.showNotification("ServiceWorker Cookbook", {
body: payload,
}),
);
});
它所做的就是添加一个 push 事件的监听器,创建由从数据中获取的文本组成的 payload 变量(或在数据为空时创建一个用于使用的字符串),然后等待直到通知显示给用户。
如果您想了解更多关于示例的处理方式,请随意探索 Service Worker Cookbook 中的其余示例。那里有大量工作示例,展示了一般用法,还有 Web 推送、缓存策略、性能、离线工作等等。