Codelab:构建推送通知服务器

凯特·杰弗里斯
Kate Jeffreys

此 Codelab 会逐步向您展示如何构建推送通知服务器。 完成此 Codelab 后,您将拥有一个具有以下功能的服务器:

  • 跟踪推送通知订阅(即,在客户端选择接收推送通知时,服务器会创建新的数据库记录,并在客户端选择停用时删除现有的数据库记录)
  • 向单个客户端发送推送通知
  • 向所有订阅的客户端发送推送通知

此 Codelab 侧重于通过实操来帮助您学习,因此不会过多介绍相关概念。如需了解推送通知概念,请查看推送通知的工作原理

此 Codelab 的客户端代码已经完成。在此 Codelab 中,您将仅实现服务器。如需了解如何实现推送通知客户端,请参阅 Codelab:构建推送通知客户端

如需查看完整代码,请访问 push-notifications-server-codelab-complete源代码)。

浏览器兼容性

此 Codelab 已知支持以下操作系统和浏览器组合:

  • Windows:Chrome、Edge
  • macOS:Chrome、Firefox
  • Android:Chrome、Firefox

已知此 Codelab 不支持以下操作系统(或操作系统和浏览器组合):

  • macOS:Brave、Edge、Safari
  • iOS

应用堆栈

  • 该服务器基于 Express.js 构建而成。
  • web-push Node.js 库可处理所有推送通知逻辑。
  • 使用 lowdb 将订阅数据写入 JSON 文件。

您无需使用上述任何技术来实现推送通知。 我们之所以选择这些技术,是因为它们提供了可靠的 Codelab 体验。

初始设置

获取可编辑的代码副本

在此 Codelab 中,您在这些说明右侧看到的代码编辑器称为 Glitch 界面

  1. 点击 Remix to Edit 使项目可修改。

设置身份验证

您需要先使用身份验证密钥设置服务器和客户端,然后才能使推送通知正常运行。如需了解原因,请参阅为 Web 推送协议请求签名

  1. 点击 Tools,然后点击 Terminal 以打开 Glitch 终端。
  2. 在终端中,运行 npx web-push generate-vapid-keys。复制私钥和公钥值。
  3. 打开 .env 并更新 VAPID_PUBLIC_KEYVAPID_PRIVATE_KEY。将 VAPID_SUBJECT 设置为 mailto:test@test.test。所有这些值都应该用英文双引号引起来。进行更新后,您的 .env 文件应如下所示:
VAPID_PUBLIC_KEY="BKiwTvD9HA…"
VAPID_PRIVATE_KEY="4mXG9jBUaU…"
VAPID_SUBJECT="mailto:test@test.test"
  1. 关闭 Glitch 终端。
  1. 打开 public/index.js
  2. VAPID_PUBLIC_KEY_VALUE_HERE 替换为您的公钥的值。

管理订阅

您的客户端会处理大部分订阅流程。您的服务器需要执行的主要操作是保存新的推送通知订阅并删除旧订阅。这些订阅使您能够将来向客户端推送消息。如需详细了解订阅流程,请参阅为客户端订阅推送通知

保存新订阅信息

  1. 如需预览网站,请按查看应用,然后按全屏 全屏
  1. 在应用标签页中点击 Register Service Worker。在状态框中,您应该会看到类似于以下内容的消息:
Service worker registered. Scope: https://desert-cactus-sunset.glitch.me/
  1. 在应用标签页中,点击订阅推送。您的浏览器或操作系统可能会询问您是否允许网站向您发送推送通知。点击允许(或您的浏览器/操作系统使用的任何对等短语)。在状态框中,您应该会看到类似于以下内容的消息:
Service worker subscribed to push.  Endpoint: https://fcm.googleapis.com/fcm/send/…
  1. 点击 Glitch 界面中的 View Source 以返回您的代码。
  2. 点击工具,然后点击日志,打开 Glitch 日志。您应该会看到 /add-subscription,后跟一些数据。/add-subscription 是客户端想要订阅推送通知时向其发送 POST 请求的网址。后面的数据是您需要保存的客户端订阅信息。
  3. 打开 server.js
  4. 使用以下代码更新 /add-subscription 路由处理程序逻辑:
app.post('/add-subscription', (request, response) => {
  console.log('/add-subscription');
  console.log(request.body);
  console.log(`Subscribing ${request.body.endpoint}`);
  db.get('subscriptions')
    .push(request.body)
    .write();
  response.sendStatus(200);
});

删除旧订阅信息

  1. 返回应用标签页。
  2. 点击退订推送
  3. 再次查看故障日志。您应该会看到 /remove-subscription,后跟客户端的订阅信息。
  4. 使用以下代码更新 /remove-subscription 路由处理程序逻辑:
app.post('/remove-subscription', (request, response) => {
  console.log('/remove-subscription');
  console.log(request.body);
  console.log(`Unsubscribing ${request.body.endpoint}`);
  db.get('subscriptions')
    .remove({endpoint: request.body.endpoint})
    .write();
  response.sendStatus(200);
});

发送通知

发送推送消息中所述,您的服务器实际上并不会直接向客户端发送推送消息,而是依靠推送服务来完成这项工作。您的服务器基本上只是通过向用户所使用的浏览器供应商拥有的网络服务(推送服务)发出网络服务请求(Web 推送协议请求),从而启动向客户端推送消息的过程。

  1. 使用以下代码更新 /notify-me 路由处理程序逻辑:
app.post('/notify-me', (request, response) => {
  console.log('/notify-me');
  console.log(request.body);
  console.log(`Notifying ${request.body.endpoint}`);
  const subscription = 
      db.get('subscriptions').find({endpoint: request.body.endpoint}).value();
  sendNotifications([subscription]);
  response.sendStatus(200);
});
  1. 使用以下代码更新 sendNotifications() 函数:
function sendNotifications(subscriptions) {
  // TODO
  // Create the notification content.
  const notification = JSON.stringify({
    title: "Hello, Notifications!",
    options: {
      body: `ID: ${Math.floor(Math.random() * 100)}`
    }
  });
  // Customize how the push service should attempt to deliver the push message.
  // And provide authentication information.
  const options = {
    TTL: 10000,
    vapidDetails: vapidDetails
  };
  // Send a push message to each client specified in the subscriptions array.
  subscriptions.forEach(subscription => {
    const endpoint = subscription.endpoint;
    const id = endpoint.substr((endpoint.length - 8), endpoint.length);
    webpush.sendNotification(subscription, notification, options)
      .then(result => {
        console.log(`Endpoint ID: ${id}`);
        console.log(`Result: ${result.statusCode}`);
      })
      .catch(error => {
        console.log(`Endpoint ID: ${id}`);
        console.log(`Error: ${error} `);
      });
  });
}
  1. 使用以下代码更新 /notify-all 路由处理程序逻辑:
app.post('/notify-all', (request, response) => {
  console.log('/notify-all');
  response.sendStatus(200);
  console.log('Notifying all subscribers');
  const subscriptions =
      db.get('subscriptions').cloneDeep().value();
  if (subscriptions.length > 0) {
    sendNotifications(subscriptions);
    response.sendStatus(200);
  } else {
    response.sendStatus(409);
  }
});
  1. 返回应用标签页。
  2. 点击退订推送,然后再次点击订阅推送。 此操作之所以必要,是因为如前所述,每当您修改代码时,Glitch 都会重启项目,并且项目配置为在启动时删除数据库。
  3. 点击通知我。您应该会收到一条推送通知。标题应为 Hello, Notifications!,正文应为 ID: <ID>,其中 <ID> 为随机数。
  4. 在其他浏览器或设备上打开您的应用,尝试为它们订阅推送通知,然后点击全部通知按钮。您应该会在所有订阅设备上收到相同的通知(即推送通知正文中的 ID 应相同)。

后续步骤

  • 阅读推送通知概览,更深入地了解推送通知的工作原理。
  • 查看 Codelab:构建推送通知客户端,了解如何构建一个客户端来请求通知权限、为设备订阅接收推送通知、使用 Service Worker 接收推送消息并将消息显示为通知。