不要错过实时乐趣!使用 Firefox OS 推送通知

Firefox OS v1.1 为开放式 Web 应用引入了推送通知,使 Web 开发人员能够利用实时更新,而无需自行实现复杂的轮询逻辑。原生推送通知支持意味着
Firefox OS 设备只需维护一个连接,应用程序可以关闭,从而延长电池寿命并提高设备响应速度,同时仍能向用户提供即时更新。

Firefox OS 推送通知专为唤醒应用而设计。它们不处理数据、桌面通知和其他功能,因为有其他 Web API 提供这些功能。在这篇 Hacks 文章中,我们将研究构建一个小型聊天应用程序,该应用程序使用推送通知来更新对话。

范围和工作流程

在开始应用程序之前,最好先了解一下推送通知的范围和工作流程。

无轮询

推送通知的范围是通知客户端上的 Web 应用程序网络上的一些内容已更改,以便客户端拥有的数据应更新到远程服务器上的最新数据。推送取代了应用程序必须使用 XHR 或使用 WebSockets 不断轮询的责任,并通知应用程序更改。此数据通常存储在应用程序服务器(在云端)上,并在一个或多个用户之间分配的多个设备上共享。

例如,用户可以拥有一个个人日历,该日历在笔记本电脑、手机和平板电脑之间共享。此外,用户可能拥有一个与同事共享的工作日历。这些设备上的良好日历应用程序将使设备与服务器上的“主”日历保持同步。借助推送通知,Firefox OS 设备现在可以立即进行此同步并具有很强的响应能力。

无数据

Firefox OS 推送通知不支持通过推送发送消息。从一开始,推送通知就被设计为明确不携带有效负载。这可能看起来很荒谬,但它符合 Mozilla 确保用户隐私的使命。多个组织将运行推送服务器,如果允许在推送协议上使用数据,则此数据最终会落入这些第三方的手中。意外泄露机密用户数据非常容易。例如,如果聊天应用程序通过推送发送实际消息,并且用户决定向其他用户发送其信用卡号码,则信用卡号码现在掌握在推送服务器运营商手中。

为了防止这种情况,协议根本不允许数据。唯一允许的特定于应用程序的数据是版本号(可选)。此版本号对于协作应用程序很有用。如果发送了版本号,并且客户端设备已收到具有相同或更旧版本号的消息,则不会再次收到通知。版本号可用于将用户在此设备上所做的更改与从服务器收到的更改合并。

工作流程

继续以日历为例,推送通知的流程如下。移动应用程序注册一个推送端点。此推送端点是一个唯一的 URL,表示接收更新的一个通道。这些 URL 指向第三方推送服务器,该服务器设置在设备上,并可能由 Mozilla 或其他组织或个人运营。每个应用程序可以有多个推送端点(想象一下用户拥有的每个日历一个)。然后,移动应用程序将此端点发送到应用程序服务器。日历服务器会将此端点与用户和特定日历关联。每个日历都将有一组端点,每个用户订阅该日历的每个设备一个。

任何用户在其设备上更改日历时,应用程序都会将这些更改保存到服务器。这是推送通知开始发挥作用的地方。应用程序服务器将向已修改的日历集中的每个端点发出 HTTP 请求。每个推送服务器现在负责将消息传递到设备。当每个设备收到通知时,它会启动设备上的日历应用程序并通知它。然后,日历应用程序可以从日历服务器获取所有更改。

注意:此 URL 应视为不透明的,您的应用程序和应用程序服务器不应对其做出任何假设,也不应对其进行编辑。

如果我的应用程序未运行会发生什么?

推送通知不仅是一种便利,对于需要保持同步但可能并不总是运行的移动应用程序来说,它们至关重要。如果您的应用程序在收到推送通知时未运行,则会启动应用程序,导航到处理页面并将消息传递给它。如果在收到推送后打开应用程序切换器,您将能够看到应用程序在后台运行。

设置环境

有很多关于使用适当的工具编写 Firefox OS 应用程序并使用系统提供的所有功能的资源。两部分 Hacks 系列文章为 Firefox OS 构建待办事项应用程序(第 1 部分)就是一个很好的例子。相反,聊天应用程序将非常基础。我们不会关心 Volo 等额外工具。也不会通过使用标准的 UX 元素来使应用程序看起来很好看。我们将坚持使用简单的 HTML 和 JavaScript,仅使用 jQuery 和 Bootstrap 用于一些实用程序。

服务器将使用 Python 编写,使用 Flask 框架,该框架很小,即使您以前从未使用过 Python 也应该很容易理解。此外,服务器使用Redis作为持久数据存储。

所有代码均在 Github 上可用

由于推送通知从 Firefox OS v1.1 开始可用,因此您目前需要 Firefox OS 模拟器的预览版本(请注意,这些是实验性的,并非所有功能都能按预期工作)。将您平台的 URL 粘贴到 Firefox 中,安装模拟器加载项,您就可以开始使用推送功能了。

addons.mozilla.org 上模拟器的稳定版本没有推送功能。

应用程序清单

使用推送通知需要在应用程序清单中添加三个重要的条目。

push权限允许应用程序使用推送通知。我们还请求desktop-notification权限,以便在聊天中提及用户时显示通知。

messages条目指定在收到推送通知时应通知应用程序的哪个页面。在本例中,由于我们的应用程序是单页面应用程序,因此将其设置为/。Firefox OS v1.1 仅允许注册推送通知的页面接收它们,因此您不能让/调用navigator.push.register()并让/push.html成为使用navigator.mozSetMessageHandler()的接收器。

push消息是在收到实际通知时传递的消息。push-register消息是一种错误恢复机制。在极少数情况下,推送服务器可能会发生数据丢失,或者它可能会决定释放一些旧的端点。在这两种情况下,如果您的应用程序的端点不再有效,它将收到push-register消息,在这种情况下,您的应用程序应注册新的端点并更新应用程序服务器。

主页面

我们的聊天应用程序将是一个单页面应用程序,包含最新的 50 条消息,然后是一个输入新消息的字段。

无序列表chat将保存聊天消息,这些消息将由 JavaScript 加载。theform用于提交消息。div nickPrompt用作模式对话框,以在应用程序第一次运行时询问用户的昵称。最后,我们包含了驱动应用程序的各种脚本。

离线 Web 应用

随着 Web 应用程序在移动设备上使用,重要的是要设计它们,以便尽可能地离线使用。在这种情况下,一种常见的模式是在客户端使用 localStorage 或 IndexedDB 进行持久存储。当网络连接可用时,此存储会更新,并且用户内容从此离线副本显示。这样,用户就可以始终与设备上已有的内容进行交互。我们的应用程序遵循类似的模型。最新的聊天消息存储在设备上,使用 IndexedDB。推送通知将首先更新此本地数据,然后更新 UI。

聊天数据库

存储在model.js中实现,它抽象了一个 IndexedDB 数据库以支持简单的聊天实现所需的操作。每条消息都存储为一个对象

{
  "id": 5, // Integer message ID. Unique for every message.
  "nick": "EdwardSnowden", // The sender of the message.
  "message": "The NSA is spying!" // The actual text.
}

为了与 Snapchat 竞争,服务器和客户端都仅存储最新的 50 条消息。

id字段是作为推送通知的version字段使用的理想选择。在聊天的情况下,每条消息都是新的,因此版本字段并不那么重要,但我们将出于演示目的使用它。

安装应用程序

在 Firefox OS v1.1 中,推送通知仅适用于应用程序,而不适用于在浏览器中加载的网页。因此,当用户在浏览器中访问我们的应用程序时,它会提示安装该应用程序。

注册推送通知

当用户启动应用程序时,会调用go()。这会在第一次运行时提示用户输入昵称,并使用任何已存在的离线消息填充聊天列表。

推送通知的重要部分是

navigator.push是提供推送通知操作的 PushManager 对象。您可以检查

if ('push' in navigator) {
}

以查看页面是否可以使用推送通知。

registrations()用于检查应用程序是否已注册现有端点。与其他一些 Firefox OS API 一样,它返回一个DOMRequest对象,因为它是一个异步操作。成功后,结果将是一个 PushRegistration 对象列表。PushRegistration 具有以下字段

interface PushRegistration {
  string pushEndpoint; // A URL specifying the endpoint.
  unsigned long int version; // The latest known version.
}

如果我们没有注册,我们将使用navigator.push.register()请求新的注册。注册成功后,DOMRequest 的result字段将是一个 URL,它是此注册的端点。现在我们有了端点,我们将使用 XHR 将其发送到我们的服务器。

在服务器上,让我们将所有端点存储在一个名为endpoints的列表中

此外,我们向添加的新端点发送快速 ping。我们为此使用了很棒的Requests库,您可以看到在服务器上使用推送通知有多么容易。只需对端点发出 HTTP PUT 请求,请求正文为

version=<number>

navigator.push.register()调用可能会由于多种原因而失败。最常见的是设备没有网络连接。在这种情况下,将触发 DOMRequest 的onerror处理程序,并且请求的
error将具有错误的简短描述名称。

分派通知

每次向聊天发布新消息时,我们都希望通过推送通知通知所有客户端。提交表单时

服务器会收到消息文本和发送者的昵称。

我们使用INCR原子命令从 Redis 请求消息的id。我们还希望在服务器上执行两件事

  1. 按此消息 ID 对消息进行排序,以便我们可以快速从某个 ID 开始提供消息。为此,我们使用 Redis 的排序集,其中score为消息 ID。这使其保持排序,并允许我们快速获取从特定 ID 开始的消息,以便客户端不必下载所有消息,但我们不必为客户端维护单独的队列。
  2. 将消息限制为最新的 50 条(MAX_MESSAGES)。我们通过使用ZREMRANGEBYRANK截断集合来实现此目的。

我们使用redis-py的管道系统以原子方式执行这两项操作,以便如果添加失败,则删除旧消息也会失败。您的语言/框架将具有类似的操作来实现此一致性。

消息本身存储为 JSON 表示形式的字符串,以便将消息发送到客户端只需提取元素并将它们连接成 JSON 列表即可。

之后,我们将新的消息 ID 放入notification_queue。这让我们了解使用推送通知时的最佳实践。应用程序服务器通常会通知多个端点有关更改的信息。在这种情况下,阻塞主服务器不是一个好主意,而是应该有一个专用的线程或进程可以 ping 端点。这甚至可以转移到专用的机器上。版本号系统允许您并行化 ping 操作。

在这个简单的服务器中,我们使用 Python 的内置Queue,它允许我们实现生产者-消费者系统。在app.py中启动通知推送服务器的线程并运行notify()函数

notify()函数在notification_queue上阻塞。当有新的消息 ID 可用时,它会循环遍历所有端点并通知它们新版本。

成功的 PUT 将导致端点返回200 OK响应。这意味着通知已排队等待传递。当设备联机并连接到推送服务器时,传递将在几秒钟内完成。

服务器的工作完成了!现在应用程序必须处理推送通知。

接收推送通知

接收推送通知是通过设置一个函数来完成的,该函数在收到“push”消息时被调用。这是使用mozSetMessageHandler完成的。注意:设置接收器的页面和清单文件中列出的消息
接收器必须匹配!

模型成功初始化后,我们将设置处理程序。message将是

interface PushMessage {
  string pushEndpoint; // The push endpoint that changed.
  unsigned long int version; // The new version.
}

pushEndpoint 允许应用程序识别哪个端点(在其已
注册的众多端点中)发生了更改。

在本例中,我们只想使用此版本在需要时下载新的聊天消息。

model.latest()返回本地存储中的最新消息 ID,并且仅当推送通知的版本更新时,我们才会下载新消息。

服务器有一个/message/<id> REST 接口,给定一个消息 ID,它将返回其之后的所有消息。

它简单地使用消息 ID 作为有序集合的分数,并返回一个 JSON 响应。

在客户端,我们更新本地存储。为了处理提及,我们还会循环遍历接收到的新消息,并检查是否有任何消息包含用户的昵称。如果找到,则会创建一个包含该消息的桌面通知,以便用户注意到一条可能重要的消息。

最后更新 UI。

由于旧消息必须从视图中移除,因此我们移除所有列表项并添加新的列表项。现代 Web 运行时即使将多个新元素添加到 DOM 中,只要它们都一次性添加,也只会进行一次重绘。因此不会出现闪烁。在这里,我们也为提及用户的消息赋予不同的颜色。

需要处理的一件小事是push-register消息。如果你还记得,我提到过这是一种可能会被调用的错误恢复机制。

我们的演示应用程序只是向用户显示一个错误,但理想情况下,您应该在后台静默注册新的端点。

取消注册

推送通知的一个非常常见的模式是为每个用户关联一个端点,其中用户在您的应用程序和应用程序服务器(例如电子邮件 ID)中具有唯一的 ID。在这种情况下,您的应用程序可能被设计为支持切换用户。在这种情况下,您不再希望在旧用户感兴趣的事情发生时接收通知。可能还有其他情况,您不再对推送端点感兴趣。表达这种不感兴趣的方式是使用navigator.push.unregister()

// Assuming pushEndpoint1 is the endpoint associated with an old user
var request = navigator.push.unregister(pushEndpoint1);

unregister()返回一个 DOMRequest,其onsuccess将在 Firefox OS 删除注册后立即触发。unregister()调用失败的可能性极小。失败的最可能原因是该端点不是 Firefox OS 发行给应用程序的有效端点。

良好的设计:将推送与用户流程解耦

尽管 Web 应用最常见的推送通知用途是向用户显示桌面通知,但推送注册、接收推送和取消注册的过程应该尽可能地保持在用户 UI 流程之外。
这在注册期间最适用。

类似于 Web 应用程序用户体验的渐进增强,您的应用程序应该准备好处理缺少推送通知的情况。例如,您不应该仅仅因为navigator.push.register()由于缺乏连接而失败就阻止用户登录您的应用程序。相反,让用户继续使用应用程序的离线部分,并将注册请求排队,以便在一段时间后重试。 Alarm API 可用于稍后唤醒您的应用程序以完成此类“后台”任务。

同样,数据密集型应用程序不应该等待用户响应桌面通知后再获取数据。一个好的日历应用程序将在收到推送后立即更新其本地存储。只有这样,它才会弹出任何提醒,以便用户启动应用程序后立即看到新的事件和更改。同时,如果用户选择手动同步选项,它应该unregister()端点。

重要的安全注意事项

register()获取的推送端点应保密!最好的方法是

1) 始终通过安全(https)连接将端点发送到应用程序服务器。否则,攻击者可能会执行中间人攻击并捕获通过网络传输的端点。

2) 保持数据库安全。端点应该与用户个人数据一样受到保护。

如果攻击者能够访问端点,他或她可能会影响拥有这些端点的设备。例如,他们可能会导致您的应用程序频繁运行,从而影响用户体验。或者,他们可能会向端点发送具有非常高版本号的推送,以便您的应用程序服务器执行的后续有效推送不会传递到应用程序。

推送服务器和设备之间的连接始终通过安全连接,因此在交换的这一部分可以合理地预期安全性。

通过以上介绍,我们简要了解了 Firefox OS 推送通知 API。看到您开发者如何利用它来创建出色的 Web 应用程序,将是一件令人兴奋的事情。

可以在irc.mozilla.org上的#push IRC 频道联系推送通知团队。

其他

推送通知交付保证

推送通知是一个尽力而为的系统。推送服务器将尝试通知设备其为端点收到的最新版本。这意味着更新会被折叠。如果在设备离线时将端点更新到版本 254,然后更新到 255,然后设备上线,它将只接收版本 255 的更新。当设备和服务器最终处于无法修复的不一致状态时,通常会触发push-register以允许应用程序重置自身。尽管采取了这些预防措施,但在某些情况下

通知可能无法交付

如果设备离线时间超过几天(一周或更长时间),推送服务器可能会放弃待处理的通知以节省其资源。在这种情况下,设备在重新上线后将不会接收更新,直到应用程序服务器发送新更新。如果用户在漫游时旅行并关闭移动数据,则可能发生这种情况。

如果设备电池没电或设备崩溃,恰好是在通知交付到应用程序时,应用程序将错过该通知。这种情况极其罕见。

push-register 可能不会交付

即使推送服务器与设备处于不一致状态后,push-register 也可能不会交付。

如果设备在推送服务器丢失状态时处于离线状态,推送服务器可能无法在设备重新上线时通知设备所有状态都已丢失。

对于大多数应用程序来说,以上情况不应该成为问题,因为它们发生的频率非常低。如果您的应用程序需要更高的可靠性,您可以使用其他一些方法来同步以减轻上述可能性。一种方法是定期获取新的端点,而不是等待push-register。另一种方法是使用 Alarm API 在较长一段时间后与应用程序服务器进行无条件同步。例如,应用程序将使用推送接收定期更新,但在每天午夜设置一个闹钟,无论推送状态如何,它都会去获取更新。

关于实验性质的法定警告

Firefox OS 的推送通知 API 处于实验阶段。Firefox OS 的新版本中即将发生的一项重大更改是将从 DOMRequest 切换到 Promises。在 Bug 800431 修复之前,调用register()的页面必须与处理通知(“push”系统消息)的页面相同。将来,我们希望添加对后台服务或工作程序的支持来处理通知。这将确保除非应用程序请求,否则不会启动任何 UI。可能会添加其他功能,并且在极少数情况下,现有 API 可能会发生更改。

虽然 Mozilla 会尽最大努力避免破坏现有应用程序,但开发者应该了解并计划可能需要对应用程序进行的更改。

关于 Nikhil Marathe

Nikhil 是 Mozilla 的平台工程师。他喜欢技术写作,撰写了“libuv 入门”一书,并在 http://blog.nikhilism.com 上撰写博客。当他不远足、攀岩或阅读时,他可能会在工作。

Nikhil Marathe 的更多文章…

关于 Robert Nyman [名誉编辑]

Mozilla Hacks 的技术布道师和编辑。发表关于 HTML5、JavaScript 和开放 Web 的演讲和博客。Robert 是 HTML5 和开放 Web 的坚定支持者,自 1999 年以来一直在从事 Web 前端开发工作——在瑞典和纽约市。他还定期在 http://robertnyman.com 上撰写博客,并且喜欢旅行和结识新朋友。

Robert Nyman [名誉编辑] 的更多文章…


4 条评论

  1. Ollie Parsley

    很棒的文章。使用处理程序,是否可以在后台处理推送消息,而无需向用户显示活动?在没有后台权限的情况下,因为这是针对特权应用程序的。例如,处理程序是否可以在后台,并在下次启动应用程序时将一些数据存储在本地存储中?希望这说得通

    2013 年 7 月 26 日 04:53

    1. Nikhil

      不幸的是,目前还不可能。我们希望解决这个问题,以便拥有类似后台服务的功能。相关的错误是 868322,以及 https://groups.google.com/forum/#!topic/mozilla.dev.webapi/diddFoHYLVs 中的讨论

      2013 年 7 月 26 日 13:00

      1. Ollie Parsley

        太棒了,Nikhil,感谢链接。我将跟踪它 :)

        2013 年 7 月 26 日 13:02

  2. Gene Vayngrib (@urbien)

    +1
    我们正在为 FirefoxOS 应用程序创建一个框架,并且已经确定了许多推送通知用例,例如数据库同步、应用程序更新等,并且只有其中几个需要 UI,大多数需要静默处理,请参阅 http://urbien.com/v.html?uri=sql/www.hudsonfog.com/voc/software/crm/Feature%3fsynopsis%3dSupport%2bPush%2bnotifications%2bon%2bmobile%26opened%3d2013-07-24%2b17:00:12

    顺便说一句,那些不静默的用例将需要两种 UI 变体,一种是显眼的——用于 WebRTC 调用,另一种是尽量不打扰的,例如有人喜欢你的帖子,最好只在通知中心显示。

    2013 年 8 月 3 日 20:54

本文的评论已关闭。