Web 推送通知:运行原理与用好它的方法
Web 推送通知如何工作,从 Service Worker 和 VAPID 到包括 iOS 在内的真实浏览器支持情况,再到决定成败的授权体验与指标。
Web 推送通知是唯一一个渠道:一个糟糕的设计决定,就可能把你永久挡在客户之外。在错误的时机请求授权,访客点了“屏蔽”,那个浏览器就对你永远关闭了。没有二次提示,没有第二次营销活动,也没有挽回邮件。正是这种不对称,让人在写下第一条消息之前就值得先弄懂它的机制。
Web 推送通知是什么
Web 推送通知是从你的服务器发到订阅者浏览器的一条消息,由操作系统的通知中心展示,即使你的站点已经关闭也能送达。最后这一点才是关键:与页面内弹窗不同,Web 推送能触达此刻并没有在看你网站的人。
它由三个协同工作的 Web 平台 API 构成,而它们之间的分工解释了这个渠道的大部分行为:Service Worker API、Push API 和 Notifications API。
Web 推送的底层运行方式
Service Worker
Service Worker 是一个 JavaScript worker,充当 Web 应用、浏览器与网络之间的代理。它运行在独立线程上,没有 DOM 访问权限,并且在注册它的页面关闭之后继续存在。正是这种持久性,让它能在你的标签页都没打开时接收推送消息并展示通知。Service Worker 只能在安全上下文中运行,也就是 HTTPS,开发时 http://localhost 被视为安全。
订阅:一个 endpoint 加两把密钥
Service Worker 激活之后,页面调用 registration.pushManager.subscribe()。浏览器与厂商的推送服务通信,返回一个 PushSubscription,其中包含:
endpoint,推送服务接收消息的唯一能力 URLkeys.p256dh,P-256 曲线上的椭圆曲线 Diffie-Hellman 公钥keys.auth,一个认证密钥
你的服务器要把这三项都存下来,并把 endpoint 当作机密对待,因为任何拿到它的人都能向该订阅者发送消息。密钥的存在是因为负载采用端到端加密。RFC 8291 规定了做法:P-256 上的 ECDH 交换建立共享密钥,HKDF 从中派生出密钥,负载在 aes128gcm 内容编码下用 AES-128-GCM 封装。推送服务转发的是它读不懂的密文。
推送服务
你并不是把推送消息直接发给设备。你发给的是浏览器厂商运营的推送服务:Chrome 用 Google 的 FCM endpoint,Firefox 用 Mozilla 的 autopush,Safari 用 Apple 的推送服务。RFC 8030 “Generic Event Delivery Using HTTP Push” 定义了这套协议。你的服务器向订阅 endpoint 发起 POST,推送服务负责移动端投递中最难的部分:与设备保持一条省电的连接,离线时排队,消息到达时唤醒浏览器。这也是投递无法被保证的原因。如果设备关机时间够长,消息会按各自的 TTL 过期并被丢弃。
VAPID:证明是谁在发送
仅靠 endpoint 保密,安全性很薄弱。RFC 8292 引入了 Voluntary Application Server Identification,即 VAPID,让推送服务能分辨消息来自哪台应用服务器。
你在 NIST P-256 曲线上生成一对 ECDSA 密钥,只需一次。浏览器订阅时把公钥放进 applicationServerKey,从而把订阅与你的服务器绑定。每次推送时,你的服务器用对应私钥以 ES256 签发一个 JWT,其中带有指向推送服务来源的 aud 声明、不超过 24 小时的 exp 声明,以及可选的带联系方式的 sub 声明。推送服务会验证签名,因此仅仅偷走一个 endpoint 已不足以骚扰你的订阅者。
端到端的投递路径
- 页面注册 Service Worker,在授权通过后用你的 VAPID 公钥调用
subscribe()。 - 你的服务器把返回的 endpoint 和密钥存入订阅者记录。
- 发送时,服务器用
p256dh和auth加密负载,签发 VAPID JWT,并向 endpoint 发起 POST。 - 推送服务验证请求并投递加密消息。
- 浏览器以
push事件唤醒 Service Worker,由它解密负载并调用ServiceWorkerRegistration.showNotification()。 - 点击会在 Service Worker 中触发
notificationclick,你在这里打开目标 URL。
授权模型为什么如此严格
看看一次订阅授予了什么:一个在你站点未打开时仍会运行的后台进程,加上在操作系统通知界面上绘制内容的能力。所以浏览器把它锁在一道明确的、按来源划分的、由用户授予的权限之后,而且多数浏览器要求请求必须跟随一次真实的用户手势。
第二条约束常让人意外。Chrome 和 Edge 要求订阅时带上 userVisibleOnly: true,这是一个承诺:每一次推送都会产生用户可见的通知,因此静默的后台推送并不是这个 API 支持的用法。Firefox 也会对不产生通知的推送消息设置配额。
浏览器与平台支持
按 MDN 的说法,Push API 自 2023 年 3 月起已是 Baseline 广泛可用,意味着它在桌面端的 Chrome、Edge、Firefox 和 Safari 当前版本上可用,在 Android 的 Chrome 和 Firefox 上也可用。有两点注意事项比这个结论本身更重要。
第一,Notifications API 并非各处一致可用。MDN 标注它为有限可用,因为 Notification() 构造函数在大多数移动浏览器上会抛出 TypeError。凡是必须在手机上工作的场景,请改用 ServiceWorkerRegistration.showNotification() 的持久化通知,反正你走的就是 Service Worker 这条路。
第二,通知选项的实现参差不齐。操作按钮、角标、图片和 requireInteraction 在不同浏览器和操作系统上表现不一,所以要把通知设计成只有标题、正文和图标时也读得通。
iOS 与 iPadOS 的前提条件
这条限制决定了 Web 推送对以移动端为主的受众是否可行,而它几乎在所有地方都被讲错了。
Apple 在 iOS 与 iPadOS 16.4 中加入了 Web Push,并且只对已添加到主屏幕的 Web 应用有效。用 WebKit 的原话说,“我们正在为主屏幕 Web 应用加入对 Web Push 的支持”,而且“已添加到主屏幕的 Web 应用可以请求接收推送通知的权限”。用户通过分享菜单里的“添加到主屏幕”完成这一步,之后授权请求必须由用户的直接操作触发,例如点击一个订阅按钮。
在 iPhone 上以普通 Safari 标签页打开的站点无法创建推送订阅。这是一道真实的门槛:你还没来得及请求授权,就先要求对方完成一次安装动作。在 macOS 上情况轻松得多,因为 macOS Ventura 上的 Safari 16.1 为普通网站带来了基于标准的 Web Push,无需任何安装步骤。
一个最小订阅示例
这就是客户端的完整流程。它应该放在点击处理函数里,而不是页面加载时。
async function subscribeToPush(vapidPublicKey) { // 推送和 Service Worker 需要安全上下文(HTTPS)。 if (!("serviceWorker" in navigator) || !("PushManager" in window)) return null;
const registration = await navigator.serviceWorker.register("/sw.js");
// 必须由用户手势触发调用,且每位用户只调用一次。 const permission = await Notification.requestPermission(); if (permission !== "granted") return null;
const subscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: vapidPublicKey, // base64url 编码的 P-256 公钥 });
// 在服务端保存 endpoint 与密钥;把 endpoint 当作机密对待。 await fetch("/api/push/subscribe", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(subscription), });
return subscription;}在 sw.js 里,你处理 push 事件,调用 self.registration.showNotification(title, options),并处理 notificationclick 以打开目标 URL。
授权体验:多数项目失败的地方
永远不要在页面加载时询问
Lighthouse 为此专门设了一项审计:“如果你的页面在加载时就请求发送通知的权限,这些通知可能与用户或其需求并不相关”。它的建议是提供一种具体类型的通知,只有在用户选择了该类型之后才请求授权。Safari 12.1 和其他浏览器走得更远,要求必须先与页面发生交互才允许发起请求。
使用软性预提示
先展示你自己的页面内邀请。它说明价值主张,可以被关闭且没有永久代价,只有点击它才会触发真正的浏览器提示。今天忽略软提示的人,下个月还能再问一次;点了“屏蔽”的人则不能。有两条规则让它奏效:说清楚你实际会发什么,而不是“获取更新”;绝不要诱导误点,因为误触带来的接受会立刻换来退订。
在恰当的场景中询问
Chrome 自己的建议是“让用户自己主动,按他们自己的节奏开启通知”,把开关低调地放进已有的界面里,并避免“在没有场景铺垫的情况下,或在用户刚落地站点时就展示提示或浮层”。
在电商中真正奏效的时刻很具体:售罄商品上的“到货通知我”控件、提供配送更新的订单确认页、常看商品上的降价提醒开关。授权是用一项被明确说出的好处换来的,而不是被顺手收割的。
一次拒绝几乎是永久的
当有人屏蔽通知时,浏览器会为你的来源保存这个决定。之后调用 Notification.requestPermission() 会直接返回已存储的 denied 值,不展示任何内容,这也是 MDN 自己的示例在调用 requestPermission() 之前先检查 Notification.permission 的原因。撤销屏蔽意味着要翻进站点设置,而基本上没人会这么做。
做错了浏览器会怎么处理
后果早已不只是订阅率低。
- Chrome 会自动把接受率极低的来源纳入更安静的授权界面,并按设备类型分别判断,对所有人抑制提示。
- Chrome 会对高推送量加低互动的站点施加速率限制,返回 HTTP 429。升级机制先是一天,然后七天,再十四天,只有在连续 42 天没有打扰行为后才重置。
- Chrome 现在会自动撤销用户近期没有互动过的站点的通知权限,条件是“用户互动极低,同时发送的通知量很大”。Google 给出的理由很直白:“所有通知中,得到用户任何互动的不足 1%。”
只要发得糟糕,你就会失去已经赢得的订阅者。
Web 推送与邮件、短信的对比
| 维度 | Web 推送 | 邮件 | 短信 |
|---|---|---|---|
| 触达范围 | 仅限已订阅的浏览器 | 任何有邮箱地址的人 | 任何有手机号的人 |
| 边际成本 | 基本为零 | 极低 | 按条计费,最高 |
| 即时性 | 数秒,由操作系统呈现 | 数分钟到数天,埋在收件箱里 | 数秒 |
| 消息长度 | 一个标题加一行短正文 | 不限长度,富文本排版 | 每条约 160 个字符 |
| 同意方式 | 浏览器提示,一次点击 | 收集地址,最好是双重确认订阅 | 明确同意且受严格监管 |
| 身份 | 一台设备上的一个浏览器 | 一个人 | 一个人 |
| 可迁移性 | 无 | 完整导出 | 完整导出 |
改变策略的所有权差异
一次推送订阅是绑定在某台设备某个浏览器配置上的能力 URL。它不是一个人。同一位客户在笔记本上用 Chrome、在手机上用 Firefox,就是两个互不相关的订阅,除非他们主动表明身份,否则你无从知道那是同一个人。
它也不可迁移。你可以导出邮件列表,明天就装进另一个平台。推送订阅无法在厂商之间搬迁,因为密钥和 VAPID 绑定是针对某个特定应用服务器密钥创建的。
所以把 Web 推送当作自有渠道的助推器,绝不要当作替代品。用推送这个时刻去换取一个邮箱地址或手机号,而不是反过来。营销自动化完整指南讲了如何把多个渠道接进同一条旅程。
真正有效的使用场景
这个渠道偏爱那些时效性强、与个人高度相关、一次点击即可行动的消息。
- 弃购挽回。 一小时内推送一次,稍后再用邮件加强。弃购邮件指南讲了节奏安排。
- 到货提醒。 最强的场景,因为用户明确要求被告知。
- 关注商品降价。 同样的逻辑,相关性由用户自选。
- 配送与订单状态。 打开意愿高,投诉风险低。
- 已订阅主题下的突发更新。 新闻、赛果、可预约时段。
失败的场景同样清晰:泛泛的“我们发布了新文章”广播、千篇一律的每日优惠、任何超过一个标题加一行的内容、向已经忽略了上二十条通知的订阅者做的唤醒轰炸,以及需要留存凭据的交易类内容。
频次、时机与细分
保守起步:每位订阅者每周一到三条,只有在退订率和点击率保持稳定时才增加。疲劳出现得比邮件更快,因为静音只需要在操作系统已经推到眼前的通知上点一下。
时机既是优势也是风险。推送即时送达,所以凌晨 2 点发出的消息就在凌晨 2 点到达。在订阅时存储或推断订阅者的时区,并把发送控制在设定的时间窗内。
细分受限于你对一次订阅而非一个人的了解,因此可用的维度都是行为类的:浏览过的页面、关注的商品、购物车状态、最近购买时间、平台。客户细分指南讲得更深。
衡量 Web 推送
有四个指标重要,而且它们的可测量方式并不相同。
- 投递。 推送服务是否接受了请求。201 表示已接受,不等于已送达。404 或 410 表示订阅已失效。
- 展示。 通知是否被显示。只有当
showNotification()完成后 Service Worker 回报时你才知道。 - 点击率。 点击数除以展示数。这是值得优化的数字。
- 退订率。 每次发送带来的退订与权限撤销。要比点击率盯得更紧,因为它是渠道走向死亡的先行指标。
归因陷阱
推送的归因容易自我美化。通知到达的是订阅者本来就握在手里的设备,所以它常常把一次本来也会发生的会话算作自己的功劳。用对照组,而不是假定增量存在。展示数被少计而每次点击都被记录,因此用发送数作分母算出的点击率会高估表现。又因为一次订阅是一个浏览器而不是一个人,在手机上点击、最终在桌面端完成购买的推送看起来像两件互不相关的事。邮件营销指标指南讲了跨渠道的衡量卫生。
同意、GDPR 与退订
浏览器的授权提示是技术闸门,它并不自动构成营销的完整合法性基础。
如果你面向欧盟或英国的人做营销,请像对待邮件一样对待推送。在提示出现之前说明你会发什么,让同意是知情且具体的。记录订阅创建的时间和位置,绝不要把推送同意捆绑进一个无关的操作。如果你把订阅与已识别的客户关联起来,那些数据就落入你的个人数据义务范围,包括删除请求。
退订卫生同样重要。提供站内偏好设置控件,让人们能降低频次而不是直接屏蔽;他们这么做时,调用 PushSubscription.unsubscribe() 并在服务端删除记录;遇到 404 或 410 时清除订阅。MDN 的指引简短而准确:应当为用户“提供一种简便的方式,让他们可以选择今后不再接收”。
Web 推送在渠道组合中的位置
Web 推送是一个好的第三渠道,却是一个糟糕的首选渠道:快、边际成本近乎为零、在时间敏感的提醒上无可匹敌,但绑定设备、无法导出,而且离永久失去只差一次点击。
于是编排才是真正的问题。哪条消息走哪个渠道,推送已经带来转化时如何抑制那封邮件,以及在识别方式各不相同的界面之间如何保持对客户的统一视图。Brevo 在邮件和短信之外还提供 Web 与移动推送,而 Tajo 构建在 Brevo 之上,为 Shopify 商店协调这套跨渠道逻辑。短信那一侧可以看短信自动化指南。
关键要点
- Web 推送是三个 API 的协作:Service Worker 负责后台执行,Push API 负责订阅与传输,Notifications API 负责展示。一次订阅是一个 endpoint 加一把
p256dh密钥和一个auth密钥,负载端到端加密,VAPID 证明发送方身份。 - 推送自 2023 年 3 月起已是 Baseline 广泛可用,但在 iOS 与 iPadOS 上只对添加到主屏幕的 Web 应用有效。
- 绝不要在页面加载时请求授权。使用软性预提示,在恰当场景中询问,并记住屏蔽对该来源是永久的。
- Chrome 现在会执行更安静的提示、速率限制和自动权限撤销,所以发得糟糕会让你失去已有的订阅者。
- 一次订阅是一个浏览器,不是一个人,而且无法导出。先建起邮件列表,再用推送去加速它。