如何使用通知和推送使 PWA 重新参与
两个 API,一个目标
该 Push API 和 Notifications API 是两个独立的 API,但在您想要在应用中提供引人入胜的功能时,它们可以很好地协同工作。Push 用于在没有任何客户端干预的情况下将新内容从服务器传递到应用,并且其操作由应用的服务工作者处理。通知可以由服务工作者用于向用户显示新信息,或者至少在某些内容更新时提醒他们。
它们在浏览器窗口之外工作,就像服务工作者一样,因此更新可以在应用页面失去焦点甚至关闭时推送,并且通知可以显示。
通知
让我们从通知开始 - 它们可以独立工作,但与推送结合使用时会更有用。让我们首先看一下孤立的通知。
请求权限
要显示通知,我们必须首先请求显示通知的权限。但是,最佳实践建议我们不要立即显示通知,而应该在用户通过单击按钮请求通知时显示弹出窗口。
const button = document.getElementById("notifications");
button.addEventListener("click", () => {
Notification.requestPermission().then((result) => {
if (result === "granted") {
randomNotification();
}
});
});
这将使用操作系统自己的通知服务显示弹出窗口
当用户确认接收通知时,应用就可以向他们显示通知。用户操作的结果可以是默认、已授予或已拒绝。当用户不做出选择时,将选择默认选项,而当用户分别点击“是”或“否”时,将设置另外两个选项。
当接受后,该权限对通知和推送都有效。
创建通知
示例应用会从可用数据中创建一个通知 - 随机选择一个游戏,并使用选定的游戏为通知提供内容:它将游戏的名称设置为标题,在正文中提到作者,并将图像显示为图标
function randomNotification() {
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,
};
new Notification(notifTitle, options);
setTimeout(randomNotification, 30000);
}
每 30 秒就会创建一个新的随机通知,直到它变得过于烦人并被用户禁用。(对于真正的应用,通知的频率应该低得多,并且更有用。)Notifications API 的优势在于它使用了操作系统的通知功能。这意味着即使用户没有查看 Web 应用,也可以向他们显示通知,并且通知看起来与原生应用显示的通知相似。
推送
推送比通知更复杂 - 我们需要订阅一个服务器,然后该服务器会将数据发送回应用。应用的服务工作者将从推送服务器接收数据,然后可以使用通知系统或其他机制(如果需要)显示这些数据。
该技术还处于非常早期的阶段 - 一些工作示例使用 Google Cloud Messaging 平台,但正在被重写以支持 VAPID(自愿应用标识),它为您的应用提供了额外的安全层。您可以检查 Service Workers Cookbook 示例,尝试使用 Firebase 设置推送消息服务器,或构建您自己的服务器(例如使用 Node.js)。
如前所述,要能够接收推送消息,您必须有一个服务工作者,其基础知识已在 使用服务工作者使 PWA 离线工作 文章中进行了说明。在服务工作者内部,通过调用 getSubscription()
方法创建推送服务订阅机制 PushManager
接口。
navigator.serviceWorker.register("service-worker.js").then((registration) => {
return registration.pushManager.getSubscription().then(/* ... */);
});
用户订阅后,他们就可以从服务器接收推送通知。
在服务器端,出于安全原因,整个过程必须使用公钥和私钥加密 - 允许每个人使用您的应用不安全地发送推送消息是一个糟糕的主意。有关保护服务器的详细信息,请参阅 Web Push 数据加密测试页面。服务器存储用户订阅时接收的所有信息,以便以后在需要时发送消息。
要接收推送消息,我们可以在服务工作者文件中监听 push
事件
self.addEventListener("push", (e) => {
/* ... */
});
可以检索数据,然后立即将其显示为用户通知。例如,这可以用于提醒用户某些事情,或让他们知道应用中是否有新内容可用。
推送示例
推送需要服务器部分才能工作,因此我们无法在托管在 GitHub Pages 上的 js13kPWA 示例中包含它,因为它只提供静态文件的托管。所有内容都在 Service Worker Cookbook 中进行了说明 - 请参阅 推送有效负载演示。
此演示包含三个文件
- index.js,其中包含我们应用的源代码
- server.js,其中包含服务器部分(使用 Node.js 编写)
- service-worker.js,其中包含服务工作者特定代码。
让我们探索所有这些
index.js
index.js
文件首先注册服务工作者
navigator.serviceWorker
.register("service-worker.js")
.then((registration) => {
return registration.pushManager
.getSubscription()
.then(async (subscription) => {
// registration part
});
})
.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 标识的 Web 推送通知 博客文章。
应用现在可以使用 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 密钥,如果 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
我们将要查看的最后一个文件是 service worker
self.addEventListener("push", (event) => {
const payload = event.data?.text() ?? "no payload";
event.waitUntil(
self.registration.showNotification("ServiceWorker Cookbook", {
body: payload,
}),
);
});
它所做的只是为 push
事件添加一个监听器,创建包含从数据中获取的文本的有效载荷变量(如果数据为空,则创建一个要使用的字符串),然后等待通知显示给用户。
如果您想了解这些示例是如何处理的,请随时探索 Service Worker Cookbook 中的其余示例。这里有一大批工作示例,展示了通用用法,以及网络推送、缓存策略、性能、离线工作等等。