跳转到内容

Notifications API:系统通知

发布于 更新于

一句话: Notifications API 让网页得以控制向最终用户显示系统通知。Web 通知是一个由操作 系统自身的原生通知系统渲染的消息框,因此它的外观与该平台上任何其他应用的通知完全一致——并且 正因为由操作系统渲染,它位于顶层浏览上下文视口之外,即使用户切换了标签页或转到了另一个 应用,它也能被显示出来。它是通知的显示那一半;Web Push 是投递那一半,会在页面关闭时 唤醒你的 service worker。

该 API 仅在安全上下文(HTTPS)中可用(在部分或全部支持它的浏览器中如此),并且可在 Web Worker 中使用。

  • Notifications API —— 创建并显示一条通知。该接口同时暴露给 Window 与 Worker,而其 构造函数仅在 ServiceWorkerGlobalScope 中被禁止,因此一条非持久通知可以来自页面,也可以 来自其他 worker 上下文;service worker 则改用 showNotification()。
  • Web Push —— 让服务器在站点关闭时也能向用户设备投递消息;浏览器唤醒 service worker, 后者随即调用 Notifications API 显示内容。要记住的是这层分工:Notifications API 负责显示, 而 Web Push 让服务器能在应用关闭时主动发起投递。

会话内提醒直接用 Notifications API;必须在应用未打开时送达的消息,则与 Web Push 搭配。

规范是按 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 而非页面中处理激活:

service-worker.js
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,在通知被接受时会被打开。当设置了 navigate URL 时,非持久通知 不会触发 click 事件;用户代理会改为导航到该 URL。

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 不支持。

由于该接口可能整个是 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;不要 await navigator.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 规则。
  • 通知权限 —— 如何询问,以及被拒绝意味着什么。
  • 通知操作与徽标 —— 实践中的操作 按钮与应用图标徽标。