持续推送,使用 W3C Push API

大家对这种体验都很熟悉——手机上突然弹出一个气泡,里面包含一条烦人的消息,比如“你那些可爱的小怪物已经休息好了,想要去战斗了!”,或者“你有未回复的来自陌生人的好友请求。快去回复吧!”

推送消息绝对不是一个新概念,多年来一直是移动平台的一项热门功能。然而,直到最近,推送才出现在 Web 平台上。本文将带您了解基础知识,并概述推送的当前状态。

与其他用于向用户发送消息的技术选择相比,推送的优势在于,推送消息可以让用户选择接收更新,而无需客户端进行任何干预——只要服务器拥有端点(见下文),它就可以仅在需要时向已订阅的客户端发送更新(即,它不需要像套接字那样保持持续连接,这对电池续航等方面有利)。

注意:MDN 包含 Push API 的完整参考文档 以及详细教程,使用 Push API

浏览器支持

Push API 目前处于工作草案阶段。(最新的编辑草案在这里。)大多数 API 在 Chrome (42+) 和 Firefox (42+) 的最新版本中都受支持,但目前仅在预发布版本(例如 Nightly、Developer Edition 和 Beta)中受支持。例外情况是 PushMessageData,它目前仅在 Firefox 44+ 中受支持(同样不在发布版本中)。

Push 需要 服务工作线程 才能运行,服务工作线程在 Chrome 和 Firefox 的最新版本中也大多受支持(尽管在 Firefox 最新发布版本中默认禁用)。由于服务工作线程出于安全目的需要 https 才能工作,因此 Push 也需要 https

值得注意的是,Push 通常与通信 API(如 Web NotificationsChannel Messaging)一起使用,用于传达推送消息发送结果。这些 API 在现代浏览器中都具有相当广泛的支持。

推送过程

在本节中,我们将介绍一个使用推送消息的应用程序的典型设置过程。

注意:您可以在 Github 上找到一个 演示 Push API 用法的示例。运行该示例将有助于您在阅读以下各节时理解内容。

请求权限

您应该做的第一件事是请求 Web 通知或任何其他需要权限的功能的权限。例如

Notification.requestPermission();

注册服务工作线程并订阅推送

接下来,使用 ServiceWorkerContainer.register() 注册一个服务工作线程来控制页面。这将返回一个 Promise,该 Promise 将解析为一个 ServiceWorkerRegistration 对象。

navigator.serviceWorker.register('sw.js').then(function(reg) {
  // do something with reg object
});

一旦我们有了这个注册对象,我们就可以开始注册推送。通常,您会将 ServiceWorkerRegistration 对象发送到某种订阅函数。

在任何时候,我们都可以使用 ServiceWorkerContainer.ready 属性检查服务工作线程是否处于活动状态并已准备好开始执行操作,该属性返回一个 Promise,该 Promise 解析为前面提到的 ServiceWorkerRegistration 对象。一旦我们拥有了它,我们就可以使用 ServiceWorkerRegistration.pushManager 属性访问 PushManager。然后,我们可以使用 PushManager.subscribe() 订阅推送服务,该方法返回一个 Promise,该 Promise 解析为一个 PushSubscription 对象。

navigator.serviceWorker.ready.then(function(reg) {
  reg.pushManager.getSubscription()
  .then(function(subscription) {
    // do something with your PushSubscription object
  });
}

要取消订阅,代码类似,但您必须使用 PushSubscription.unsubscribe() 方法取消订阅推送。

navigator.serviceWorker.ready.then(function(reg) {
  reg.pushManager.getSubscription()
  .then(function(subscription) {
    subscription.unsubscribe().then(function(successful) {
      // unsubscribe was successful
    })
  });
}

推送服务器和发送推送消息

要发送推送消息,您需要一个服务器组件。可以使用任何您喜欢的服务器端语言编写,只要它能够处理安全请求/响应和数据加密。(推送消息需要 https,并且推送发送的数据需要加密)。

注意:PushMessageData 和加密的支持目前仅限于 Firefox,加密过程仍在完善中。(加密和 getKey() 尚未出现在规范中)。

一旦我们拥有了 PushSubscription 对象,我们就需要获取两条用于发送推送消息的信息。

  • PushSubscription.endpoint:这是一个指向处理推送消息发送的推送服务器的唯一 URL(每个浏览器都有自己的推送服务器)。例如:https://updates.push.services.mozilla.com/push/gAAAAABWJ-VZaQ9DhwvjZJHEHlZCzNJBPTPAcucU9mprtyzisSow75qHbY5lrjglEXE7G6SIfWvz-QSwhBcjpRjx2PAnKCAHd-5XHh1RFXa1ngqq_2-I0-PZoEqigI7E3ISO5zE1tNy29_Iyiu06m0tc_2nfKyuEcjwDPLyOC8c3IvawhBUUzMM=。您的服务器使用此 URL 发送推送消息——请求会命中推送服务器,末尾的随机字符串确保推送消息发送到与该特定推送订阅关联的服务工作线程。
  • PushSubscription.getKey('p256dh'):此方法生成一个公钥客户端密钥,它是用于加密数据的组件之一。然后,应将这些详细信息发送到服务器,以便在需要时发送推送消息。(您可以使用 FetchXMLHttpRequest 来执行此操作)。

在服务器端,您应该存储端点和任何其他所需详细信息,以便在需要向推送订阅者发送推送消息时可以使用它们(使用数据库或任何您喜欢的存储方式)。在生产应用程序中,请确保隐藏这些详细信息,以免恶意方窃取端点并向订阅者发送垃圾推送消息。任何拥有端点的人都可以发送推送消息,只要订阅保持活动状态。

要发送没有数据的推送消息,您需要使用 POST 方法将其发送到端点 URL。要在 Firefox 中发送包含数据的消息,您需要对其进行加密,这涉及到客户端公钥。这是一个非常复杂的过程(阅读 Web 推送的消息加密 以了解更多详细信息)。随着时间的推移,将编写库来为您执行此类操作;Marco Castelluccio 的 NodeJS web-push 库 是 NodeJS 的一个不错的选择。

服务工作线程和响应推送消息

在您的服务工作线程中,您需要设置一个 onpush 处理程序来响应接收到的推送消息。

self.addEventListener('push', function(event) {
  var obj = event.data.json();
  // do something with JSON
});

请注意,事件对象类型为 PushEvent;其 data 属性包含一个 PushMessageData 对象,该对象包含通过推送消息发送的数据。此对象具有可用于将消息有效负载作为 Blob、ArrayBuffer、JSON 对象或纯文本字符串返回的方法(我们在上面将其转换为 JSON)。获得有效负载后,您可以对其执行任何操作。

发送通道消息

如果要通过向主上下文发送 通道消息 来响应推送事件,则首先需要在主上下文和服务工作线程之间打开一个消息通道。在主上下文中,您可以执行以下操作

navigator.serviceWorker.ready.then(function(reg) {
  var channel = new MessageChannel();
  channel.port1.onmessage = function(e) {
    handleChannelMessage(e.data);
  }

  mySW = reg.active;
  mySW.postMessage('hello', [channel.port2]);
});

首先,我们使用构造函数创建一个新的 MessageChannel 对象,然后设置一个 onmessage 处理程序来处理跨通道传送到主上下文的消息。

然后,与之前一样,我们获取对 ServiceWorkerRegistration 对象的引用。然后,我们使用其 active 属性返回一个 ServiceWorker 对象。我们可以使用 ServiceWorker 对象的 postMessage() 方法向服务工作线程上下文发送消息,以及消息通道的 port2

在服务工作线程中,我们使用以下方法获取对 port2 的引用

var port;

self.onmessage = function(e) {
  port = e.ports[0];
}

一旦建立了此链接,就可以使用以下方法将数据发送回主上下文

port.postMessage('my message');

触发通知

如果要通过触发系统通知来响应,可以使用 ServiceWorkerRegistration.showNotification

function fireNotification(obj, event) {
  var title = 'Subscription change';
  var body = obj.name + ' has ' + obj.action + 'd.';
  var icon = 'push-icon.png';
  var tag = 'push';

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

请注意,我们在这里在 ExtendableEvent.waitUntil 方法内运行了此代码——这会将事件的生命周期延长到通知触发之后,因此我们可以确保一切按预期发生。

处理过早的订阅过期

有时,推送订阅会过早过期,而没有调用 unsubscribe()。当服务器过载或您长时间离线时,可能会发生这种情况。这在很大程度上取决于服务器,因此很难预测确切的行为。无论如何,您可以使用 onsubscriptionchange 处理程序来处理此问题,该处理程序仅在这种特定情况下会被调用。

self.addEventListener('subscriptionchange', function() {
  // do something, usually resubscribe to push and
  // send the new subscription details back to the
  // server via XHR or Fetch
});

Chrome 对 Push 的支持

Chrome 也很好地支持 Push,但与 Firefox 有一些区别。首先,它尚不支持在推送消息中发送 PushMessageData;它还依赖于 Google Cloud Messaging 服务。阅读 Chrome 支持的其他步骤 以获取完整详细信息。

关于 Chris Mills

Chris Mills 是 Mozilla 的高级技术作家,他撰写有关开放式 Web 应用程序、HTML/CSS/JavaScript、A11y、WebAssembly 等方面的文档和演示。他喜欢捣鼓 Web 技术,并在会议和大学偶尔进行技术讲座。他曾为 Opera 和 W3C 工作,喜欢演奏重金属鼓和饮用好啤酒。他与妻子和三个可爱的女儿住在英国曼彻斯特附近。

更多 Chris Mills 的文章…


11 条评论

  1. PhistucK

    快速说明和一些更正——
    Chrome (42) 默认支持 Push API(您的措辞暗示它仅在早期发布版本中支持)。
    服务工作线程确实在 Chrome (40) 中默认受支持,但在 Firefox 中默认禁用(即使在最新的稳定版本中也是如此)。

    2015 年 10 月 26 日 14:09

    1. Chris Mills

      感谢 PhistucK!您说得对,我的措辞有点误导性。我已更新了措辞,希望现在更清楚了。

      2015 年 10 月 27 日 01:38

  2. Chao

    谁能解释一下,为什么我们需要为此创建一个单独的 API,而不是在服务工作线程中使用 WebSockets 之类的东西?

    2015 年 10 月 26 日 15:36

    1. Chris Mills

      Chao 提出的这是一个有用的问题。我认为 PhistucK 在下面的评论中已经回答了,但我也在文章开头添加了一条说明性注释。

      2015 年 10 月 27 日 01:39

  3. voracity

    哦,我想这个功能_确实_需要一个集中式服务器。在视野范围内没有去中心化的替代方案(适用于 IPv6 世界)?

    2015 年 10 月 26 日 19:57

  4. PhistucK

    嗯,WebSockets 需要与服务器保持持续连接。我认为 Push 不需要,并且服务工作线程通常不运行,除非您使用网站或收到推送通知。这样可以节省大量电池电量。

    2015 年 10 月 27 日 01:31

  5. Rafael

    看起来 Pinterest 已经测试了一段时间,但我还没有看到实际的通知。我一直收到“允许 Pinterest 发送通知”的提示,但我想该首选项位还没有设置。

    还不错。

    2015 年 10 月 29 日 12:47

  6. Tyf0x

    我理解推送是 WebSockets 的一种替代方案,它不需要脚本和推送服务器之间保持永久连接。

    但是,为了让服务器向客户端发送任何内容,某个地方需要维持连接(否则我们会遇到防火墙/NAT 问题)。

    这是否意味着 Firefox 在后台维护了一个打开的套接字连接?如果是,它是否节省了资源,因为无论有多少脚本端点,只需要一个与推送服务器的连接?

    或者我完全错了?

    2015 年 10 月 29 日 22:59

    1. Chris Mills

      这是一个有趣的问题;我不确定 Fx 在后台维护连接的确切方式,因此我将联系我们的工程师,请他们提供正确的答案。

      2015 年 10 月 30 日 05:51

      1. Chris Mills

        好的,我从我们的推送工程团队那里得到了一个答案。

        所以,是的,Firefox 会与单个服务器保持连接。此单个连接使我们能够将大量流量整合到一个连接中,从而有机会节省电池电量甚至网络成本。将来,我们可能能够移除甚至
        那个单一连接,并使用更先进的技术来传递消息。

        推送并不总是完全或完美地替代 WebSockets。如果您的应用程序是 WebSockets 的重度用户,那么最好建立一个 WebSocket 并将其用于较短时期的双向消息交换。推送最适合于少量、不频繁的消息。推送在全双工通信方面永远不会像 WebSockets 那样有效,也不像 HTTP 那样适合大规模运行。

        特别是,推送消息几乎在所有情况下都会增加额外的延迟。如果您关心延迟,WebRTC 数据通道是目前可用的最佳选择。

        另一方面,推送总是比长期存在的 WebSocket 更好(几乎总是比长轮询更好)。

        2015年10月30日 06:27

  7. Tyf0x

    感谢你挖掘出答案:)

    这完全说得通。

    我现在很好奇他们将来可能使用的这种“更高级的技术”。我想我必须等到它被实现,然后再提出问题。

    2015年10月30日 07:14

本文评论已关闭。