Notifications API:系统通知
发布于 更新于
一句话: Notifications API 让网页得以控制向最终用户显示系统通知。Web 通知是一个由操作 系统自身的原生通知系统渲染的消息框,因此它的外观与该平台上任何其他应用的通知完全一致——并且 正因为由操作系统渲染,它位于顶层浏览上下文视口之外,即使用户切换了标签页或转到了另一个 应用,它也能被显示出来。它是通知的显示那一半;Web Push 是投递那一半,会在页面关闭时 唤醒你的 service worker。
该 API 仅在安全上下文(HTTPS)中可用(在部分或全部支持它的浏览器中如此),并且可在 Web Worker 中使用。
Notifications API 对比 Web Push
Section titled “Notifications API 对比 Web Push”- Notifications API —— 创建并显示一条通知。该接口同时暴露给
Window与Worker,而其 构造函数仅在ServiceWorkerGlobalScope中被禁止,因此一条非持久通知可以来自页面,也可以 来自其他 worker 上下文;service worker 则改用showNotification()。 - Web Push —— 让服务器在站点关闭时也能向用户设备投递消息;浏览器唤醒 service worker, 后者随即调用 Notifications API 显示内容。要记住的是这层分工:Notifications API 负责显示, 而 Web Push 让服务器能在应用关闭时主动发起投递。
会话内提醒直接用 Notifications API;必须在应用未打开时送达的消息,则与 Web Push 搭配。
持久通知与非持久通知
Section titled “持久通知与非持久通知”规范是按 service worker 注册来划界的:**非持久(non-persistent)**通知是其 service worker 注册为 null 的通知,**持久(persistent)**通知则是其注册非 null 的通知。这一个区分几乎 决定了通知行为的其余一切。
| 非持久 | 持久 | |
|---|---|---|
| 创建方式 | new Notification(title, options) |
registration.showNotification(title, options) |
| 创建位置 | 任何允许使用该构造函数的作用域——Window 或非 service worker 的 Worker;MDN 把常见情形描述为浏览上下文,例如网页或标签页 |
具有 ServiceWorkerRegistration 的 Window 或 Worker 上下文 |
| 生命周期 | 与创建它的上下文绑定;MDN 的表述是:页面一旦关闭,通知便无法再被交互 | 可以在单个页面的生命周期之外继续保持可交互 |
| 事件 | show、click、close 在 Notification 对象上触发 |
notificationclick 与 notificationclose 在 ServiceWorkerGlobalScope 上触发 |
| 通知中心 | 用户代理不应将其显示在平台的“通知中心”中(若平台提供) | 用户代理应当将其显示在那里 |
| 操作按钮 | 若 options["actions"] 非空,构造函数抛出 TypeError |
支持(参见下方的逐浏览器表格) |
| 由什么表示 | 恰好一个 Notification 对象 |
零个或多个 Notification 对象 |
其中两个后果值得明说:
- 非持久通知在设计上就是转瞬即逝的。 用户代理应当在非持久通知创建后数秒就执行其关闭
步骤,且应当把它排除在通知中心之外;而持久通知才是用户代理应当显示在那里的那一类。这些
是
should级别的建议而非绝对要求,因此请把它们当作设计意图来理解:如果一条通知需要留存到 用户之后回头查看,持久通知这条路径才是为此而写的。 - 在移动端,持久通知是可移植的选择。 MDN 的指引非常直接:如果你的代码需要在移动设备上
运行,那么你必须使用持久通知,因为
Notification()构造函数在大多数移动浏览器上会抛出TypeError。「大多数」这个词是准确的——browser-compat-data 记录 Chrome for Android 总是抛错, 而 Firefox for Android 镜像桌面版 Firefox,在那里构造函数是受支持的。按持久通知这条路径来写, 正是让代码跨越这道分裂的办法。
// 持久通知:能跨越上述移动端分裂的那条路径。// `ready` 正是「把执行推迟到 worker 活跃为止」的工具——在你已确知本页会注册 worker 时// 用它没问题。但它不适合用在*检测*路径:那里它永不 reject 且会无限期等待,// 详见下文「如何在运行时检测」。async function notify(title, body) { const registration = await navigator.serviceWorker.ready; await registration.showNotification(title, { body, data: { url: '/inbox' } });}在 service worker 而非页面中处理激活:
self.addEventListener('notificationclick', (event) => { event.notification.close(); event.waitUntil(clients.openWindow(event.notification.data.url));});ServiceWorkerRegistration.getNotifications() 会按创建顺序返回当前源通过当前 service worker
注册创建的通知列表——用于合并或清理你已经显示过的内容很方便。
显示通知需要用户授予当前源显示系统通知的权限。Notification.permission 是以下三个字符串之一:
-
granted—— 用户已明确授予当前源显示系统通知的权限。 -
denied—— 用户已明确拒绝。 -
default—— 用户的决定未知;此时应用会按权限被拒绝来行事。请把它理解为「目前还不能显示任何 东西」,而不要当成「用户从未见过提示」的保证。 -
用
Notification.permission读取当前状态。 -
用
Notification.requestPermission()请求,它会解析为最终状态。 -
从用户手势中调用它。 MDN 指出该方法只应在处理用户手势(例如处理一次鼠标点击)时调用。
-
选择会持续。 一旦做出选择,该设置通常会在当前会话内持续——因此在拒绝之后,你在该会话 内通常无法再弹出请求。
btn.addEventListener('click', async () => { // 先做守卫:在 iOS 标签页里该接口是 undefined,读取它会抛错。 if (!('Notification' in window)) return; const permission = await Notification.requestPermission(); if (permission === 'granted') { await notify('已为你开启', '有新动态时我们会通知你。'); }});options 常包含 body、icon、badge、tag(用于合并或替换通知)以及 data(供点击处理器
使用的负载)。另外两个也值得了解:
actions—— 操作按钮。Notification.maxActions是一个静态 getter,其步骤是返回所支持的 最大操作数量,所以请读取它,不要凭假设写死数量。切记构造函数会以TypeError拒绝非空的actions列表。navigate—— 一个 URL,在通知被接受时会被打开。当设置了navigateURL 时,非持久通知 不会触发click事件;用户代理会改为导航到该 URL。
浏览器与生态支持
Section titled “浏览器与生态支持”MDN 将 Notifications API 的可用性标注为有限——它不是 Baseline,因为它在部分最广泛使用的 浏览器中并不工作。来自 MDN browser-compat-data 的逐浏览器情况,其不均衡之处比版本号本身更值得 关注:
| 浏览器 | 起始支持 Notification |
browser-compat-data 记录的注意事项 |
|---|---|---|
| Chrome(桌面) | 20 | 自 Chrome 49 起,通知在隐身模式下不工作 |
| Chrome(Android) | 42 | 部分支持:通知只能从 service worker 发送,且构造函数总是抛出 TypeError |
| Edge | 14 | —— |
| Firefox | 22 | actions 自 Firefox 152 起才支持 |
| Safari(macOS) | 7 | 不支持 actions |
| Safari(iOS/iPadOS) | 16.4 | 部分支持:除非页面是已添加到主屏、且其 manifest 的 display 为非默认值的 web 应用,否则 Notification 接口是 undefined;通知只能从 service worker 发送 |
| Samsung Internet | 4.0 | 部分支持:仅通过 service worker 可用 |
| Android WebView / iOS WebView | 不支持 | —— |
iOS/iPadOS 那一行最让人意外,而它与 WebKit 自己的公告是吻合的:驱动通知的 Web Push 在 iOS 与 iPadOS 16.4 中是为已添加到主屏的 web 应用引入的,而非为在 Safari 标签页中打开的 站点。该平台的规则见 iOS 与 Safari Web Push。
actions 与 navigate 的支持落后于基础接口:actions 在 Chrome 53、Edge 18、Opera 39 与
Firefox 152 落地,Safari 不支持;而 navigate 在 Safari 18.4 中受支持,Chrome 不支持。
如何在运行时检测
Section titled “如何在运行时检测”由于该接口可能整个是 undefined(iOS 标签页),也由于即便接口存在构造函数仍可能抛错
(Android 上的 Chrome),请探测持久通知那条路径,并在其缺失时显式分支——不要假设可以退回到
new Notification()。
有一个细节决定了回退分支究竟会不会被执行:'serviceWorker' in navigator 只证明该 API 存在,
并不证明当前页面已有可用的 worker。ServiceWorkerContainer.ready 的用途是把代码执行推迟到
service worker 处于活跃状态为止——MDN 给出的契约是:它返回的 Promise 永远不会 reject,并会
无限期等待,直到与当前页面关联的注册拥有活跃 worker。因此在探测路径里 await 它没有有界结果:
当没有东西可供激活时,它既不兑现也不 reject,页面内回退永远不会执行。
getRegistration() 是那个会 settle 的替代方案——MDN 描述它 resolve 为一个
ServiceWorkerRegistration 或 undefined——但仅有注册还不够。
ServiceWorkerRegistration.active 返回状态为 activating 或 activated 的那个 worker,MDN 说明
没有时它为 null;一个注册可能只持有 installing 或 waiting worker,而按 Service Worker
规范,安装失败的 installing worker 会变为 redundant,此时活跃 worker 永远不会出现。所以请
直接检查活跃 worker,没有就返回 null,而不要把等待交给 ready:
async function getNotifier() { if (!('Notification' in window) || !('serviceWorker' in navigator)) { // 没有可用的 Notifications API —— 例如并非主屏 web 应用的 iOS 标签页。 return null; } // getRegistration() 无论如何都会 settle;而 `ready` 永不 reject 且会无限期等待。 const registration = await navigator.serviceWorker.getRegistration(); // 注册可能只持有 installing / waiting worker,而该安装还可能变成 redundant, // 所以这里要求活跃 worker,而不是在此等待激活。 if (!registration || !registration.active) { return null; } if (!('showNotification' in registration)) { return null; } return registration;}
async function alertUser(title, body) { const notifier = await getNotifier(); if (!notifier) { // 回退:把消息留在页面内,而不是直接丢掉。 showInAppBanner(title, body); return; } if (Notification.permission !== 'granted') { showInAppBanner(title, body); return; } await notifier.showNotification(title, { body });}- 使用前同时特性检测
Notification与navigator.serviceWorker,并为两者都不存在的分支保留页面内回退。 - 在检测路径中用
navigator.serviceWorker.getRegistration()取得注册并要求registration.active;不要 awaitnavigator.serviceWorker.ready——它永不 reject,且会为一个可能永远产生不出活跃 worker 的注册(只有installing/waiting,或安装失败)无限期等待。若确实需要等待激活,请监听 worker 的statechange区分activated与redundant,并加上你自己的超时,超时即走回退。 - 各处都优先使用
registration.showNotification()——它是移动端可移植的路径,是用户代理应当在通知中心里呈现的那一类,也是唯一接受actions的路径。 - 绝不要向
new Notification()传入actions:非空列表会抛出TypeError。 - 读取
Notification.maxActions,而不要写死能显示多少个按钮。 - 从用户手势、在上下文中、在用户选择加入提醒之后请求权限。
- 在拒绝之后隐藏该功能,而非在该会话内再次弹出请求——该设置通常会在会话内持续。
- 在 service worker 的
notificationclick事件中处理点击;聚焦或打开正确的页面。 - 在 iOS 与 iPadOS 上,把整个功能限制在 manifest 设置了非默认
display值的主屏 web 应用之内。 - 不要把通知当作 WebView 功能来交付:Android 与 iOS WebView 都不支持该接口。
- 当通知必须在应用关闭时送达时,与 Web Push 搭配。
- Web Push —— 投递那一半:VAPID 密钥、订阅与服务端 发送路径。
- iOS 与 Safari Web Push —— 主屏要求,以及 上表 iOS 那一行背后的 WebKit 规则。
- 通知权限 —— 如何询问,以及被拒绝意味着什么。
- 通知操作与徽标 —— 实践中的操作 按钮与应用图标徽标。