通知 · API
Web Push 与 PushManager.subscribe()
发布于
registration.pushManager.subscribe() 向浏览器的推送服务申请一个 PushSubscription,你的服务器可以向它的 endpoint POST 加密消息;浏览器为每条消息唤醒 Service Worker 并触发 push 事件,即使没有页面打开,worker 随后显示通知。它与 Notifications API 一起构成 Web Push,是服务器触达已关闭应用的 PWA 用户的唯一标准方式。
registration.pushManager.subscribe(options)registration.pushManager.getSubscription()registration.pushManager.permissionState(options)subscription.toJSON()subscription.unsubscribe()subscribe() 以 PushSubscription 兑现;若该源已用同一密钥订阅,则返回现有订阅。getSubscription() 以当前订阅或 null 兑现。三者在窗口和 Service Worker 自身的 self.registration 上都可用。支持情况:Chrome 42、Firefox 44、Edge 17、macOS Ventura 上的 Safari 16、iOS 上的 Safari 16.4 仅限主屏幕 Web App(BCD api.PushManager.subscribe)。Firefox 72+ 要求 subscribe() 在用户手势内运行;Chromium 要求提供 applicationServerKey。
| 参数 | 类型 | 含义 |
|---|---|---|
options.userVisibleOnly |
boolean | 承诺每次推送都产生可见通知。Chromium 与 WebKit 对 false 以 NotAllowedError 拒绝;WebKit 还会撤销 push 处理函数未显示通知的订阅(Meet Web Push,webkit.org)。Firefox 接受 false,改为对静默推送施加配额。 |
options.applicationServerKey |
BufferSource 或 base64url 字符串 |
你的 VAPID 公钥,65 字节的未压缩 P-256 点。服务器用对应的私钥为每次发送签名(RFC 8292,ietf.org)。Chromium 中必填;Firefox 中没有它的订阅无法在不退订的情况下迁移到 VAPID。 |
返回的 PushSubscription 经 toJSON() 序列化为 { endpoint, expirationTime, keys: { p256dh, auth } }。endpoint 是浏览器厂商推送服务上的能力 URL(Chrome 为 https://fcm.googleapis.com/fcm/send/…,Safari 为 *.push.apple.com 主机);持有它的任何人都能向该设备发送,所以它要像凭据一样传输和存储。p256dh 与 auth 是客户端用于 RFC 8291 载荷加密的公钥和认证密钥。expirationTime 在所有现行引擎中都是 null。
| 异常 | 触发条件 |
|---|---|
NotAllowedError DOMException |
通知权限为 "denied",或用户关掉了 subscribe() 触发的提示;在 Chromium 或 WebKit 中 userVisibleOnly 为 false;在 Firefox 72+ 中调用不在用户手势内;Service Worker 的作用域不是安全上下文。 |
InvalidStateError DOMException |
注册没有活跃 worker,或已存在使用不同 applicationServerKey 的订阅。更换密钥前先退订。 |
InvalidAccessError DOMException |
applicationServerKey 不是合法的 P-256 公钥(长度错误或不是未压缩点)。 |
InvalidCharacterError DOMException |
applicationServerKey 的 base64url 字符串无法解码。 |
AbortError DOMException |
推送服务无法创建订阅,通常因为设备离线或服务不可达。稍后重试。 |
服务端的失败是推送服务返回的 HTTP 状态而不是异常:404 和 410 表示订阅已失效,必须删除;413 表示载荷超过 4 KB 上限;429 表示推送服务正在对应用服务器限流。
页面负责订阅,Service Worker 负责接收,第三段代码在做这两件事之前先检查已有的订阅。
在点击中订阅并把订阅发给服务器
Section titled “在点击中订阅并把订阅发给服务器”权限与订阅在同一个手势里完成。密钥以 base64url 字符串形式从服务器取得,规范允许 subscribe() 直接接受这种形式;旧教程里的 urlBase64ToUint8Array() 转换只在早于这一变化的引擎上才需要。
button.addEventListener('click', async () => { const registration = await navigator.serviceWorker.ready; const permission = await Notification.requestPermission(); if (permission !== 'granted') return; const { publicKey } = await (await fetch('/api/push/key')).json(); const subscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: publicKey, }); await fetch('/api/push/subscriptions', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(subscription.toJSON()), });});按用户和设备存储订阅,并在发送返回 404 或 410 时删除;持续向失效 endpoint 推送是投递静默失败最常见的原因。
接收消息并在 endpoint 变化时重新订阅
Section titled “接收消息并在 endpoint 变化时重新订阅”push 处理函数必须以 showNotification() 结束。推送服务轮换 endpoint 时触发 pushsubscriptionchange;worker 用同一密钥重新订阅并告知服务器。
self.addEventListener('push', (event) => { const data = event.data?.json() ?? {}; event.waitUntil( self.registration.showNotification(data.title ?? '有新内容', { body: data.body ?? '', data: { url: data.url ?? '/' }, }) );});
self.addEventListener('pushsubscriptionchange', (event) => { event.waitUntil((async () => { const key = event.oldSubscription?.options.applicationServerKey; const subscription = await self.registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: key }); await fetch('/api/push/subscriptions', { method: 'POST', body: JSON.stringify(subscription.toJSON()) }); })());});pushsubscriptionchange 在 Firefox 44(没有 oldSubscription,bug 1497429)、Safari 16 和 Chrome 138 中触发(BCD api.ServiceWorkerGlobalScope.pushsubscriptionchange_event);Chrome 138 之前服务器只能通过收到的 410 察觉轮换。
检测支持并复用已有订阅
Section titled “检测支持并复用已有订阅”先检查注册上是否有 PushManager,再优先调用 getSubscription(),这样刷新页面不会创建第二个订阅。兜底是轮询或应用内消息。
async function currentSubscription() { if (!('serviceWorker' in navigator) || !('PushManager' in window)) { return null; // 没有推送:应用打开期间轮询更新 } const registration = await navigator.serviceWorker.getRegistration(); if (!registration?.active) return null; return registration.pushManager.getSubscription(); // 已有订阅或 null}权限为 "granted" 却返回 null,说明用户还没有订阅,这正是显示第一个示例里那个启用按钮的时机。
配套演示 /demo/#push 调用 Notification.requestPermission(),再用为演示生成的 VAPID 公钥调用 pushManager.subscribe(),并打印得到的 PushSubscription JSON。它背后没有服务器:什么都不会发送,也不会有消息到达,因此是观察权限流程和订阅形状的安全场所。
- Push API: subscribe() method(w3.org)
- RFC 8292: Voluntary Application Server Identification (VAPID) for Web Push(ietf.org)
- Push notifications overview(web.dev)
- Notifications API
- iOS 与 iPadOS 上的 Web Push
- Notification.requestPermission()
规范
| 规范 | 状态 |
|---|---|
| Web Push(网页推送) | W3C |
- 图例
- 支持
- 部分支持
- 需开启标志
- 不支持
- 未知
| 浏览器 / 平台 | 支持 | 版本 | 置信度 | 来源 | 备注 |
|---|---|---|---|---|---|
| Chrome (Android) | 支持 | 50 | 中 | 来源 | — |
| Chrome (Desktop) | 支持 | 50 | 中 | 来源 | — |
| Edge (Desktop) | 支持 | 17 | 中 | 来源 | — |
| Safari (iOS) | 部分支持 | 16.4 | 中 | 来源 | 1 |
| Safari (macOS) | 支持 | 16 | 中 | 来源 | — |
| Firefox (Desktop) | 支持 | 44 | 中 | 来源 | — |
| Samsung Internet | 支持 | 5.0 | 中 | 来源 | — |
- 仅限已安装到主屏幕的 Web 应用;需要在用户手势下授权,并通过 APNs 投递 Web Push。
生态与商业政策
| 主体 | 类型 | 场景 | 状态 | 赞助 | 备注 |
|---|---|---|---|---|---|
| Apple Push (APNs) | delivery_policy | iOS | 部分支持 | 否 | iOS 上的 Web Push 要求用户先把应用添加到主屏幕。 |