订阅用户

Matt Gaunt

第一步是从用户那里获取向其发送推送消息的权限,然后我们就可以获取 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;
}

虽然浏览器对服务工件和推送消息的支持正在快速增长,但最好还是针对这两者进行功能检测,并逐步增强

注册服务工作线程

通过功能检测,我们知道该浏览器同时支持服务工件和推送。下一步是“注册”我们的服务工作线程。

注册服务工作线程时,我们会告知浏览器服务工作线程文件的位置。该文件仍然只是 JavaScript,但浏览器会“向其授予”对服务工作线程 API(包括推送)的访问权限。更确地说,浏览器会在服务工作器环境中运行该文件。

如需注册服务工作器,请调用 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.js。在后台,浏览器会在调用 register() 后执行以下步骤:

  1. 下载服务工作器文件。

  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() 的调用是重要的代码段。此方法会向用户显示提示:

桌面版和移动版 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() 方法时,我们会传入 options 对象,其中包含必需参数和可选参数。

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

userVisibleOnly 选项

首次向浏览器添加推送功能时,我们不确定开发者是否应能够发送推送消息而不显示通知。这通常被称为静默推送,因为用户不知道后台发生了什么。

人们担心的是,开发者可能会在用户不知情的情况下做一些不良的事情,例如持续跟踪用户的位置信息。

为避免这种情况,并让规范作者有时间考虑如何最好地支持此功能,我们添加了 userVisibleOnly 选项,传入值 true 是对浏览器的象征性约定,表示网络应用将在每次收到推送时显示通知(即不静默推送)。

目前,您必须传入 true 值。如果您未添加 userVisibleOnly 键或传入 false,则会收到以下错误:

Chrome 目前仅支持针对会导致用户看到消息的订阅使用 Push API。您可以改为调用 pushManager.subscribe({userVisibleOnly: true}) 来指明这一点。如需了解详情,请访问 https://goo.gl/yqv4Q4

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

applicationServerKey 选项

我们在上一部分中简要介绍了“应用服务器密钥”。“应用服务器密钥”由推送服务用于识别订阅用户的应用,并确保同一应用向该用户发送消息。

应用服务器密钥是您的应用特有的公钥和私钥对。私钥应对应用保密,而公钥可以自由分享。

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

下图展示了这些步骤。

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

插图:订阅方法中使用的公共应用服务器密钥。

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

发送消息时如何使用私有应用服务器密钥

从技术层面来说,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() 时没有显示通知的权限,浏览器会为您请求权限。如果您的界面适用于此流程,此方法会很有用,但如果您希望获得更多控制权(我认为大多数开发者都会这样),请坚持使用我们之前使用的 Notification.requestPermission() API。

什么是 PushSubscription?

我们调用 subscribe(),传入一些选项,然后得到一个解析为 PushSubscription 的 Promise,从而生成如下代码:

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 是推送服务网址。如需触发推送消息,请向此网址发出 POST 请求。

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

定期重新订阅以防止订阅到期

订阅推送通知后,您通常会收到 nullPushSubscription.expirationTime。从理论上讲,这意味着订阅永不过期(与您收到 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.');
      }
    });
}

节点服务器会接收此请求,并将数据保存到数据库以供日后使用。

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 详细信息,我们就可以随时向用户发送消息。

定期重新订阅以防止订阅到期

订阅推送通知后,您通常会收到 nullPushSubscription.expirationTime。从理论上讲,这意味着订阅永不过期(与您收到 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,用于描述您的应用需要发出哪些网络请求才能触发推送消息。

如果我在用户的桌面设备上为其订阅了服务,那么他们在手机上是否也会订阅?

很遗憾,不能。用户必须在希望接收消息的每个浏览器中注册接收推送通知。另请注意,这需要用户在每部设备上授予权限。

下一步做什么

Codelab