当我开始 code8cn.com 时,我很快就面临着如何在新文章发布时通知用户的难题。最重要的是,我想在不依赖第三方服务的情况下完成所有这些工作。我认为这将是使用网络通知的绝佳机会。
本机浏览器通知仅在用户打开网站时才真正起作用。我希望所有用户在发生新事情时都能从服务器收到通知,无论网站是否打开。
但是如果用户关闭了页面,我们如何才能得到通知呢?为此,我们必须使用服务人员,并且我们需要存储订阅者的详细信息。在本文中,我将介绍如何做这些事情并最终创建您自己的推送通知系统。
Service Worker 快速介绍#
出于我们的目的,服务工作者归结为一个文件,即使网站关闭,该文件也可以从服务器捕获推送事件。这意味着我们可以在页面关闭但浏览器打开时运行 Javascript。这有很多用途(即预加载资源,以便网页加载更快),但至关重要的是,我们可以将其用作发送通知的设备。
用户必须同意允许整个过程发生,因此我们必须请求此访问权限。这个请求过程对我们来说有点像这样:
0. MongoDB#
由于本文使用 mongoDB,请确保您已按照此处的说明安装,然后再继续。如果您有另一个存储系统,那么不用担心,只需确保相应地调整模型和 NPM 包即可。
1. 请求 Vapid 密钥#
Vapid 密钥是我们用来验证只有我们正在使用的 Web 服务器才能发送通知的密钥。还有其他机制可以做到这一点,但让我们专注于 vapid 键,因为它是最直接的。我们要做的第一件事是安装我们将用于本教程的主 JS 包。为此,请在终端中运行:
npm i web-push -g
web-push generate-vapid-keys
上面的第二行应该输出一个私钥和公钥。这些是您的 Vapid 密钥,我们将在接下来的几个步骤中使用它们。
2. 客户端“点击”事件#
下一步是在客户端设置一段代码,向服务器发送请求。在我们的服务器上,我们将创建一个 /subscribe 路由来处理所有订阅。现在,让我们看一下客户端。请注意,我们需要在顶部插入我们的 vapid 公钥。
当用户点击“订阅”按钮时,我们在这里只做三件事:
- 我们注册服务工作者。
- 我们使用pushManager Javascript API 为用户创建新订阅。使用此方法为每个用户生成一个唯一的订阅对象。
- 我们将订阅发送到我们的服务器:
const vapidKey = 'Your Public Vapid Key';
document.getElementById('subscribe').addEventListener('click', async function(e) {
const registration = await navigator.serviceWorker.register('worker.js', {scope: '/'});
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(vapidKey)
});
await fetch('/subscribe', {
method: 'POST',
body: JSON.stringify(subscription),
headers: {
'content-type': 'application/json'
}
});
});
const urlBase64ToUint8Array = function(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/\-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
应用程序的 URL 需要转换为特定的形式,这就是第二个函数正在帮助我们做的事情。除此之外,当用户单击订阅按钮时,我们将向服务器发送一个请求(到 /subscribe),我们将在其中处理该请求。
在第 4 行,我们引用了一个 worker.js 文件。这是我们的服务人员。我们还需要创建该文件——我不会在这里列出它,但可以在 GitHub Repo 中找到它。它大约有 8 行,它处理来自我们服务器的传入消息并将它们作为 Web 通知输出。
3. 服务器端“商店订阅”#
我在 mongoDB 中为订阅创建了一个模型。这可以通过 GitHub Repo 访问,并且相对简单。然后让我们看一下 /subscribe 路由。我们在这里做了几件事:
- 我们设置了 Vapid 键。
- 我们散列订阅对象以用作我们的唯一键。
- 我们检查该订阅哈希是否已经存在于数据库中。
- 如果订阅文档不存在,我们在数据库中创建它。
- 否则,我们会回复适当的消息。
// Service Worker Notifications
const publicVapidKey = 'Public Vapid Key';
const privateVapidKey = 'Private Vapid Key';
webpush.setVapidDetails('mailto:someEmail@emailSite.com', publicVapidKey, privateVapidKey);
app.post('/subscribe', jsonParser, async function(req, res) {
try {
let hash = objectHash(req.body);
let subscription = req.body;
let checkSubscription = await Subscription.Subscription.find({ 'hash' : hash });
let theMessage = JSON.stringify({ title: 'You have already subscribed', body: 'Some body text here.' });
if(checkSubscription.length == 0) {
const newSubscription = new Subscription.Subscription({
hash: hash,
subscriptionEl: subscription
});
newSubscription.save(function (err) {
if (err) {
theMessage = JSON.stringify({ title: 'We ran into an error', body: 'Please try again later' });
webpush.sendNotification(subscription, payload).catch(function(error) {
console.error(error.stack);
});
res.status(400);
} else {
theMessage = JSON.stringify({ title: 'Thank you for Subscribing!', body: 'Some body text here' });
webpush.sendNotification(subscription, payload).catch(function(error) {
console.error(error.stack);
});
res.status(201);
}
});
} else {
webpush.sendNotification(subscription, theMessage).catch(function(error) {
console.error(error.stack);
});
res.status(400);
}
} catch(e) {
console.log(e);
}
});
太好了,所以现在我们有了订阅路线。我们所有的订阅都存储在我们的 MongoDB 数据库中,我们可以在需要时向它们发送通知。现在让我们尝试发送一些东西。
4. 发送通知#
请记住,由于我们加载了服务器工作者,我们的服务器现在可以与用户的浏览器交互,即使页面已关闭。
现在我们已经收集了我们的订阅者,我们想向他们发送一些东西。我们所要做的就是遍历它们并为每个人生成一个通知:
const sendNotifications = async function() {
const allSubscriptions = await Subscription.Subscription.find();
allSubscriptions.forEach(function(item) {
let ourMessage = JSON.stringify({
'title' : req.body.titles[0].title,
'body' : req.body.description
});
webpush.sendNotification(item.subscriptionEl, ourMessage).catch(function(error) {
console.error(error.stack);
});
});
}
5.添加安全性#
如果您正在运行它,请务必添加适合您的设置的正确安全性。例如,您可能希望使用 express-rate-limit 包来防止有人向订阅路由请求发送垃圾邮件。
同样,您需要确保在发送通知时对服务器进行安全检查,即至少有用户名和密码。
如果您将通知服务作为 API 运行,您可能需要遵循保护 API 的准则。我们不希望发生有人恶意向我们所有的订阅者发送通知。
本教程重点介绍如何创建推送通知服务,但设置您自己的用户身份验证系统将在其他教程中得到更好的说明。
结论#
我希望这不仅能让您了解如何制作通知系统,还可以了解 Service Worker 的力量。