想象你常去的咖啡馆出了新品,店员说:“回头给你寄张优惠券。”可你不住在店里,信怎么到你手上?现实世界靠的是邮局——寄信人写好地址,邮局分拣、运输,最后投进你家的信箱,哪怕你不在家。Web 世界里也藏着一座电子邮局:网站的服务器是寄信人,负责运输的是“推送邮局”,你手机里住着一位全年无休的看门人,专门替你签收信件;最后,那封摊开在你眼前的信纸,就是浏览器弹出的通知。更妙的是,这封信从寄出起就锁在加密信封里,连邮递员都偷看不走。本文将把这座信息邮局从寄信、运输、投递到拆信的每个环节拆开讲透,读完你就能为自己的应用铺一条“人未到、消息先到”的邮路。

岗位关联

掌握这套“信息邮局”,是 PWA 与 AI 应用工程师的必修课:模型跑完的异步结果、个性化推荐、系统异常告警,都能第一时间送到用户手边,让 AI 从“等你来问”变成“主动来找你”。

目录
  1. 可视化演示
  2. 概念辨析:邮递员与信,是两回事
  3. 订阅生命周期:先登记地址,邮路才能通
  4. 消息加密机制:给信里加一道双人锁
  5. 通知交互流程:信到了,然后呢?
  6. 权限策略:别在人家门口硬塞广告
  7. 跨浏览器兼容:各国有各国的邮规
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

演示Web Push推送服务注册、订阅生命周期、消息载荷加密与通知交互,理解权限策略与跨浏览器兼容。

本节配有交互动效(进入视口后自动加载,约 3KB)。

推送流程 服务器 Service Worker srv→not 端到端推送管道

概念辨析:邮递员与信,是两回事

常去咖啡馆寄信,你得先分清两样东西:一个是骑着车跑腿的邮递员,一个是你手里拆开的那封信。Web 世界里,Push(推送)就是那个邮递员,负责把消息从服务器“主动”送到浏览器;Web Notification 则是你眼前展开的信纸,负责把内容展示给用户。它们是两个独立 API,却总被初学者混为一谈。

为什么需要分工?因为“投递”和“展示”的时机完全不同。Push 消息可以在页面关闭、浏览器退到后台时悄悄抵达;而展示必须发生在“用户看得到”的界面层。于是浏览器设计了一个中间人——Service Worker,它常驻后台,负责签收推送消息,再调用 Notification API 把内容弹出来。

完整链路是:应用服务器把消息 POST 到推送服务,推送服务按订阅地址投递到用户设备,浏览器唤起 Service Worker 的 push 事件,事件回调里调用 self.registration.showNotification(...) 展示通知。三者各司其职:Push 管“怎么送到”,Service Worker 管“何时接收”,Notification 管“如何呈现”。

所以下次有人说“我收到了推送”,你可以追问一句:“是邮递员到了,还是信纸已经摊开了?”只有 Push 投递加上 Notification 展示首尾衔接,才构成完整的通知体验——这也是 PWA 离线和后台场景下最常用的组合拳。

Notification=Push→Service WorkershowNotification(title, body, icon)\displaystyle \text{Notification} = \text{Push} \xrightarrow{\text{Service Worker}} \text{showNotification}(\text{title},\ \text{body},\ \text{icon})
这条式子表达了“到达 ≠ 展示”:Push 把信送到,Service Worker 中转签收,Notification 最终摊开信纸。
  1. Push 投递服务器将消息交给推送服务,如同寄信人把信交给邮局分拣。
    POST endpoint⟶PushService\text{POST}\ \text{endpoint} \longrightarrow \text{PushService}
  2. Service Worker 中转浏览器后台进程按订阅唤醒,如同看门人常年守在门口签收信件。
    SW.onpush: message→event handler\text{SW.onpush}:\ \text{message} \rightarrow \text{event handler}
  3. showNotification 展示调用浏览器本地 API 弹出通知,如同把信纸摊开在桌上。
    showNotification(title, body)→visible notification\text{showNotification}(\text{title},\ \text{body}) \rightarrow \text{visible notification}

订阅生命周期:先登记地址,邮路才能通

邮局寄信有个前提:它得先知道你的地址。Web Push 里,这个“登记地址”的动作叫订阅(subscribe)。用户首次打开页面,应用调用 pushManager.subscribe(),浏览器便生成一个独一无二的 PushSubscription 对象——这就是你的“电子门牌号”。

这个门牌号包含三件套:endpoint 是投递端点,相当于“XX路XX号”;p256dh 是公钥,相当于信箱的锁;auth 是认证密文,相当于备用钥匙。应用拿到订阅后要立刻 POST 给自己的服务器保存。服务器推送消息时,就靠 endpoint 作为收件地址,靠 p256dh 加密内容。

有生就有灭。用户可能关停账号、清除站点数据,浏览器也可能回收失效订阅。取消订阅时前端调用 subscription.unsubscribe(),同时必须请求服务器删除对应记录;服务器推送时若收到 404/410 响应,说明地址已废弃,也应主动清理。订阅还会过期(expirationTime),需要监听并自动“续订”,保持邮路畅通。

把订阅想象成一份“寄信契约”再合适不过:前端退订而服务端不删,就像你搬了家却忘了通知杂志社,广告照样寄往空屋,白费纸张。真正的工程实践里,收到 410 状态码主动清理订阅表,是每个推送系统必须写进代码里的“邮差自觉”。

Lifecycle: subscribe→persist→dispatch→unsubscribe→cleanup\displaystyle \text{Lifecycle}:\ \text{subscribe} \rightarrow \text{persist} \rightarrow \text{dispatch} \rightarrow \text{unsubscribe} \rightarrow \text{cleanup}
订阅的完整生命周期像一份“寄信契约”:建立时双方盖章,取消时双方销章,任何一端不同步都会让邮路变成死胡同。
  1. subscribe 创建浏览器生成订阅对象,如同邮局发给你一个专属信箱。
    pushManager.subscribe({userVisibleOnly:true})→PushSubscription\text{pushManager.subscribe}(\{\text{userVisibleOnly}: \text{true}\}) \rightarrow \text{PushSubscription}
  2. persist 持久化把 endpoint 与密钥发给应用服务器存档,如同把新地址抄进通讯录。
    server.save(sub.endpoint, sub.keys)\text{server.save}(\text{sub}.\text{endpoint},\ \text{sub}.\text{keys})
  3. unsubscribe 取消用户主动退订,门牌号作废。
    sub.unsubscribe()→false\text{sub}.\text{unsubscribe}() \rightarrow \text{false}
  4. cleanup 服务器同步服务端删除记录,否则信件会不断投往空屋。
    server.delete(endpoint)\text{server.delete}(\text{endpoint})

消息加密机制:给信里加一道双人锁

寄机密信件时,你会担心邮递员偷看。最简单的办法是买一把“双人锁”:只有你和收信人各持一把钥匙,才能共同打开铁盒,途经的邮递员只能看到盒子,看不到一个字。Web Push 的加密正是基于这个智慧——推送服务可以送信,但永远读不了信。

加密的第一步是身份认证。应用服务器用 VAPID 协议生成一对公钥/私钥:私钥在服务器上对 JWT 令牌签名,推送服务验证签名后确认“寄信人是真的”,再放行。没有这一步,任何人都能冒用你的名义给用户推送垃圾信息,相当于伪造寄件人地址。

第二步是密钥协商与加密。服务器取订阅里的 p256dh 公钥,与自己的私钥做 ECDH 椭圆曲线协商,得到一个双方共享的会话密钥;再用 AES-128-GCM 对消息载荷加密。数据到达设备后,Service Worker 用同样的共享密钥解密,再展示通知。整个过程推送服务只能转发密文,无法读取内容,实现端到端保密。

记住:推送服务是“邮局”,可以看信封上的地址,却永远不该看到信里的内容。凡是把消息明文交给推送服务的做法,都相当于把情书贴在明信片上寄出——用户数据和隐私安全,全靠这一层加密守护。

K=ECDH(appPriv, u2fPub),C=AES-128-GCM(M, K, nonce)\displaystyle K = \text{ECDH}(\text{appPriv},\ \text{u2fPub}), \qquad C = \text{AES-128-GCM}(M,\ K,\ \text{nonce})
前一半公式是“换钥匙”:双方各出一把钥匙,拼出一把公共锁;后一半公式是“上锁”:用这把公共锁把消息锁进密文铁盒。
  1. VAPID 签名服务器签发 JWT 证明身份,如同信封盖上骑缝章,防止冒名寄信。
    JWT=RSASSA-PKCS1(payload, vapidPrivKey)\text{JWT} = \text{RSASSA-PKCS1}(\text{payload},\ \text{vapidPrivKey})
  2. ECDH 协商双方私钥加对方公钥算出共享密钥,如同两把钥匙同时插入才能开锁。
    K=ecdh(appPrivKey, u2fPubKey)K = \text{ecdh}(\text{appPrivKey},\ \text{u2fPubKey})
  3. AES 加密用共享密钥加密消息体,中间节点只能看到乱码。
    C=AES-128-GCM(message, K)C = \text{AES-128-GCM}(\text{message},\ K)
  4. 解密展示Service Worker 收到密文后用同一共享密钥反向还原。
    M=AES-128-GCM−1(C, K)M = \text{AES-128-GCM}^{-1}(C,\ K)

通知交互流程:信到了,然后呢?

信送到你手上之后,剧情才刚刚开始:你可能拆开细读(点击),可能顺手夹进书里(留存),也可能看都不看丢进垃圾桶(关闭)。Web Notifications 用事件机制把这三类反应一一记录下来,让开发者能精准回应。

应用在 Service Worker 里调用 showNotification 时,可以指定 title、body、icon 等外观,还能塞一个 data 字段,把“这封信来自哪个订单”之类的上下文带进去。用户点击通知时,浏览器触发 notificationclick 事件,回调里通过 clients.matchAll() 找到已打开的页面窗口并调用 focus() 聚焦;如果没有窗口,则 clients.openWindow() 新开页面直达详情。

通知还可以带 actions 按钮(如“查看”与“忽略”),点击不同按钮会携带不同的 action 值,供回调区分处理。实践中常用 tag 字段对通知分组(同一 tag 的通知会合并替换),用 renotify 控制是否再次提醒,避免满屏轰炸;展示时机则应选在用户可能注意到的时刻,避开深夜打扰。notificationclose 事件则负责“用户手动划掉通知”这类清理动作。

交互设计上,好通知像一扇门:用户点一下就能到达想去的地方。如果通知点击后只是“哦我知道了”,甚至点了没反应,用户很快会把它划掉,你的邮路也就彻底失宠了——所以 data 带上文、action 给选项、tag 做分类,是三件必须养成的习惯。

event∈{notificationclick, notificationclose}⇒action(focus∪openWindow∪close)\displaystyle \text{event} \in \{ \text{notificationclick},\ \text{notificationclose} \} \Rightarrow \text{action}(\text{focus} \cup \text{openWindow} \cup \text{close})
这行式子把“用户动作”映射成“程序动作”:点击通知,系统要么聚焦旧窗口,要么开新页面,要么默默收起通知——每一种反应都有对应的代码回执。
  1. notificationclick用户点击通知,事件携带通知对象与按钮标识。
    e=notificationclick(notification, action, data)e = \text{notificationclick}(\text{notification},\ \text{action},\ \text{data})
  2. focus 聚焦找到已有窗口并拉起到前台,如同推门进入已经开着的房间。
    clients.matchAll→windowClient.focus()\text{clients.matchAll} \rightarrow \text{windowClient.focus}()
  3. openWindow 开窗没有窗口时新开页面直达目标页。
    clients.openWindow(url)\text{clients.openWindow}(\text{url})
  4. close 清理按钮点击后主动关闭通知,避免残留打扰。
    event.notification.close()\text{event.notification.close}()

权限策略:别在人家门口硬塞广告

想象一个推销员,见面第一秒就掏出喇叭喊“办卡吗”——你大概率关门。但如果他先在店门口放一杯免费试饮,等你尝了觉得不错,再问“要不要留个地址寄优惠券”,成交率就高得多。通知权限的请求,正是这个“要地址”的瞬间。

浏览器规定 Notification.requestPermission() 必须在用户手势(例如点击按钮)触发的代码路径里调用,才会弹出权限询问框。权限状态只有三种:granted(已授权)、denied(已拒绝)、default(未询问)。一旦用户选择“拒绝”,浏览器不再弹出询问框——这就像邮局把那个推销员拉进了黑名单。

所以工程上要讲究策略:先用站内引导让用户理解“订阅后能得到什么”,再在恰当时机(如用户完成一次有价值操作后)请求权限;被拒绝后不要反复轰炸,而是提供“重新开启”的站内说明,引导用户去浏览器设置中手动打开。收到推送后还要监测送达率,如果用户长期不点击,应主动降低推送频率,避免“邮件疲劳”。

权限的本质是“用户把家门钥匙交给你”的信任决定。尊重这个决定,在它不点头时不硬塞、被拒绝后不纠缠、拿到授权后克制地使用,你的邮路才能长久畅通——这也是为什么产品设计里“请求权限的时机”往往比“推送的内容”更决定成败。

permission={granted用户授权denied用户拒绝default未询问\displaystyle \text{permission} = \begin{cases} \text{granted} & \text{用户授权} \\ \text{denied} & \text{用户拒绝} \\ \text{default} & \text{未询问} \end{cases}
权限是一个三态开关:授权后邮路开通,拒绝后永不再问,未询问时则可以选择一个更好的时机开口。
  1. 状态读取任何时刻用 Notification.permission 查询三态。
    Notification.permission∈{granted, denied, default}\text{Notification.permission} \in \{ \text{granted},\ \text{denied},\ \text{default} \}
  2. 手势触发在用户 click 事件回调里发起请求,如同挑对方心情好的时候递名片。
    btn.onclick→Notification.requestPermission()\text{btn.onclick} \rightarrow \text{Notification.requestPermission}()
  3. 结果分支根据返回值决定是订阅还是走降级引导。
    result=granted ? subscribe():guide flow\text{result} = \text{granted} \ ?\ \text{subscribe}() : \text{guide flow}

跨浏览器兼容:各国有各国的邮规

出国寄信前你得研究当地邮规:有的国家要贴邮票、有的要填税号、有的周末不送。浏览器世界同样如此:Chrome 用自己的 FCM 做推送服务,Firefox 用 Mozilla 自建服务,Safari 直到 16.4 才完整支持 Web Push,而且要求用户先把站点加入程序坞或主屏幕。无视这些差异,你的“邮路”可能在某个浏览器上直接断掉。

工程上的第一道防线是特性检测:注册 Service Worker 前先确认 'serviceWorker' in navigator,再确认 'PushManager' in window 与 'Notification' in window。三者全部为真才走推送流程,否则降级为站内消息、邮件等替代方案,保证核心通知能力不断档。

具体差异要逐个击破:旧版 Chrome 需要 manifest 里的 gcm_sender_id;Firefox 与 Chrome 的订阅端点格式不同,服务器要用统一的 web-push 库封装;Safari 的权限请求 UI 与桌面端不同,需要额外的“添加到主屏幕”引导。调试时,Chrome DevTools 的 Application 面板可以手动触发 push 事件、查看订阅详情、模拟离线,是排查“信为什么没送到”的利器。

兼容性的心法是“先查路况、再定路线”:能走大路(标准 API)就走大路,不能走就抄小路(降级方案),永远别让用户因为浏览器不同而收不到信。把特性检测写成应用的第一道关卡,把降级方案做成最后的底线,这才是一个成熟工程师该有的邮局思维。

support=SWAvailability∧PushManagerAvailability∧NotificationAvailability\displaystyle \text{support} = \text{SWAvailability} \wedge \text{PushManagerAvailability} \wedge \text{NotificationAvailability}
支持度是三个能力的“与”关系:就像出远门前要同时确认护照、签证和机票,缺一样都走不了。
  1. 逐项检测三个能力缺一不可,如同出远门先查三样证件。
    ’serviceWorker’∈navigator∧’PushManager’∈window\text{'serviceWorker'} \in \text{navigator} \wedge \text{'PushManager'} \in \text{window}
  2. 分支处理能力齐全走推送,否则走降级通道。
    supported⇒subscribe()∣站内信兜底\text{supported} \Rightarrow \text{subscribe}() \enspace | \enspace \text{站内信兜底}
  3. 统一封装服务端用 web-push 库适配各家端点格式。
    webpush.sendNotification(sub, payload, options)\text{webpush.sendNotification}(\text{sub},\ \text{payload},\ \text{options})

AI 应用场景

AI 智能触达引擎:让模型决定何时寄信

阶段:个性化推荐 / 用户触达

解决的问题:传统推送“定时轰炸”,用户烦不胜烦、卸载率飙升。借助用户行为序列训练点击率预测模型,AI 算出每位用户未来 1 小时内的最佳接收窗口与最可能感兴趣的内容主题,再由 Web Push 在最佳时刻投递,送达率、打开率显著提升。

具体案例:某内容社区上线“智能推送”:模型给每条待推送内容打分,深夜只推“助眠电台”类内容;用户在工作时段收到行业早报;连续 7 天不点击的用户自动降频,改为每周摘要。上线后通知打开率提升 38%,卸载率下降 12%。

大模型异步任务完成通知:模型跑完了,喊你一声

阶段:异步任务编排 / 任务状态回调

解决的问题:文生图、视频生成、语音合成等大模型任务耗时数秒到数分钟,用户关掉页面去干别的事,结果出来却无人告知。通过后端任务回调触发 Web Push,结果生成后第一时间通知用户,无需用户一直盯着进度条。

具体案例:某 AI 绘画站让用户提交“生成 4 张插画”任务后切到后台,模型跑完服务端调用推送服务,用户手机弹出“你的画作已出炉”,点击直达下载页。单次任务流失率下降 25%。

AI 异常监控告警:模型当保安,告警靠推送

阶段:智能运维 / 实时告警

解决的问题:业务指标异常发现滞后,值班人员错过黄金响应期。AI 异常检测模型对时序指标流式打分,一旦偏离基线立即通过 Web Push 推送告警,PWA 方式让值班员无需安装 App 也能实时收到。

具体案例:某电商数据团队部署 AI 告警:模型监测到支付接口成功率跌破阈值,5 秒内推送“支付链路异常,成功率 92%,请立即查看”,值班员点通知直达监控大盘,平均故障恢复时间缩短 40%。

Python 演示

python87 行
# -*- coding: utf-8 -*-"""Simulate the Web Push pipeline: permission, subscribe, encrypt, deliver, unsubscribe."""  class PushClient:    def __init__(self, user_id):        self.user_id = user_id        self.subscription = None  # 已登记的收信地址        self.permission = 'default'  # 权限三态: granted/denied/default     def ask_permission(self, yes=True):        if yes:            self.permission = 'granted'        else:            self.permission = 'denied'        return self.permission     def subscribe(self, endpoint, public_key, auth_secret):        if self.permission != 'granted':            raise RuntimeError('No permission, cannot subscribe')        self.subscription = {            'endpoint': endpoint,            'p256dh': public_key,            'auth': auth_secret,        }        return self.subscription     def unsubscribe(self):        self.subscription = None  class PushService:    """Forward node between the app server and the browser."""     def __init__(self):        self.registry = {}     def save_subscription(self, user_id, sub):        self.registry[user_id] = sub     def send(self, user_id, message, server_key):        sub = self.registry.get(user_id)        if sub is None:            raise LookupError('Subscription missing, no receiver')        ciphertext = encrypt(message, sub, server_key)        return deliver(ciphertext, sub, server_key)  def _seed(sub, server_key):    raw = server_key + sub['p256dh'] + sub['auth']    return sum(ord(c) for c in raw) & 0xFF  def encrypt(message, sub, server_key):    seed = _seed(sub, server_key)  # 密钥协商得到共享密钥    return ''.join(chr(ord(c) ^ seed) for c in message)  def deliver(ciphertext, sub, server_key):    seed = _seed(sub, server_key)    plain = ''.join(chr(ord(c) ^ seed) for c in ciphertext)    return 'delivered: %s (cipher length: %d)' % (plain, len(ciphertext))  def main():    server_key = 'vapid-server-private-key'    alice = PushClient('alice')    print('1) request permission ->', alice.ask_permission(True))    sub = alice.subscribe(        'https://push.example.com/endpoint-alice-001',        'alice-p256dh-public-key',        'alice-auth-secret',    )    svc = PushService()    svc.save_subscription('alice', sub)    print('2) subscribed ->', sub['endpoint'])    print('3) server delivers:', svc.send('alice', 'Hello Web Push!', server_key))    alice.unsubscribe()    svc.registry.pop('alice', None)    try:        svc.send('alice', 'Nobody home', server_key)    except LookupError as exc:        print('4) after unsubscribe ->', exc)  if __name__ == '__main__':    main()
目的:模拟 Web Push 从权限申请、订阅登记、加密投递到取消订阅的完整生命周期,帮你直观看到“地址失效后信件被退回”的过程。
  1. if self.permission != 'granted':权限是订阅的前置条件,就像先拿到开门许可才能登记地址;权限不足直接抛错。
  2. self.subscription = {记录订阅三件套:endpoint 是收信地址,p256dh 是加密公钥,auth 是认证密文。
  3. svc.save_subscription('alice', sub)服务端持久化订阅,把新地址抄进通讯录,否则推送时找不到人。
  4. sub = self.registry.get(user_id)推送前先查订阅表,查无此号直接退信。
  5. raise LookupError('Subscription missing, no receiver')没有订阅还硬发消息,就像往空屋寄信,白费力气。
  6. return sum(ord(c) for c in raw) & 0xFF模拟从双方密钥派生共享会话密钥(真实实现用 ECDH + HKDF)。
  7. return ''.join(chr(ord(c) ^ seed) for c in message)模拟对称加密:用共享密钥逐字符异或生成密文。
  8. plain = ''.join(chr(ord(c) ^ seed) for c in ciphertext)模拟接收端用同一密钥解密还原消息。
  9. svc.registry.pop('alice', None)服务端同步删除订阅记录,防止继续向废弃地址投递。

常见误区

误区Push 通知和 Web Notification 不都是浏览器弹通知吗?我以为它们是同一个东西,学了 Notification API 就等于会推送了。

正解两者是完全不同的两个 API:Push 是“投递通道”,负责把服务器消息送到浏览器后台;Notification 是“展示层”,负责把内容弹给用户,中间还得靠 Service Worker 衔接。用比喻说,Push 是邮递员,Notification 是信纸——邮递员不拿信纸就无货可送,信纸不靠邮递员就不会自己跑腿,两者缺一不可。

为何常见:从用户视角看,“收到推送”和“看到通知”发生在同一个瞬间,两个名字又都带“通知”字样,很容易误以为是一个 API。

误区用户点了允许,我就等于拿到了一张无限次数的推送饭票,想发多少发多少,什么时候发都行。

正解订阅只是“地址登记”,不代表用户可以无限承受打扰。工程上还要考虑内容频率、用户活跃度、点击率等;用户长期不点击或主动关闭通知时,应主动降频甚至停止。而且推送服务有配额与节流策略,滥用会导致域名被限制,就像邮局会把乱寄广告的人列入黑名单。

为何常见:把“权限”理解成“所有权”,忽略了尊重用户注意力的产品伦理——用户允许你寄信,不等于允许你寄垃圾。

误区用户取消订阅?我在浏览器里调一下 subscription.unsubscribe() 就完事了,服务器不用管。

正解取消订阅是“三端联动”:浏览器端取消 + 应用服务器删除记录 + 推送服务侧识别失效端点。如果只在前端取消,服务器仍会向已失效的 endpoint 发消息,收到 404/410 不仅浪费资源,还会影响推送服务的信誉评分,相当于搬家后旧地址还在杂志社的通讯录里。

为何常见:只盯着自己写的页面代码,忽视了订阅信息早已同步到服务器这条“邮路”——本地退订只是撕掉了自己手里的那份契约。

误区消息反正要过浏览器,推送服务又是官方提供的,直接明文发送没啥问题吧?

正解推送服务本质上是一个中间代理,理论上能看到转发的所有内容。Web Push 标准强制使用 VAPID 身份认证 + ECDH 密钥协商 + AES-128-GCM 加密,正是为了让中间节点“能送信、不能读信”。安全设计应遵循“永远假设中间链路不可信”,就像即使邮局是国家的,机密信件你也会上锁。

为何常见:看到“官方服务”字样就放下戒备,忽略了“官方能送信”与“官方能读信”是两件完全不同的事。

面试题

Q1. 「推送」和「通知」在 PWA 中的关系是什么?

单选题 · easy

  • A.两者是完全相同的一件事
  • B.推送负责投递通知负责展示← 正确答案
  • C.通知负责投递推送负责展示
  • D.Push 与 Notification 互不相关

答案:B。Push 管“送到”,Notification 管“展示”:前者是服务器经过推送服务投递到浏览器后台的通道,后者是浏览器本地弹出通知的 API,二者由 Service Worker 衔接。选 A 混淆了传输与展示,选 C 把分工颠倒,选 D 则忽略了二者的协作关系。

Q2. 推送消息到达浏览器时由哪个对象接收?

单选题 · easy

  • A.页面里的业务 JavaScript
  • B.浏览器后台 Service Worker← 正确答案
  • C.浏览器插件或外部扩展程序
  • D.推送服务直接展示通知给用户

答案:B。推送到达时页面可能根本没打开,浏览器会唤醒注册过的 Service Worker 来处理 push 事件;页面 JS 无法在后台接收。插件并非标准接收者,推送服务只负责运输、不负责展示。

Q3. PushSubscription 中哪个字段相当于收信地址?

单选题 · easy

  • A.p256dh 公钥字段
  • B.auth 认证信息字段
  • C.endpoint 投递端点地址← 正确答案
  • D.expirationTime 过期时间

答案:C。endpoint 是浏览器为订阅生成的专属 URL,服务器向它 POST 消息即可投递,正是“收信地址”。p256dh 是加密用公钥,auth 是派生密钥用的认证密文,expirationTime 记录过期时间,都不是地址本身。

Q4. VAPID 协议在 Web Push 中的作用是什么?

单选题 · mid

  • A.对推送消息内容做端到端加密保护
  • B.验证应用服务器的合法身份与来源← 正确答案
  • C.压缩消息体以降低网络传输开销
  • D.将消息从 HTTP 升级成 WebSocket

答案:B。VAPID(Voluntary Application Server Identification)让应用服务器用私钥对 JWT 签名,推送服务验证后确认是真实的应用方,防止冒名推送。它不负责加密内容、不压缩、不改变传输协议——载荷加密靠的是 ECDH + AES-GCM。

Q5. 用户取消订阅后正确的清理动作是哪些?

多选题 · mid

  • A.浏览器端调用 subscription.unsubscribe← 正确答案
  • B.从应用服务器删除该用户订阅记录← 正确答案
  • C.删除本机浏览器缓存与全部历史记录
  • D.因订阅已取消无需做任何额外操作

答案:AB。取消订阅是“三端联动”:浏览器 unsubscribe 释放本地订阅,服务器删除记录停止投递;推送服务侧的旧 endpoint 会在投递时报 404/410,由服务器收到后清除。C 是过度操作,D 会留下脏数据导致空投递。

Q6. Notification.permission 可能存在哪三种取值?

单选题 · mid

  • A.granted、denied 与 default 三种状态← 正确答案
  • B.allow、block 与 pending 三种状态
  • C.yes、no 与 maybe 三种暂存状态
  • D.subscribed 与 unsubscribed 两种状态

答案:A。Notification API 规定的权限三态是 granted / denied / default,其中 default 表示尚未询问、等同未授权。allow/block 是浏览器设置面板的显示词,yes/no/maybe 与 subscribed/unsubscribed 都是对权限模型的错误记忆。

Q7. 用户点击通知后触发哪个事件处理页面跳转?

单选题 · mid

  • A.push 事件负责接收服务器推送
  • B.notificationclose 事件负责关闭通知
  • C.notificationclick 事件代表用户点击行为← 正确答案
  • D.message 事件负责处理消息队列任务

答案:C。用户点击通知会触发 notificationclick 事件,回调中拿到通知对象与 data/action 后聚焦窗口或打开新页面。push 事件是消息到达时触发,notificationclose 是用户划掉通知时触发,message 是 postMessage 通道的事件,都不负责点击后的跳转。

Q8. Web Push 标准下消息加密采用何种算法组合?

单选题 · hard

  • A.使用 RSA 算法对消息进行签名保护
  • B.采用 ECDH 协商并经 AES-GCM 加密← 正确答案
  • C.消息以 Base64 编码后明文传输即可
  • D.使用 MD5 散列后混入随机数来传输

答案:B。标准 Web Push 加密链路是 ECDH 做密钥协商 + AES-128-GCM 对称加密消息载荷;VAPID 负责身份签名而非内容加密。RSA 签名或 MD5 散列都不是 Web Push 载荷加密的标准做法,Base64 更是只编码不保密。

Q9. 哪些信号代表服务器应判定订阅已失效?

多选题 · hard

  • A.用户在浏览器端取消订阅并完成了同步← 正确答案
  • B.推送服务返回 404 表示端点已不存在← 正确答案
  • C.推送服务返回 410 表示端点已废弃← 正确答案
  • D.服务器推送后收到 201 创建成功响应码

答案:ABC。订阅失效的信号包括:用户本地退订并同步、推送服务返回 404(端点不存在)、410(端点已废弃),服务器收到后应清理记录。201 表示成功投递,恰恰说明订阅仍然有效。

Q10. 关于 Web Push 投递机制,下列说法哪些正确?

多选题 · extreme

  • A.推送服务尽力而为且可离线暂存消息← 正确答案
  • B.消息载荷存在约 4KB 的大小上限← 正确答案
  • C.页面必须保持打开状态才能接信
  • D.推送等同于双向实时常驻长连接

答案:AB。Web Push 是“尽力而为”的异步投递:设备离线时推送服务可在 TTL 内暂存消息,载荷上限约 4KB,且不需要页面保持打开——Service Worker 在后台接收。它也不是常驻双向连接,浏览器会合并抑制通知,所以不能与 WebSocket 画等号。