认识声明式网页推送

网页推送通知是网页平台强大而重要的一部分。

正如某位非常著名的叔叔曾经说过的那样,能力越大,责任越大。当我们将网页推送添加到 WebKit 时,我们深知必须保持人们对电源效率和隐私的期望。

我们在 macOS 上的Safari 网页推送、iOS 和 iPadOS 上保存到主屏幕的网页应用以及 Mac 上的网页应用中实现网页推送时,采取了深思熟虑的方法来维持这些期望。我们知道运行额外代码来显示通知可能会影响电池续航。我们知道网页推送对 service worker JavaScript 的依赖与我们对网页用户隐私的广泛方法相悖。我们了解到,我们认为用户隐私所必需的保护措施挑战了网页开发者对其他浏览器中网页推送的假设。因此,我们挑战自己,提出了对最终用户、网页开发者和浏览器更好的方案

声明式网页推送允许网页开发者请求网页推送订阅并显示用户可见的通知,而无需安装service worker。service worker JavaScript 可以选择性地更改传入通知的内容。与原始网页推送不同,service worker 未能显示通知不会受到惩罚;如果可选的 JavaScript 处理步骤失败,声明式推送消息本身将用作回退。

声明式网页推送在设计上更节能、更注重隐私。它对您——网页开发者——来说更容易使用。而且它向后兼容现有的网页推送通知。

继续阅读我们对当今网页推送工作方式所带来的挑战的思考。或者直接跳到如何使用声明式网页推送。您可以在 iOS 18.4、iPadOS 18.4 和 macOS 15.5 beta 版上进行测试。

现状

现有的网页推送通知是按照 JavaScript 优先的理念设计的。远程推送不再直接描述用户可见的通知,而是由网站的 service worker JavaScript 处理更抽象的“推送消息”概念。

网站首先需要注册一个 service worker。然后它可以使用该 ServiceWorkerRegistration 来创建 PushSubscription,这为网站提供了远程向浏览器发送推送消息所需的信息。

为了让用户拥有直接控制权,WebKit 要求作为开发者的您始终显示通知;不允许静默推送消息。因此,我们要求推送订阅将 userVisibleOnly 标志设置为 true。尽管这可能会令人沮丧,但原始的网页推送设计为了保护用户隐私和电池续航,使其成为必要。

一旦设备收到推送消息,浏览器会确保有一个 service worker JavaScript 实例,然后向其分派一个 PushEvent。处理该事件的代码会检查推送消息中的数据,并使用它调用 ServiceWorkerRegistration.showNotification(...) 来显示用户可见的通知。

尽管一些流行的 JavaScript 库抽象掉了其中的一些复杂性,但其中涉及大量代码,并且许多地方都可能出现细微的错误。

挑战 1 — 静默推送保护

回想一下,WebKit 要求在注册推送订阅时将 userVisibleOnly 标志设置为 true。ServiceWorker 的 PushEvent 处理程序中的 JavaScript 有责任显示用户可见的通知。允许网站远程唤醒设备进行静默后台工作是一种侵犯隐私的行为,并且会消耗能量。因此,如果事件处理程序因任何原因未能显示用户可见的通知,我们将撤销其推送订阅

不幸的是,service worker 脚本中的错误、网络状况或本地设备状况都可能阻止及时调用 showNotification。这些情况可能不总是脚本作者的错,并且难以调试。如果能对 userVisibleOnly 承诺进行技术强制执行,从而可以忽略静默推送惩罚,那会更好。

挑战 2 — 追踪数据

我们之前已经写过博客,我们还会再次写;隐私是一项基本人权。

从 Safari 的第一个版本开始,我们就专注于隐私。WebKit 超越了网页标准要求的隐私保护。随着网页平台的发展,我们保护用户隐私的策略也随之发展。这现在包括主动阻止和删除网站数据,例如智能追踪预防(简称 ITP)。

ITP 会删除您一段时间未访问的网站的所有网站数据。这包括 service worker 注册。尽管这可能会让网页开发者感到沮丧,但它是保护用户隐私的关键。考虑到我们致力于保护用户,这是一个我们有意做出的艰难权衡。

当我们实现网页推送时,这产生了一个困境。由于创建和使用推送订阅本质上与拥有 service worker 相关联,ITP 删除 service worker 注册将使推送订阅失效。由于强大的反追踪预防功能似乎与现有网页推送的 JavaScript 驱动本质根本上不符,如果网页推送通知可以在没有任何 JavaScript 的情况下传送,会不会更好?

那么,声明式网页推送在实践中是怎样的呢?

如何使用声明式网页推送

要使用任何形式的网页推送,您首先需要使用 PushManager 来获取推送订阅。Apple 平台上的网页推送使用与所有 Apple 设备上原生推送相同的 Apple 推送通知服务。您无需成为 Apple 开发者计划的成员即可使用它。

原始网页推送唯一可用的 PushManagerServiceWorkerRegistration.pushManager
声明式网页推送也公开了 window.pushManager,以支持在不需要 service worker 的情况下进行订阅管理。

const subscription = await window.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: arrayForPublicKey
});

如果您确实还有一个注册的 service worker,其作用域是您的网站域的根级别,它将与 window 对象共享相同的推送订阅。但是,删除该 service worker 注册不会影响相关的推送订阅。

向该推送订阅发送推送消息的工作方式与以前完全相同。为了以声明方式处理通知,推送消息的“内容”必须符合声明式标准 JSON 格式。这种标准化格式保证浏览器有足够的信息来显示用户可见的通知,而无需任何 JavaScript。

{
    "web_push": 8030,
    "notification": {
        "title": "Webkit.org — Meet Declarative Web Push",
        "lang": "en-US",
        "dir": "ltr",
        "body": "Send push notifications without JavaScript or service worker!",
        "navigate": "https://webkit.ac.cn/blog/16535/meet-declarative-web-push/",
        "silent": false,
        "app_badge": "1"
    }
}

顶层 "web_push" 值是对 RFC 8030 – 使用 HTTP 推送的通用事件传递的致敬。这是一个神奇的值,它使您的推送消息的其余部分能够进行声明式解析。

"notification" 值是一个字典,它向浏览器描述用户可见的通知。就像您在 JavaScript 中以编程方式创建通知一样,需要一个非空的 "title" 值。NotificationOptions 字典的大多数可选成员也可以指定。

到目前为止,我们主要讨论了无需 JavaScript 即可自动显示通知。当用户激活声明式通知时,需要发生一些无需 JavaScript 即可有用的事情。这就是必需的 "navigate" 值的作用。它描述了浏览器在激活时将导航到的 URL。

最后,如果网页应用支持以支持Badging API的类应用模式运行,例如iOS 上的主屏幕网页应用,则声明式消息可以包含更新的应用角标。

关于向后兼容性的说明

实际上,绝大多数网页推送消息已经是 JSON 格式。它们描述了要显示的用户可见通知。处理这些推送消息的 service worker JavaScript 只需解析 JSON 即可通过编程方式显示通知。但这些 JSON 消息的格式因网站而异。

大多数应用程序会发现,在推送消息中发送声明式标准 JSON 并重写其 service worker 的 PushEvent 处理程序以显示它,是简单明了的。一旦完成这两个步骤,这些网页推送消息将向后兼容尚未支持声明式网页推送的浏览器。

如果您的推送消息到达较新的浏览器,它将由浏览器以声明方式处理。如果它到达较旧的浏览器,它将像往常一样由 JavaScript 以命令方式处理。

始终采用声明式标准 JSON 的一个好处是可以在所有项目中引入一致性,进一步减少多产网页开发者的维护负担。

如果我无法通过互联网发送通知描述怎么办?

所有应用程序——无论其平台如何——可能都无法通过推送服务发送通知的可见内容。通知应如何显示通常取决于用户设备上的本地应用程序状态。也许用户以服务器尚未知晓的方式使用了该应用程序,需要更新通知。或者应用程序用于安全通信,通知负载的解密密钥仅存在于设备上的应用程序中。

在这些情况下,需要代码来处理传入的推送消息以显示有意义的内容。

遇到这些边缘情况的 iOS 原生应用程序有一个名为 UNNotificationServiceExtension 的工具,它允许一小段应用程序代码响应传入的推送通知运行。传入的通知始终包含足够的内容来显示用户可见的“回退通知”,但应用程序的通知服务扩展获得少量时间来查询应用程序的本地数据存储并提出一个新的、更有意义的通知。

如果通知扩展中存在错误,或者所需数据不可用,或者其他一些不可预见的场景导致它未能及时显示不同的通知,则会显示原始的“回退内容”。

对于具有声明式网页推送的网页应用,service worker JavaScript 扮演着相同的角色。当声明式网页推送消息到达并且 service worker 已安装时,推送事件会像以前一样分派给它。

PushEvent 现在具有来自声明式网页推送消息的“建议通知”的上下文。如果事件处理程序正确显示了替换通知,则建议通知将被忽略。如果事件处理程序未能及时显示替换通知,则使用回退方案。

因为总是存在用户可见的通知——因此,破坏隐私的静默推送仍然是不可能的——浏览器不必对声明式网页推送消息施加其“静默推送惩罚”。

iOS 还支持卸载未使用的原生应用,这可以释放应用程序代码占用的存储空间,同时保留最少的应用程序功能。在这种情况下,UNNotificationServiceExtension 代码已不存在,但应用程序仍可以显示未修改的通知。

这与 iOS 上在用户或 ITP 移除网站的 service worker JavaScript 后,声明式网页推送通知的工作方式非常相似。传入推送消息的修改不再可能,但未修改的通知仍然可以显示。

标准工作

我们对声明式网页推送感到兴奋,并希望它能在任何地方得到支持。

在我们发布WebKit 声明式网页推送解释器前后,我们还在 TPAC 2023 上与其他浏览器厂商和相关方讨论了它,并针对推送 API 提出了一个问题进行讨论。尽管标准机构总是会纠结于细节,但该提案的总体目标得到了很好的实现。

2024 年,我们积极向涉及的各种规范提出了提案,并根据合理的反馈对我们的实现进行了更改。

我们认为该功能具有足够坚实的基础,可以发布它并让网页开发者开始尝试。我们预见到基于这一坚实基础的未来增强。我们希望它能尽快广泛可用。

我们鼓励您通过WebKit 的 Slack 或我们的问题追踪器与我们联系,分享您使用这项出色新功能的经验。