订阅用户

第一步是获得用户发送推送消息的许可,然后我们才能获得 PushSubscription

执行此操作的 JavaScript API 相当简单,因此让我们逐步了解逻辑流程。

功能检测

首先,我们需要检查当前浏览器是否实际支持推送消息。我们可以通过两个简单的检查来检查是否支持推送。

  1. 检查 navigator 上的 serviceWorker
  2. 检查 window 上的 PushManager
if (!('serviceWorker' in navigator)) {
  // Service Worker isn't supported on this browser, disable or hide UI.
  return;
}

if (!('PushManager' in window)) {
  // Push isn't supported on this browser, disable or hide UI.
  return;
}

虽然浏览器对 Service Worker 和推送消息的支持都在快速增长,但对两者进行功能检测并进行渐进增强始终是一个好主意。

注册 Service Worker

通过功能检测,我们知道 Service Worker 和 Push 都受支持。下一步是“注册”我们的 Service Worker。

当我们注册 Service Worker 时,我们是在告诉浏览器我们的 Service Worker 文件在哪里。该文件仍然只是 JavaScript,但浏览器将“授予其访问”Service Worker API(包括推送)的权限。更准确地说,浏览器在 Service Worker 环境中运行该文件。

要注册 Service Worker,请调用 navigator.serviceWorker.register(),传入我们文件的路径。就像这样

function registerServiceWorker() {
  return navigator.serviceWorker
    .register('/service-worker.js')
    .then(function (registration) {
      console.log('Service worker successfully registered.');
      return registration;
    })
    .catch(function (err) {
      console.error('Unable to register service worker.', err);
    });
}

此函数告诉浏览器我们有一个 Service Worker 文件及其位置。在本例中,Service Worker 文件位于 /service-worker.js。在后台,浏览器将在调用 register() 后执行以下步骤

  1. 下载 Service Worker 文件。

  2. 运行 JavaScript。

  3. 如果一切运行正常且没有错误,则 register() 返回的 Promise 将解析。如果出现任何类型的错误,Promise 将拒绝。

如果 register() 确实拒绝,请在 Chrome DevTools 中仔细检查您的 JavaScript 中是否存在拼写错误/错误。

register() 解析时,它会返回一个 ServiceWorkerRegistration。我们将使用此注册来访问 PushManager API

PushManager API 浏览器兼容性

浏览器支持

  • Chrome: 42.
  • Edge: 17.
  • Firefox: 44.
  • Safari: 16.

来源

请求权限

我们已经注册了 Service Worker,并准备好订阅用户,下一步是从用户那里获得发送推送消息的权限。

获取权限的 API 相对简单,缺点是 API 最近从接受回调更改为返回 Promise。问题在于,我们无法判断当前浏览器实现了哪个版本的 API,因此您必须同时实现和处理两者。

function askPermission() {
  return new Promise(function (resolve, reject) {
    const permissionResult = Notification.requestPermission(function (result) {
      resolve(result);
    });

    if (permissionResult) {
      permissionResult.then(resolve, reject);
    }
  }).then(function (permissionResult) {
    if (permissionResult !== 'granted') {
      throw new Error("We weren't granted permission.");
    }
  });
}

在上面的代码中,重要的代码片段是对 Notification.requestPermission() 的调用。此方法将向用户显示提示

Permission prompt on desktop and mobile Chrome.

一旦用户通过按下“允许”、“阻止”或只是关闭权限提示进行交互,我们将获得一个字符串结果:'granted''default''denied'

在上面的示例代码中,如果权限被授予,则 askPermission() 返回的 Promise 将解析,否则我们将抛出一个错误,使 Promise 拒绝。

您需要处理的一个边缘情况是用户单击“阻止”按钮。如果发生这种情况,您的 Web 应用程序将无法再次请求用户权限。他们必须通过更改其权限状态来手动“取消阻止”您的应用,这隐藏在设置面板中。请仔细考虑您何时以及如何请求用户权限,因为如果他们单击“阻止”,则很难撤消该决定。

好消息是,只要用户知道为什么要请求权限,大多数用户都乐于授予权限。

稍后我们将介绍一些流行的网站如何请求权限。

使用 PushManager 订阅用户

一旦我们注册了 Service Worker 并获得了权限,我们就可以通过调用 registration.pushManager.subscribe() 来订阅用户。

function subscribeUserToPush() {
  return navigator.serviceWorker
    .register('/service-worker.js')
    .then(function (registration) {
      const subscribeOptions = {
        userVisibleOnly: true,
        applicationServerKey: urlBase64ToUint8Array(
          'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U',
        ),
      };

      return registration.pushManager.subscribe(subscribeOptions);
    })
    .then(function (pushSubscription) {
      console.log(
        'Received PushSubscription: ',
        JSON.stringify(pushSubscription),
      );
      return pushSubscription;
    });
}

当调用 subscribe() 方法时,我们传入一个选项对象,其中包含必需参数和可选参数。

让我们看看我们可以传入的所有选项。

userVisibleOnly 选项

当首次将推送添加到浏览器时,对于开发人员是否应该能够发送推送消息而不显示通知存在不确定性。这通常被称为静默推送,因为用户不知道后台发生了什么。

令人担忧的是,开发人员可能会做一些恶意的事情,例如在用户不知情的情况下持续跟踪用户的位置。

为了避免这种情况,并给规范作者时间考虑如何最好地支持此功能,添加了 userVisibleOnly 选项,并且传入值 true 是与浏览器达成象征性协议,即 Web 应用程序将在每次收到推送时显示通知(即,没有静默推送)。

目前,您必须传入值 true。如果您不包含 userVisibleOnly 键或传入 false,您将收到以下错误

Chrome 当前仅支持用于将导致用户可见消息的订阅的 Push API。您可以通过调用 pushManager.subscribe({userVisibleOnly: true}) 来指示这一点。有关更多详细信息,请参阅 https://goo.gl/yqv4Q4

目前看来,Chrome 中永远不会实现全面的静默推送。相反,规范作者正在探索预算 API 的概念,该 API 将根据 Web 应用程序的使用情况为 Web 应用程序提供一定数量的静默推送消息。

applicationServerKey 选项

我们在上一节中简要提到了“应用程序服务器密钥”。“应用程序服务器密钥”由推送服务用于识别订阅用户的应用程序,并确保同一应用程序正在向该用户发送消息。

应用程序服务器密钥是您的应用程序独有的公钥和私钥对。私钥应保密于您的应用程序,公钥可以自由共享。

传入 subscribe() 调用的 applicationServerKey 选项是应用程序的公钥。浏览器在订阅用户时将此公钥传递给推送服务,这意味着推送服务可以将您的应用程序的公钥与用户的 PushSubscription 相关联。

下图说明了这些步骤。

  1. 您的 Web 应用程序在浏览器中加载,并且您调用 subscribe(),传入您的公共应用程序服务器密钥。
  2. 然后,浏览器向推送服务发出网络请求,推送服务将生成一个端点,将此端点与应用程序的公钥关联,并将端点返回给浏览器。
  3. 浏览器会将此端点添加到 PushSubscription,该端点通过 subscribe() Promise 返回。

Illustration of the public application server key is used in subscribe
method.

当您稍后想要发送推送消息时,您需要创建一个 Authorization 标头,其中将包含使用您的应用程序服务器的私钥签名的信息。当推送服务收到发送推送消息的请求时,它可以通过查找链接到接收请求的端点的公钥来验证此签名的 Authorization 标头。如果签名有效,则推送服务知道它必须来自具有匹配私钥的应用程序服务器。这基本上是一种安全措施,可防止其他任何人向应用程序的用户发送消息。

How the private application server key is used when sending a
message

从技术上讲,applicationServerKey 是可选的。但是,Chrome 上最简单的实现需要它,并且其他浏览器将来可能也需要它。它在 Firefox 上是可选的。

定义应用程序服务器密钥应该是什么的规范是 VAPID 规范。每当您阅读到引用“应用程序服务器密钥”“VAPID 密钥”的内容时,只需记住它们是同一件事。

如何创建应用程序服务器密钥

您可以通过访问 web-push-codelab.glitch.me 创建一组公共和私有的应用程序服务器密钥,或者您可以使用 web-push 命令行 通过执行以下操作来生成密钥

    $ npm install -g web-push
    $ web-push generate-vapid-keys

您只需为您的应用程序创建一次这些密钥,只需确保将私钥保密即可。(是的,我刚刚说过。)

权限和 subscribe()

调用 subscribe() 有一个副作用。如果您的 Web 应用程序在调用 subscribe() 时没有显示通知的权限,浏览器将为您请求权限。如果您的 UI 以此流程工作,这将很有用,但是如果您想要更多控制权(我认为大多数开发人员会这样做),请坚持我们之前使用的 Notification.requestPermission() API。

什么是 PushSubscription?

我们调用 subscribe(),传入一些选项,作为回报,我们得到一个 Promise,它解析为一个 PushSubscription,从而产生如下代码

function subscribeUserToPush() {
  return navigator.serviceWorker
    .register('/service-worker.js')
    .then(function (registration) {
      const subscribeOptions = {
        userVisibleOnly: true,
        applicationServerKey: urlBase64ToUint8Array(
          'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U',
        ),
      };

      return registration.pushManager.subscribe(subscribeOptions);
    })
    .then(function (pushSubscription) {
      console.log(
        'Received PushSubscription: ',
        JSON.stringify(pushSubscription),
      );
      return pushSubscription;
    });
}

PushSubscription 对象包含向该用户发送推送消息所需的所有信息。如果您使用 JSON.stringify() 打印出内容,您将看到以下内容

    {
      "endpoint": "https://some.pushservice.com/something-unique",
      "keys": {
        "p256dh":
    "BIPUL12DLfytvTajnryr2PRdAgXS3HGKiLqndGcJGabyhHheJYlNGCeXl1dn18gSJ1WAkAPIxr4gK0_dQds4yiI=",
        "auth":"FPssNDTKnInHVndSTdbKFw=="
      }
    }

endpoint 是推送服务的 URL。要触发推送消息,请向此 URL 发出 POST 请求。

keys 对象包含用于加密随推送消息发送的消息数据的值(我们将在本节稍后讨论)。

定期重新订阅以防止过期

当订阅推送通知时,您经常会收到 PushSubscription.expirationTimenull。从理论上讲,这意味着订阅永不过期(与您收到 DOMHighResTimeStamp 时相比,后者会告诉您订阅过期的确切时间点)。但是,在实践中,浏览器仍然经常让订阅过期,例如,如果在较长时间内未收到推送通知,或者浏览器检测到用户未使用具有推送通知权限的应用程序。防止这种情况的一种模式是在每次收到通知时重新订阅用户,如下面的代码片段所示。这要求您发送通知的频率足够高,以至于浏览器不会自动过期订阅,并且您应该非常仔细地权衡合法通知需求的优点和缺点,以及仅仅为了订阅不过期而无意中向用户发送垃圾邮件的缺点和缺点。最后,您不应该试图对抗浏览器保护用户免受长期遗忘的通知订阅的努力。

/* In the Service Worker. */

self.addEventListener('push', function(event) {
  console.log('Received a push message', event);

  // Display notification or handle data
  // Example: show a notification
  const title = 'New Notification';
  const body = 'You have new updates!';
  const icon = '/images/icon.png';
  const tag = 'simple-push-demo-notification-tag';

  event.waitUntil(
    self.registration.showNotification(title, {
      body: body,
      icon: icon,
      tag: tag
    })
  );

  // Attempt to resubscribe after receiving a notification
  event.waitUntil(resubscribeToPush());
});

function resubscribeToPush() {
  return self.registration.pushManager.getSubscription()
    .then(function(subscription) {
      if (subscription) {
        return subscription.unsubscribe();
      }
    })
    .then(function() {
      return self.registration.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: urlBase64ToUint8Array('YOUR_PUBLIC_VAPID_KEY_HERE')
      });
    })
    .then(function(subscription) {
      console.log('Resubscribed to push notifications:', subscription);
      // Optionally, send new subscription details to your server
    })
    .catch(function(error) {
      console.error('Failed to resubscribe:', error);
    });
}

将订阅发送到您的服务器

一旦您拥有推送订阅,您将希望将其发送到您的服务器。这取决于您如何执行此操作,但一个小技巧是使用 JSON.stringify() 从订阅对象中获取所有必要的数据。或者,您可以手动拼凑相同的结果,如下所示

const subscriptionObject = {
  endpoint: pushSubscription.endpoint,
  keys: {
    p256dh: pushSubscription.getKeys('p256dh'),
    auth: pushSubscription.getKeys('auth'),
  },
};

// The above is the same output as:

const subscriptionObjectToo = JSON.stringify(pushSubscription);

订阅的发送在网页中完成,如下所示

function sendSubscriptionToBackEnd(subscription) {
  return fetch('/api/save-subscription/', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(subscription),
  })
    .then(function (response) {
      if (!response.ok) {
        throw new Error('Bad status code from server.');
      }

      return response.json();
    })
    .then(function (responseData) {
      if (!(responseData.data && responseData.data.success)) {
        throw new Error('Bad response from server.');
      }
    });
}

Node 服务器接收此请求并将数据保存到数据库中以供以后使用。

app.post('/api/save-subscription/', function (req, res) {
  if (!isValidSaveRequest(req, res)) {
    return;
  }

  return saveSubscriptionToDatabase(req.body)
    .then(function (subscriptionId) {
      res.setHeader('Content-Type', 'application/json');
      res.send(JSON.stringify({data: {success: true}}));
    })
    .catch(function (err) {
      res.status(500);
      res.setHeader('Content-Type', 'application/json');
      res.send(
        JSON.stringify({
          error: {
            id: 'unable-to-save-subscription',
            message:
              'The subscription was received but we were unable to save it to our database.',
          },
        }),
      );
    });
});

有了服务器上的 PushSubscription 详细信息,我们就可以随时向用户发送消息了。

定期重新订阅以防止过期

当订阅推送通知时,您经常会收到 PushSubscription.expirationTimenull。从理论上讲,这意味着订阅永不过期(与您收到 DOMHighResTimeStamp 时相比,后者会告诉您订阅过期的确切时间点)。但是,在实践中,浏览器仍然经常让订阅过期,例如,如果在很长时间内未收到推送通知,或者浏览器检测到用户未使用具有推送通知权限的应用程序。防止这种情况的一种模式是在每次收到通知时重新订阅用户,如下面的代码片段所示。这要求您发送通知的频率足够高,以至于浏览器不会自动过期订阅,并且您应该非常仔细地权衡合法通知需求的优点和缺点,以及仅仅为了订阅不过期而向用户发送垃圾邮件的缺点和缺点。最后,您不应该试图对抗浏览器保护用户免受长期遗忘的通知订阅的努力。

/* In the Service Worker. */

self.addEventListener('push', function(event) {
  console.log('Received a push message', event);

  // Display notification or handle data
  // Example: show a notification
  const title = 'New Notification';
  const body = 'You have new updates!';
  const icon = '/images/icon.png';
  const tag = 'simple-push-demo-notification-tag';

  event.waitUntil(
    self.registration.showNotification(title, {
      body: body,
      icon: icon,
      tag: tag
    })
  );

  // Attempt to resubscribe after receiving a notification
  event.waitUntil(resubscribeToPush());
});

function resubscribeToPush() {
  return self.registration.pushManager.getSubscription()
    .then(function(subscription) {
      if (subscription) {
        return subscription.unsubscribe();
      }
    })
    .then(function() {
      return self.registration.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: urlBase64ToUint8Array('YOUR_PUBLIC_VAPID_KEY_HERE')
      });
    })
    .then(function(subscription) {
      console.log('Resubscribed to push notifications:', subscription);
      // Optionally, send new subscription details to your server
    })
    .catch(function(error) {
      console.error('Failed to resubscribe:', error);
    });
}

常见问题

人们在此阶段提出的一些常见问题

我可以更改浏览器使用的推送服务吗?

不可以。推送服务由浏览器选择,正如我们在 subscribe() 调用中看到的那样,浏览器将向推送服务发出网络请求,以检索构成 PushSubscription 的详细信息。

每个浏览器都使用不同的推送服务,它们的 API 是否不同?

所有推送服务都将期望相同的 API。

这个通用 API 称为 Web Push Protocol,它描述了您的应用程序需要发出的网络请求才能触发推送消息。

如果我在用户的桌面上订阅了用户,他们是否也在手机上订阅了?

很遗憾,不是。用户必须在他们希望接收消息的每个浏览器上注册推送。还值得注意的是,这将要求用户在每个设备上授予权限。

后续步骤

代码实验室