大家对这种体验都很熟悉——手机上突然弹出一个气泡,里面包含一条烦人的消息,比如“你那些可爱的小怪物已经休息好了,想要去战斗了!”,或者“你有未回复的来自陌生人的好友请求。快去回复吧!”
推送消息绝对不是一个新概念,多年来一直是移动平台的一项热门功能。然而,直到最近,推送才出现在 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 Notifications 和 Channel 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')
:此方法生成一个公钥客户端密钥,它是用于加密数据的组件之一。然后,应将这些详细信息发送到服务器,以便在需要时发送推送消息。(您可以使用 Fetch 或 XMLHttpRequest 来执行此操作)。
在服务器端,您应该存储端点和任何其他所需详细信息,以便在需要向推送订阅者发送推送消息时可以使用它们(使用数据库或任何您喜欢的存储方式)。在生产应用程序中,请确保隐藏这些详细信息,以免恶意方窃取端点并向订阅者发送垃圾推送消息。任何拥有端点的人都可以发送推送消息,只要订阅保持活动状态。
要发送没有数据的推送消息,您需要使用 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 工作,喜欢演奏重金属鼓和饮用好啤酒。他与妻子和三个可爱的女儿住在英国曼彻斯特附近。
11 条评论