跳转到内容

通知 · API

Notifications API

发布于 更新于

Notifications API 通过操作系统自己的通知界面在页面之外显示消息,用户切换标签页或应用后消息仍然可见。PWA 以两种形态使用它:由 ServiceWorkerRegistration.showNotification() 创建的持久通知,它比页面活得久,也是移动端浏览器唯一允许的形态;以及页面里 new Notification() 创建的非持久通知。本条目讲持久路径与两者之分;构造函数见它自己的条目。

registration.showNotification(title)
registration.showNotification(title, options)
registration.getNotifications()
registration.getNotifications({ tag })

showNotification() 返回 Promise<void>,通知交给操作系统后兑现;getNotifications() 以该注册已显示的 Notification 对象兑现,按创建顺序排列,可按 tag 过滤。两者在窗口和 Service Worker 自身(self.registration)中都可用,且要求安全上下文(BCD api.ServiceWorkerRegistration.showNotification:Chrome 42、Firefox 44、macOS Ventura 上的 Safari 16、iOS 上的 Safari 16.4 仅限主屏幕 Web App)。

参数 类型 含义
title string 通知的第一行。必填。
options.body string 标题下方的正文。
options.icon string(URL) 文字旁的大图。
options.badge string(URL) 完整通知放不下时使用的小型单色图,例如 Android 状态栏。仅 Chromium;见 actions 与 badge。
options.image string(URL) 文字下方的大图。仅 Chromium。
options.tag string 标识符;相同 tag 的新通知替换旧通知。
options.renotify boolean 按 tag 替换时再次提醒用户。要求非空 tag。仅 Chromium。
options.requireInteraction boolean 通知保留在屏幕上直到用户操作。Chromium;Firefox 117 仅限 Windows。
options.silent boolean 不发声、不振动。Chrome 43、Firefox 132、Safari 16.6。与 vibrate 不兼容。
options.data 任意可结构化克隆的值 在 notificationclick 中以 event.notification.data 读回的载荷。
options.actions array 按钮;仅持久通知。见 actions 条目。
options.navigate string(URL) Safari 18.4:激活时打开该 URL,而不触发 notificationclick。
options.timestamp、options.vibrate、options.dir、options.lang number、array、string、string 显示时间、振动模式(Android 上的 Chromium)、文字方向、BCP 47 语言标签。

持久通知拥有非空的 Service Worker 注册;规范仅凭这一点决定其余一切。它的 notificationclick 与 notificationclose 事件在 ServiceWorkerGlobalScope 上触发,它应当出现在平台的通知中心,并且可以携带 actions。非持久通知在其 Notification 对象上触发 click 与 close,显示几秒后由浏览器关闭,且拒绝 actions。

异常 触发条件
TypeError 注册没有处于 activating 或 activated 状态的 worker。navigator.serviceWorker.ready 只在存在这样的 worker 时兑现,示例等待它正是为此。
TypeError Notification.permission 不是 "granted"。Chromium 的报错文本为 Failed to execute 'showNotification' on 'ServiceWorkerRegistration': No notification permission has been granted for this origin.(service_worker_registration_notifications.cc,chromium.googlesource.com)。
TypeError renotify 为 true 但 tag 为空,或 silent 为 true 同时设置了 vibrate。
DataCloneError DOMException options.data 无法结构化克隆(函数、DOM 节点)。

Promise 被拒绝,不会同步抛出。push 处理函数内被拒绝的 showNotification() 同时违背了 userVisibleOnly 的承诺,WebKit 对此的回应是撤销订阅,所以这些情况要在调用之前处理,而不是放在 catch 里。

桌面端支持完整:Chrome 42、Edge 17、Firefox 44、macOS Ventura 上的 Safari 16。移动端才是形态真正起作用的地方。Chrome for Android(42)与 Samsung Internet 只通过 Service Worker 暴露通知,并让构造函数抛出;iOS 与 iPadOS 上的 Safari 16.4 只在主屏幕 Web App 内暴露该接口,而且只能通过 Service Worker;Android WebView 没有实现(BCD api.Notification)。Chrome 自 49 起还在无痕窗口中禁用通知。因此写成持久路径是可移植的选择,而不是优化。

示例按代码运行位置划分:页面负责显示,Service Worker 负责处理点击。

等待活跃的 worker,然后在注册上调用 showNotification()。data 成员携带点击处理函数需要的内容。

async function notify(title, body, url) {
const registration = await navigator.serviceWorker.ready;
await registration.showNotification(title, {
body,
icon: '/icons/icon-192.png',
tag: 'inbox',
data: { url },
});
}

tag: 'inbox' 把重复调用合并成一条通知,这是「3 条新消息」想要的效果,却不是三个独立聊天线程想要的。

持久通知在 worker 上触发 notificationclick。先关闭通知,然后若目标 URL 已有打开的窗口就聚焦它,否则打开新窗口。

self.addEventListener('notificationclick', (event) => {
event.notification.close();
const url = new URL(event.notification.data?.url ?? '/', self.location.origin).href;
event.waitUntil((async () => {
const windows = await self.clients.matchAll({ type: 'window', includeUncontrolled: true });
const open = windows.find((client) => client.url === url);
if (open) return open.focus();
return self.clients.openWindow(url);
})());
});

点击通知主体时 event.action 为空字符串,点击按钮时为该按钮的 action id;上面的处理函数对两者一视同仁。

三个条件必须同时成立:接口存在、权限已授予、注册有活跃 worker。逐一检查,否则返回页内兜底,而不是等待 ready,后者在没有注册的页面上不会兑现。

async function getNotifier() {
if (!('Notification' in window) || !('serviceWorker' in navigator)) {
return showInPageBanner; // iOS 标签页或应用内嵌网页视图:没有系统通知
}
const registration = await navigator.serviceWorker.getRegistration();
if (!registration?.active || Notification.permission !== 'granted') {
return showInPageBanner; // 尚未安装,或权限未授予
}
return (title, body) => registration.showNotification(title, { body });
}

两个分支返回的函数签名相同,调用方不必关心拿到的是哪一个。

规范

规范状态
无。