发布于 2026-01-06 5 阅读
0

在您的 Web 应用程序中发送通知 DEV 的全球展示挑战赛,由 Mux 呈现:展示您的项目!

在您的 Web 应用中发送通知

由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!

你是否曾好奇过 YouTube、X 或任何你常用的浏览器应用是如何发送通知的?其实这并非魔法,而是有方法可以实现的。在本文中,我将介绍其中一种方法。

在 Web 开发领域,随着一些 Web API 的引入,现在可以轻松完成一些原本需要使用移动应用才能实现的功能,例如发送通知。借助通知 API 和推送 API 以及 Service Worker,在 Web 浏览器上发送通知比以往任何时候都更加便捷。

本文将介绍如何在您的网站/Web应用程序中发送通知,我将使用JavaScript来实现这一点。让我们开始吧!

通知分为两种类型:

  1. 本地通知:这是由您的应用自身生成的。

  2. 推送通知:这是由服务器(后端)通过推送事件生成的,例如,当您最喜欢的 YouTuber 刚刚发布视频时,即使您当前不在 YouTube Web 应用上,或者当您的互联网连接恢复时,您也会收到推送通知。

本文将逐一介绍这些内容。开始之前,我们需要了解哪些信息?

发送通知所需的组件
⦁ Service Worker:Service Worker 本质上充当代理服务器,位于 Web 应用程序、浏览器和网络(可用时)之间。它们的作用之一是实现有效的离线体验、拦截网络请求,并根据网络可用性采取相应的措施。它们还允许访问推送通知和后台同步 API。Service Worker 的工作原理可以举例说明:当您离线时,如果 YouTube 通知服务器向您的浏览器发送推送通知,您将无法收到通知,直到您重新连接到互联网。

⦁ 通知 API:此 API 用于向用户显示通知提示,就像在移动设备上一样,直接从您的 Web 应用程序显示,例如当用户点击按钮或在您的应用程序中执行操作时。

⦁ 推送 API:此 API 用于从服务器获取推送消息。

发送本地通知

我们将分三个步骤在您的 Web 应用中发送本地通知。⦁
我们需要使用 requestPermission 方法通过通知 API 请求用户发送通知的权限。

⦁ 如果获得权限,我们的服务工作线程 (Service Worker) 将监听推送事件。收到推送事件后,服务工作线程将被唤醒,并使用消息中的信息通过通知 API 显示通知。

⦁ 如果未授予权限,您也应该在代码中正确处理这种情况。

我们先来编写一个简单的示例代码,该页面实现了点击“通知我”按钮时发送本地通知的功能。



<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <button class="notify">Notify Me</button>
</body>
<script src="./script.js"></script>
</html>


Enter fullscreen mode Exit fullscreen mode

现在让我们来看我们的JavaScript代码……



const Notifybtn = document.querySelector(".notify");
const sendNotification = ()=>{
    if(!("Notification" in window)){
        throw new Error("Your browser does not support push notification");
    }
    Notification.requestPermission().then((Permission)=>{
        const notificationOptions = {
            body:"Welcome to Javascript Push Notification",
            icon:"./image.png"
        }
        new Notification("Push Notification",notificationOptions);
    })
};
Notifybtn.addEventListener("click", sendNotification);


Enter fullscreen mode Exit fullscreen mode

在上面的代码中,我们有一个 sendNotification 函数,它首先检查用户的浏览器是否支持通知,然后请求用户授予发送通知的权限。如果未获得权限,则无法发送通知。
要发送通知,我们使用 Notification 构造函数,并将通知标题作为第一个参数,通知选项作为第二个参数。运行此代码后,您将得到类似这样的结果。

基本本地通知
让我们更进一步,我们想要发送更具互动性的通知,用户可以点击按钮或进行我们想要的任何交互。让我们尝试修改代码以满足这一需求。



const sendNotification = ()=>{
    if(!("Notification" in window)){
        throw new Error("Your browser does not support push notification");
    }
    Notification.requestPermission().then((Permission)=>{
        const notificationOptions = {
            body:"Welcome to Javascript Push Notification",
            icon:"./image.png",
            actions: [
      {
        action: "thanks",
        title: "Thanks",
      },
      {
        action: "view_profile",
        title: "View Profile",
      },
    ],
        }
        new Notification("Push Notification",notificationOptions);
    })
};


Enter fullscreen mode Exit fullscreen mode

我们在这里为通知选项添加了 actions 属性,用户可以在通知上点击按钮进行交互。运行此代码时,您会收到如下错误:

当我们尝试使用通知构造函数创建可交互操作时,会收到错误。
错误信息非常明确,只支持在使用 Service Worker 时执行操作,我们该如何实现呢?

使用 Service Worker 发送通知

首先,我们需要创建 Service Worker 文件并注册 Service Worker。让我们创建一个 serviceWorker.js 文件,其中只包含一个简单的 console.log 代码。



//serviceWorker.js
console.log("Hello, welcome to service worker")


Enter fullscreen mode Exit fullscreen mode

现在我们可以开始注册我们的服务人员了。



const registerServiceWorker = async () => {
  if ("serviceWorker" in navigator) {
    try {
      const registration = await navigator.serviceWorker.register(
        "serviceWorker.js",
        {
          scope: "./",
        }
      );
      return registration;
    } catch (error) {
      console.error(`Registration failed with ${error}`);
    }
  }
};


Enter fullscreen mode Exit fullscreen mode

因此,现在需要修改我们的 sendNotification 函数,创建一个具有交互功能的通知……



// script.js
const sendNotification = async () => {
  let notificationOptions = {
    body: "Elon Musk sent you a friend request",
    icon: "./image.png",
   data: {
      requestId: "1234",
      username: "elonmusk"
    },
    actions: [
      {
        action: "accept",
        title: "Accept",
      },
      {
        action: "view_profile",
        title: "View Profile",
      },
    ],
    tag: "friend_request",
  };
  const sw = await registerServiceWorker();
  sw.showNotification("Friend Request", notificationOptions);
};


Enter fullscreen mode Exit fullscreen mode

这就是全部内容。

点击后似乎没有任何反应 :) 让我们通过监听 serviceWorker 中的 notificationclick 事件来进一步了解情况。



// serviceWorker.js

self.addEventListener("notificationclick", (event) => {
  event.notification.close();

  switch (event.notification.tag) {
  case "friend_request":{
    switch (event.action) {
      case "accept": {
        console.log("accept request API call.. with ", event.notification.data);
      }

      case "view_profile":
        {
          // direct to profile page
          event.waitUntil(
            clients
              .matchAll({
                type: "window",
                includeUncontrolled: true,
              })
              .then((windowClients) => {
                const matchingClient = windowClients.find(
                  (wc) => wc.url === urlToOpen
                );

                if (matchingClient) {
                  return matchingClient.focus();
                } else {
                  return clients.openWindow(event.notification.data.username);
                }
              })
          );
        }

        break;
      // Handle other actions ...
    }
  }
  }
});


Enter fullscreen mode Exit fullscreen mode

您可以看到,我们有两个操作:接受请求和查看个人资料。当通知的标签为“friend_request”时,我们会执行这些操作。我们基本上是在通知中传递数据,这一点需要注意。

发送推送通知

目前我们已经能够在 Web 应用中发送本地通知,这很棒,对吧?但大多数情况下,在构建实际应用时,你需要的是推送通知,也就是由服务器生成的通知。在本节中,我们将使用 ExpressJS 搭建后端服务器。让我们开始设置服务器吧。



// app.js

const express = require("express");
const cors = require("cors");
require("dotenv").config();

const { API_PREFIX } = process.env;
const app = express();
const { models } = require("./config/db");
const { sendNotification } = require("./utils/helper");
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(
  cors({
    origin: "*",
  })
);
app.disable("x-powered-by");
module.exports = app


Enter fullscreen mode Exit fullscreen mode


// index.js
const app = require("./app");
const { PORT } = process.env;
const connection = require("./config/db");
(async () => {
    await connection
      .sync()
      .then(() => {
        console.log("Database successfully connected");
      })
      .catch((err) => {
        console.log(err.message);
        return process.exit(1);
      });
    app.listen(PORT, () => {
        console.log(
          `<<<<<<<<<<<<<<<< Yeeeep,Server running on port ${PORT}..>>>>>>>>>>>`
        );
    });
  })();



Enter fullscreen mode Exit fullscreen mode

这样,我们的后端服务器就启动并运行了(附注:由于一些导入问题,这段代码片段无法正常工作;本文末尾提供了所有代码的 GitHub 仓库链接)。

让我们再次把回退方案转移到前端……为了能够发送推送通知,我们需要以下条件:
⦁ VAPID 密钥
⦁ Web 应用必须通过 Service Worker 订阅推送服务。

VAPID(自愿应用服务器标识 Web 推送)是一种规范,允许后端服务器向推送服务(浏览器特定的服务)标识自身。它是一种安全措施,可以防止其他人向应用程序的用户发送消息。

VAPID 密钥如何实现安全性?
简而言之:
您在应用服务器上生成一组私钥和公钥(VAPID 密钥)。
当前端应用尝试订阅推送服务时,会将公钥发送给推送服务。现在,推送服务可以从您的应用服务器(后端)获取到该公钥。订阅方法会为您的前端 Web 应用返回一个唯一的端点。您需要将此唯一端点存储在后端应用服务器上。
应用服务器会向您刚刚保存的推送服务端点发送 API 请求。在此 API 请求中,您需要使用私钥对 Authorization 标头中的一些信息进行签名。这使得推送服务能够验证请求是否来自正确的应用服务器。
验证标头后,推送服务会将消息发送到前端 Web 应用。

接下来,我们要生成 VAPID 密钥,需要在您的后端或全局环境中安装“web-push”依赖项……



npm i web-push


Enter fullscreen mode Exit fullscreen mode

安装完成后即可运行



npx web-push generate-vapid-keys


Enter fullscreen mode Exit fullscreen mode

你还有类似这样的东西。



Public Key:
<YOUR_PUBLIC_KEY>

Private Key:
<YOUR_PRIVATE_KEY>


Enter fullscreen mode Exit fullscreen mode

您只需生成一次即可。请务必妥善保管您的私钥。

现在让我们来看 serviceWorker.js 文件。
我们需要在 service worker 注册时订阅推送服务……



//serviceWorker.js
const urlB64ToUint8Array = (base64String) => {
  const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
  const base64 = (base64String + padding)
    .replace(/\-/g, "+")
    .replace(/_/g, "/");
  const rawData = atob(base64);
  const outputArray = new Uint8Array(rawData.length);
  for (let i = 0; i < rawData.length; ++i) {
    outputArray[i] = rawData.charCodeAt(i);
  }
  return outputArray;
};

self.addEventListener("activate", async () => {
  try {
    const applicationServerKey = urlB64ToUint8Array(
      "<YOUR_PUBLIC_KEY>"
    );
    const options = { applicationServerKey, userVisibleOnly: true };
    const subscription = await self.registration.pushManager.subscribe(options);
    console.log(JSON.stringify(subscription))
  } catch (err) {
    console.log("Error", err);
  }
});


Enter fullscreen mode Exit fullscreen mode

这里我们监听 Service Worker 的注册和激活事件,使用 VAPID 公钥,将其从 base64 字符串转换为订阅选项所需的数组缓冲区。此外,我们还设置了 userVisibleOnly 属性,并将其设置为 true。您必须始终将 userVisibleOnly 设置为 true。此参数限制开发者只能使用推送消息进行通知。也就是说,开发者无法使用推送消息在不显示通知的情况下静默地向服务器发送数据。目前,必须将其设置为 true,否则会收到权限被拒绝的错误。本质上,由于隐私和安全方面的考虑,目前不支持静默推送消息。

如果运行成功,你会看到类似这样的结果:



{
"endpoint":"https://fcm.googleapis.com/fcm/send/<some_strange_id>",
"expirationTime":null,
"keys":{"p256dh":"<some_key>","auth":"<some_id>"}
}


Enter fullscreen mode Exit fullscreen mode

我们应该将收到的这个响应保存到后端数据库中,所以让我们在后端创建两个端点,一个用于保存订阅,另一个用于发送通知。



//app.js

app.post(`/${API_PREFIX}save-subscription`, async (req, res) => {

  try {
    const subscription = JSON.stringify(req.body);
    console.log(subscription);
    const sub = await models.subscriptions.create({ subsription:subscription });
    res.status(201).json({ message: "Subscription Successful" });
  } catch (error) {
    res.status(500).json({ message: error.message });
  }
});

app.post(`/${API_PREFIX}send-notification`, async (req, res) => {
  try {
    const { id } = req.body;
    const sub = await models.subscriptions.findOne({where:{id}});
    const message = {
      body: "Elon Musk sent you a friend request",
      icon: "https://media.npr.org/assets/img/2022/06/01/ap22146727679490-6b4aeaa7fd9c9b23d41bbdf9711ba54ba1e7b3ae-s800-c85.webp",
      data: {
        requestId: "1234",
        username: "elonmusk"
      },
      actions: [
        {
          action: "accept",
          title: "Accept",
        },
        {
          action: "view_profile",
          title: "View Profile",
        },
      ],
      tag: "friend_request",
    };
    await sendNotification(sub.subsription, message);
    res.json({ message: "message sent" });    
  } catch (error) {
    res.status(500).json({ message: error.message });

  }

});


Enter fullscreen mode Exit fullscreen mode

我们还需要创建导入实用程序函数,以帮助发送推送通知。



// utils/helper.js
const webpush = require("web-push");
const { VAPID_PRIVATE_KEY, VAPID_PUBLIC_KEY } = process.env;
//setting our previously generated VAPID keys
webpush.setVapidDetails(
  "mailto:<your_email>",
  VAPID_PUBLIC_KEY,
  VAPID_PRIVATE_KEY
);
//function to send the notification to the subscribed device
const sendNotification = async (subscription, dataToSend) => {
  try {
   await webpush.sendNotification(subscription, JSON.stringify(dataToSend)); //string or Node Buffer
  } catch (error) {
    console.log(error); 
    throw new Error(error.message);
  }
};
module.exports = { sendNotification };


Enter fullscreen mode Exit fullscreen mode

我们有一个用于保存订阅的端点,还有一个用于发送通知的端点(注:这只是一个示例/演示项目,在实际场景中,您很可能不会使用专门的端点来发送通知,而是会使用一个实用程序服务(当某些操作发生时可以触发的类或函数)。
现在让我们回到前端进行同步,首先通过调用端点将推送服务订阅保存到数据库。



   // serviceWorker.js
const saveSubscription = async (subscription) => {
  const SERVER_URL = "http://localhost:5005/api/save-subscription";
  const response = await fetch(SERVER_URL, {
    method: "post",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(subscription),
  });
  return response.json();
};
self.addEventListener("activate", async () => {
  try {
    const applicationServerKey = urlB64ToUint8Array(
      "<YOUR_VAPID_PUBLIC_KEY>"
    );
    const options = { applicationServerKey, userVisibleOnly: true };
    const subscription = await self.registration.pushManager.subscribe(options);
    const response = await saveSubscription(subscription);
  } catch (err) {
    console.log("Error", err);
  }
});


Enter fullscreen mode Exit fullscreen mode

我们离目标只差几步了!现在让我们来实现 showNotification 函数,以便在收到推送通知时显示该通知。



// serviceWorker.js
const showLocalNotification = (title, data, swRegistration) => {
  swRegistration.showNotification(title, data);
};


Enter fullscreen mode Exit fullscreen mode

该函数接收三个参数:推送通知的标题、通知选项和 Service Worker 注册信息。最后,我们需要监听推送事件,即推送通知发出时发生的事件。



 // serviceWorker.js
self.addEventListener("push", function (event) {
  if (event.data) {
    console.log("Push event!! ", JSON.parse(event.data.text()));
    showLocalNotification(
      "Notification ",
      JSON.parse(event.data.text()),
      self.registration
    );
  } else {
    console.log("Push event but no data");
  }
});


Enter fullscreen mode Exit fullscreen mode

看看我们有什么:

从后端发送推送通知

我们还必须注意,当页面或浏览器刷新/重启后,Service Worker 仍然保持注册状态,您可能需要修改 Service Worker 文件中的一些内容,您可以调用此函数。




const unregisterServiceWorkers = ()=>{
  if (window.navigator && navigator.serviceWorker) {
    navigator.serviceWorker.getRegistrations().then(function (registrations) {
      for (let registration of registrations) {
        registration.unregister();
      }
    });
  }
}



Enter fullscreen mode Exit fullscreen mode

每当我们使用数据库生成的 ID 访问发送通知端点时,您的设备就会收到推送通知,是不是很棒?如果您因为推送通知功能而想开发移动应用,那么如果没有其他“取舍”,您可以考虑开发 Web 应用……

感谢阅读本文,希望您能有所收获。我已经将本文的代码示例上传到此仓库,请享用!

文章来源:https://dev.to/oluwatobi_/sending-notifications-in-your-web-apps-3iof