跳转到内容

通知 · API

Web Push 与 PushManager.subscribe()

发布于

有限可用不支持的浏览器: Safari (iOS)W3C

registration.pushManager.subscribe() 向浏览器的推送服务申请一个 PushSubscription,你的服务器可以向它的 endpoint POST 加密消息;浏览器为每条消息唤醒 Service Worker 并触发 push 事件,即使没有页面打开,worker 随后显示通知。它与 Notifications API 一起构成 Web Push,是服务器触达已关闭应用的 PWA 用户的唯一标准方式。

Web Push 在网页、Service Worker、推送服务与应用服务器之间的时序图:页面请求通知权限,用 VAPID 公钥调用 pushManager.subscribe(),收到 PushSubscription 并发给服务器。服务器向 endpoint 发送带 VAPID 签名的加密请求,推送服务投递,浏览器在 Service Worker 内触发 push 事件,worker 调用 showNotification()。点按触发 notificationclick,pushsubscriptionchange 触发重新订阅。
Web Push 从订阅到通知:谁与谁通信,哪个事件在哪里触发。
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 察觉轮换。

先检查注册上是否有 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。它背后没有服务器:什么都不会发送,也不会有消息到达,因此是观察权限流程和订阅形状的安全场所。

规范

规范状态
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中来源—
  1. 仅限已安装到主屏幕的 Web 应用;需要在用户手势下授权,并通过 APNs 投递 Web Push。

生态与商业政策

主体类型场景状态赞助备注
Apple Push (APNs)delivery_policyiOS部分支持否iOS 上的 Web Push 要求用户先把应用添加到主屏幕。

源数据: /compatibility/web-push.json · 全球使用占比: 89 % (StatCounter 2026-05)

来源: 规范 · MDN · 最近核验 2026-06-24 · 置信度: 中 (由来源计算)