构建推送通知服务器

在此 Codelab 中,您将构建一个推送通知服务器。服务器将管理推送订阅列表,并向其发送通知。

客户端代码已完成,在本 Codelab 中,您将着手实现服务器端功能。

系统会自动屏蔽嵌入式 Glitch 应用的通知,因此您无法在此页面上预览该应用。请改为执行以下操作:

  1. 点击 Remix to Edit 即可修改项目。
  2. 如需预览网站,请按 View App(查看应用)。然后按 Fullscreen(全屏)全屏

实时应用会在新标签页中打开。在嵌入的 Glitch 中,点击查看源代码即可再次显示代码。

在完成此 Codelab 的过程中,请更改此页面中嵌入的 Glitch 中的代码。刷新包含实时应用的新标签页,即可查看更改。

首先,查看应用的客户端界面。

在新 Chrome 标签页中

  1. 按 `Control+Shift+J`(在 Mac 上为 `Command+Option+J`)打开 DevTools。 点击控制台标签页。

  2. 尝试点击界面中的按钮(查看 Chrome 开发者控制台中的输出)。

    • 注册服务工作器会为您的 Glitch 项目网址的作用域注册服务工作器。取消注册 Service Worker 会移除 Service Worker。如果有推送订阅附加到该应用,则推送订阅也会被停用。

    • 订阅推送用于创建推送订阅。只有在注册了 Service Worker 且客户端代码中存在 VAPID_PUBLIC_KEY 常量(稍后会详细介绍)时,此按钮才可用,因此您暂时无法点击它。

    • 如果您有有效的推送订阅,Notify current subscription 会请求服务器向其端点发送通知。

    • Notify all subscriptions 会指示服务器向其数据库中的所有订阅端点发送通知。

      请注意,其中一些端点可能处于非活动状态。订阅在服务器向其发送通知时消失的情况始终有可能发生。

我们来看看服务器端发生了什么。如需查看来自服务器代码的消息,请在 Glitch 界面中查看 Node.js 日志。

  • 在 Glitch 应用中,依次点击工具 -> 日志

    您可能会看到类似 Listening on port 3000 的消息。

    如果您在已发布的应用界面中尝试点击通知当前订阅通知所有订阅,则还会看到以下消息:

    TODO: Implement sendNotifications()
    Endpoints to send to:  []

现在,我们来看一些代码。

  • public/index.js 包含完成的客户端代码。它会执行功能检测、注册和取消注册服务工件,并控制用户对推送通知的订阅。它还会将有关新订阅和已删除订阅的信息发送到服务器。

    由于您只需要处理服务器功能,因此无需修改此文件(除了填充 VAPID_PUBLIC_KEY 常量之外)。

  • public/service-worker.js 是一个简单的 Service Worker,用于捕获推送事件并显示通知。

  • /views/index.html 包含应用界面。

  • .env 包含 Glitch 在启动时加载到应用服务器中的环境变量。您将使用身份验证详细信息填充 .env,以便发送通知。

  • server.js 是您在此 Codelab 中完成大部分工作的文件。

    启动代码会创建一个简单的 Express Web 服务器。有四项 TODO 待办事项,在代码注释中用 TODO: 标记。您需要执行的操作:

    在此 Codelab 中,您将逐个完成这些 TODO 项。

生成和加载 VAPID 详细信息

第一个 TODO 项是生成 VAPID 详细信息,将其添加到 Node.js 环境变量,并使用新值更新客户端和服务器代码。

背景

用户订阅通知时,需要信任应用及其服务器的身份。用户还需要确信,他们收到的通知来自设置订阅的同一应用。他们还需要信任,其他任何人都无法读取通知内容。

用于确保推送通知安全且私密的协议称为“网络推送自主应用服务器标识 (VAPID)”。VAPID 使用公钥加密来验证应用、服务器和订阅端点的身份,并加密通知内容。

在此应用中,您将使用 web-push npm 软件包生成 VAPID 密钥,并加密和发送通知。

实现

在此步骤中,为您的应用生成一对 VAPID 密钥,并将其添加到环境变量中。在服务器中加载环境变量,并将公钥作为常量添加到客户端代码中。

  1. 使用 web-push 库的 generateVAPIDKeys 函数创建一对 VAPID 密钥。

    server.js 中,移除以下代码行周围的注释:

    server.js

    // Generate VAPID keys (only do this once).
    /*
     * const vapidKeys = webpush.generateVAPIDKeys();
     * console.log(vapidKeys);
     */

    const vapidKeys = webpush.generateVAPIDKeys();
    console
    .log(vapidKeys);
  2. Glitch 重启您的应用后,会将生成的密钥输出到 Glitch 界面中的 Node.js 日志(而非 Chrome 控制台)。如需查看 VAPID 密钥,请在 Glitch 界面中依次选择 Tools -> Logs

    请务必从同一密钥对中复制公钥和私钥!

    Glitch 会在您每次修改代码时重启应用,因此随着更多输出内容的出现,您生成的第一对密钥可能会滚动到视野之外。

  3. .env 中,复制并粘贴 VAPID 密钥。将键括在双引号 ("...") 中。

    对于 VAPID_SUBJECT,您可以输入 "mailto:test@test.test"

    .env

    # process.env.SECRET
    VAPID_PUBLIC_KEY
    =
    VAPID_PRIVATE_KEY
    =
    VAPID_SUBJECT
    =
    VAPID_PUBLIC_KEY
    ="BN3tWzHp3L3rBh03lGLlLlsq..."
    VAPID_PRIVATE_KEY
    ="I_lM7JMIXRhOk6HN..."
    VAPID_SUBJECT
    ="mailto:test@test.test"
  4. server.js 中,再次注释掉这两行代码,因为您只需生成一次 VAPID 密钥。

    server.js

    // Generate VAPID keys (only do this once).
    /*
    const vapidKeys = webpush.generateVAPIDKeys();
    console.log(vapidKeys);
    */

    const vapidKeys = webpush.generateVAPIDKeys();
    console
    .log(vapidKeys);
  5. server.js 中,从环境变量加载 VAPID 详细信息。

    server.js

    const vapidDetails = {
     
    // TODO: Load VAPID details from environment variables.
      publicKey
    : process.env.VAPID_PUBLIC_KEY,
      privateKey
    : process.env.VAPID_PRIVATE_KEY,
      subject
    : process.env.VAPID_SUBJECT
    }
  6. 公钥复制并粘贴到客户端代码中。

    public/index.js 中,为 VAPID_PUBLIC_KEY 输入与您复制到 .env 文件中的值相同的值:

    public/index.js

    // Copy from .env
    const VAPID_PUBLIC_KEY = '';
    const VAPID_PUBLIC_KEY = 'BN3tWzHp3L3rBh03lGLlLlsq...';
    ````

实现发送通知的功能

背景

在此应用中,您将使用 web-push npm 软件包发送通知。

此软件包会在调用 webpush.sendNotification() 时自动加密通知,因此您无需担心。

web-push 接受多种通知选项,例如,您可以将标头附加到消息,并指定内容编码。

在此 Codelab 中,您将仅使用以下两行代码定义的两个选项:

let options = {
  TTL
: 10000; // Time-to-live. Notifications expire after this.
  vapidDetails
: vapidDetails; // VAPID keys from .env
};

TTL(存留时间)选项用于设置通知的到期超时时间。这样,服务器便可避免在通知不再相关时向用户发送通知。

vapidDetails 选项包含您从环境变量加载的 VAPID 密钥。

实现

server.js 中,修改 sendNotifications 函数,如下所示:

server.js

function sendNotifications(database, endpoints) {
 
// TODO: Implement functionality to send notifications.
  console
.log('TODO: Implement sendNotifications()');
  console
.log('Endpoints to send to: ', endpoints);
  let notification
= JSON.stringify(createNotification());
  let options
= {
    TTL
: 10000, // Time-to-live. Notifications expire after this.
    vapidDetails
: vapidDetails // VAPID keys from .env
 
};
  endpoints
.map(endpoint => {
    let subscription
= database[endpoint];
    webpush
.sendNotification(subscription, notification, options);
 
});
}

由于 webpush.sendNotification() 会返回 promise,因此您可以轻松添加错误处理。

server.js 中,再次修改 sendNotifications 函数:

server.js

function sendNotifications(database, endpoints) {
  let notification
= JSON.stringify(createNotification());
  let options
= {
    TTL
: 10000; // Time-to-live. Notifications expire after this.
    vapidDetails
: vapidDetails; // VAPID keys from .env
 
};
  endpoints
.map(endpoint => {
    let subscription
= database[endpoint];
    webpush
.sendNotification(subscription, notification, options);
    let 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.body} `);
   
});
 
});
}

处理新订阅

背景

当用户订阅推送通知时,会发生以下情况:

  1. 用户点击订阅推送

  2. 客户端使用 VAPID_PUBLIC_KEY 常量(服务器的公开 VAPID 密钥)生成一个特定于服务器的唯一 subscription 对象。subscription 对象如下所示:

       {
         
    "endpoint": "https://fcm.googleapis.com/fcm/send/cpqAgzGzkzQ:APA9...",
         
    "expirationTime": null,
         
    "keys":
         
    {
           
    "p256dh": "BNYDjQL9d5PSoeBurHy2e4d4GY0sGJXBN...",
           
    "auth": "0IyyvUGNJ9RxJc83poo3bA"
         
    }
       
    }
  3. 客户端向 /add-subscription 网址发送 POST 请求,并在正文中将订阅作为字符串化 JSON 包含在内。

  4. 服务器从 POST 请求的正文中检索字符串化的 subscription,将其解析回 JSON,然后将其添加到订阅数据库。

    数据库会使用自己的端点作为键来存储订阅:

    {
     
"https://fcm...1234": {
        endpoint
: "https://fcm...1234",
        expirationTime
: ...,
        keys
: { ... }
     
},
     
"https://fcm...abcd": {
        endpoint
: "https://fcm...abcd",
        expirationTime
: ...,
        keys
: { ... }
     
},
     
"https://fcm...zxcv": {
        endpoint
: "https://fcm...zxcv",
        expirationTime
: ...,
        keys
: { ... }
     
},
   
}

现在,服务器可以使用新订阅发送通知。

实现

新订阅请求会发送到 /add-subscription 路由,该路由是一个 POST 网址。您会在 server.js 中看到一个桩路由处理程序:

server.js

app.post('/add-subscription', (request, response) => {
 
// TODO: implement handler for /add-subscription
  console
.log('TODO: Implement handler for /add-subscription');
  console
.log('Request body: ', request.body);
  response
.sendStatus(200);
});

在您的实现中,此处理程序必须:

  • 从请求正文中检索新订阅。
  • 访问有效订阅的数据库。
  • 将新订阅添加到有效订阅列表中。

如需处理新订阅,请执行以下操作

  • server.js 中,修改 /add-subscription 的路由处理程序,如下所示:

    server.js

    app.post('/add-subscription', (request, response) => {
     
// TODO: implement handler for /add-subscription
      console
.log('TODO: Implement handler for /add-subscription');
      console
.log('Request body: ', request.body);
      let subscriptions
= Object.assign({}, request.session.subscriptions);
      subscriptions
[request.body.endpoint] = request.body;
      request
.session.subscriptions = subscriptions;
      response
.sendStatus(200);
   
});

处理订阅取消

背景

服务器并不总是知道订阅何时变为非活动状态,例如,当浏览器关闭服务工作器时,订阅可能会被清除。

不过,服务器可以通过应用界面了解已取消的订阅。在此步骤中,您将实现从数据库中移除订阅的功能。

这样,服务器就可以避免向不存在的端点发送大量通知。显然,对于简单的测试应用,这并不重要,但对于更大规模的应用,这会变得重要。

实现

取消订阅的请求会发送到 /remove-subscription POST 网址。

server.js 中的桩路由处理程序如下所示:

server.js

app.post('/remove-subscription', (request, response) => {
 
// TODO: implement handler for /remove-subscription
  console
.log('TODO: Implement handler for /remove-subscription');
  console
.log('Request body: ', request.body);
  response
.sendStatus(200);
});

在您的实现中,此处理脚本必须:

  • 从请求的正文中检索已取消订阅的端点。
  • 访问有效订阅的数据库。
  • 从有效订阅列表中移除已取消的订阅。

来自客户端的 POST 请求正文包含您需要移除的端点:

{
 
"endpoint": "https://fcm.googleapis.com/fcm/send/cpqAgzGzkzQ:APA9..."
}

如需处理订阅取消,请执行以下操作

  • server.js 中,修改 /remove-subscription 的路由处理程序,如下所示:

    server.js

  app.post('/remove-subscription', (request, response) => {
   
// TODO: implement handler for /remove-subscription
    console
.log('TODO: Implement handler for /remove-subscription');
    console
.log('Request body: ', request.body);
    let subscriptions
= Object.assign({}, request.session.subscriptions);
   
delete subscriptions[request.body.endpoint];
    request
.session.subscriptions = subscriptions;
    response
.sendStatus(200);
 
});