订阅用户

Matt Gaunt

第一步是获得用户许可,以便向他们发送推送消息,然后我们 获取 PushSubscription

实现此目的的 JavaScript API 相当简单直接, 整个逻辑流程

功能检测

首先,我们需要检查当前浏览器是否真的支持推送消息传递。我们可以检查 只需进行两项简单的检查即可支持 push

  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 和推送都受支持。下一步 “注册”我们的 Service Worker。

当我们注册 Service Worker 时,我们会告知浏览器 Service Worker 文件的位置。 该文件仍只是 JavaScript,但浏览器会“授予其访问权限”发送到 Service Worker 包括推送更确切地说,浏览器会在 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 开发者工具中仔细检查您的 JavaScript 是否存在拼写错误 / 错误。

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

PushManager API 浏览器兼容性

浏览器支持

  • Chrome:42。 <ph type="x-smartling-placeholder">
  • Edge:17。 <ph type="x-smartling-placeholder">
  • Firefox:44。 <ph type="x-smartling-placeholder">
  • Safari:16. <ph type="x-smartling-placeholder">

来源

正在请求权限

我们已经注册了 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 是符号 与浏览器达成协议,即 Web 应用会在每次推送消息时显示通知 (即没有静默推送)。

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

Chrome 目前只支持对会导致以下结果的订阅使用 Push API 向用户显示的消息。您可以通过调用 pushManager.subscribe({userVisibleOnly: true})。请参阅 如需了解详情,请访问 https://goo.gl/yqv4Q4

目前,Chrome 似乎绝不会实现一揽子静默推送功能。相反, 规范作者正在探索预算 API 的概念,这种 API 可以让网络应用 根据 Web 应用的使用情况计算的静默推送消息数量。

applicationServerKey 选项

我们简要提到了“应用服务器密钥”。"应用程序 服务器密钥”供推送服务用来识别订阅用户和 确保由同一应用向该用户发送消息。

应用服务器密钥是应用特有的公钥和私钥对。 私钥应对您的应用保密,并且公钥可以共享 自由发挥。

传递到 subscribe() 调用的 applicationServerKey 选项是应用的公共选项 键。在订阅用户时,浏览器将其传递给推送服务,也就是推送 服务可以将应用的公钥与用户的 PushSubscription 绑定起来。

下图说明了这些步骤。

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

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

如果稍后想要发送推送消息,您将需要创建 Authorization 标头 其中将包含使用应用服务器的私钥进行签名的信息。当 推送服务收到发送推送消息的请求,它可以验证这个已签名的 Authorization 标头 查询与接收请求的端点相关联的公钥。如果签名为 这样,推送服务就知道它必须来自 匹配的私钥。从根本上说,它是一种安全措施,可防止他人 向应用用户发送消息

发送
消息

从技术上讲,applicationServerKey 是可选的。不过,最简便的 需要它,而其他浏览器可能也需要 未来。在 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 使用 YAML 格式

什么是 PushSubscription?

我们调用 subscribe(),传入一些选项,作为回报,我们会获得一个解析为 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 是推送服务网址。要触发推送消息,请发出 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 称为 网络推送协议 并说明网络请求 来触发推送消息。

如果我在某个用户桌面设备上订阅,对方也会在手机上订阅吗?

很遗憾,还不能。用户必须在他们希望访问的每个浏览器上注册推送 用于接收消息。另外值得注意的是 用户在每台设备上授予权限。

下一步做什么

Codelab