使用 Service Worker 创建 NodeJS 推送通知系统

当我开始 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 的力量。

从 GitHub Repo 获取完整代码