Notification API介绍。- 关闭浏览器后,点击历史通知仍能打开站点并跳转目标页,如何实现。
1. 先说结论
只用new Notification()不够。要覆盖“旧通知点击跳转”,必须:
- 发送阶段:优先
ServiceWorkerRegistration.showNotification() - 点击阶段:在
public/notification-sw.js监听notificationclick - 跳转策略:先找已有窗口并
focus(),没有再openWindow()
一句话:把点击处理从页面 JS 移到 Service Worker。
2. Notification API 参数
2.1new Notification(title, options)的核心参数
title:通知标题(必填)body:正文内容icon:大图标(建议 192x192 或 256x256)badge:小徽标(Android 常见,建议单色清晰图)tag:通知分组标识;相同tag会覆盖旧通知data:自定义数据载荷(本方案用来传url)requireInteraction:true表示通知不自动关闭(浏览器行为可能有差异)silent:是否静音(不同浏览器支持度不同)
示例(占位链接):
newNotification('系统提醒',{body:'您有一条待处理消息',icon:'https://example.com/assets/notify-icon.png',badge:'https://example.com/assets/notify-badge.png',tag:'todo-1001',requireInteraction:true,data:{url:'https://example.com/app/todo?id=1001'}})2.2 常用事件
notification.onclick:页面存活时可用notification.onclose:通知关闭回调notification.onerror:创建或展示失败回调
页面被关闭后,
onclick不可靠,所以才需要 SW 的notificationclick。
2.3 权限相关 API
Notification.permission:default/granted/deniedNotification.requestPermission():请求授权(需要用户手势触发更稳)
3. 项目落地实现(3 步)
3.1 注册通知 Service Worker
文件:src/main.ts
if('serviceWorker'innavigator){window.addEventListener('load',()=>{navigator.serviceWorker.register('/notification-sw.js').catch((error:unknown)=>{console.warn('[NotificationSW] register failed:',error)})})}作用:让浏览器知道通知点击事件由public/notification-sw.js接管。
3.2 统一封装通知发送(优先 SW,失败降级)
文件:src/composables/useBrowserNotification.ts
项目实现的关键点:
- 权限不是
granted直接拦截 - 先拿 SW registration,再
showNotification - 通过
data.url传跳转目标 - SW 发送失败再降级到
new Notification
核心片段:
constnotificationOptions:NotificationOptions={body:options.body,icon:notificationIcon,badge:notificationBadgeIcon,requireInteraction:options.requireInteraction??false,tag:options.tag,data:{url:options.clickUrl??''}}awaitregistration.showNotification(options.title,notificationOptions)3.3 在 SW 中处理点击(关键中的关键)
文件:public/notification-sw.js
self.addEventListener('notificationclick',(event)=>{event.notification.close()consttargetUrl=String(event.notification?.data?.url||'').trim()if(!targetUrl)returnevent.waitUntil(self.clients.matchAll({type:'window',includeUncontrolled:true}).then((clients)=>{for(constclientofclients){if(client.url===targetUrl&&'focus'inclient){returnclient.focus()}}returnself.clients.openWindow(targetUrl)}))})这段逻辑保证:
- 有现成页面:聚焦现有页
- 没有页面:新开页并跳转
- 浏览器关闭后点击历史通知:依然可回站
4. 为什么“旧通知点击可跳转”
点击系统通知时,事件发给的是 Service Worker,不依赖页面是否还活着。
因此即使用户关了页面,只要 SW 生效,仍可完成focus/openWindow。
5. 注意
- 使用 HTTPS 或
localhost,否则 Notification/SW 都可能不可用 clickUrl建议绝对地址,避免路由 base 造成解析偏差tag要按业务维度设计(例如module-item-123),防止通知刷屏- 对
denied状态给出 UI 引导,提示去浏览器设置中手动开启 requireInteraction行为在不同浏览器有差异,需实机验证
useBrowserNotification全量源码
import{computed,ref,typeComputedRef,typeRef}from'vue'importnotificationBadgeIconfrom'@/assets/images/notify-badge-placeholder.png'importnotificationIconfrom'@/assets/images/notify-icon-placeholder.png'typeNotifyPermission=NotificationPermission|'unsupported'interfaceSendBrowserNotificationOptions{title:stringbody:stringclickUrl?:stringtag?:stringrequireInteraction?:booleanautoCloseMs?:numberonClick?:()=>void}interfaceUseBrowserNotification{message:Ref<string>isSupported:Ref<boolean>permissionState:Ref<NotifyPermission>supportText:ComputedRef<string>permissionLabel:ComputedRef<string>requestNotifyPermission:()=>Promise<void>sendBrowserNotification:(options:SendBrowserNotificationOptions)=>void}exportconstuseBrowserNotification=():UseBrowserNotification=>{constmessage=ref('等待操作')constisSupported=ref<boolean>(typeofwindow!=='undefined'&&'Notification'inwindow)constpermissionState=ref<NotifyPermission>(isSupported.value?Notification.permission:'unsupported')constsupportText=computed(()=>(isSupported.value?'是':'否'))constpermissionLabel=computed(()=>{if(permissionState.value==='unsupported')return'浏览器不支持'if(permissionState.value==='granted')return'已授权'if(permissionState.value==='denied')return'已拒绝'return'未授权(default)'})constupdatePermissionState=():void=>{permissionState.value=isSupported.value?Notification.permission:'unsupported'}constrequestNotifyPermission=async():Promise<void>=>{if(!isSupported.value){message.value='当前浏览器不支持 Notification API'return}try{constresult=awaitNotification.requestPermission()permissionState.value=result message.value=`权限申请结果:${result}`}catch(error){message.value='申请通知权限失败,请稍后重试'console.error('Notification.requestPermission failed:',error)}}constgetServiceWorkerRegistration=async():Promise<ServiceWorkerRegistration|null>=>{if(typeofwindow==='undefined'||!('serviceWorker'innavigator))returnnulltry{returnawaitnavigator.serviceWorker.getRegistration()}catch{returnnull}}constsendBrowserNotification=(options:SendBrowserNotificationOptions):void=>{if(!isSupported.value){message.value='当前浏览器不支持 Notification API'return}updatePermissionState()if(permissionState.value!=='granted'){message.value='请先授权通知权限后再发送'return}constautoCloseMs=options.autoCloseMs??4000;(async()=>{constregistration=awaitgetServiceWorkerRegistration()if(registration){try{constnotificationOptions:NotificationOptions={body:options.body,icon:notificationIcon,badge:notificationBadgeIcon,requireInteraction:options.requireInteraction??false,tag:options.tag,data:{url:options.clickUrl??''}}awaitregistration.showNotification(options.title,notificationOptions)message.value=`通知已发送:${options.title}`return}catch(error){console.warn('ServiceWorker showNotification failed, fallback to page notification:',error)}}try{constnotificationOptions:NotificationOptions={body:options.body,icon:notificationIcon,badge:notificationBadgeIcon,requireInteraction:options.requireInteraction??false}// 不传 tag 时允许系统通知叠加显示;传 tag 时按 tag 覆盖同组通知if(options.tag){notificationOptions.tag=options.tag}constnotice=newNotification(options.title,notificationOptions)constshouldAutoClose=!(options.requireInteraction??false)constautoCloseTimer=shouldAutoClose?window.setTimeout(()=>{notice.close()},autoCloseMs):nullnotice.onclick=()=>{window.focus()notice.close()if(options.clickUrl){window.open(options.clickUrl,'_blank','noopener,noreferrer')}options.onClick?.()message.value='已点击通知,窗口已尝试聚焦'}notice.onclose=()=>{if(autoCloseTimer!==null){window.clearTimeout(autoCloseTimer)}}notice.onerror=()=>{if(autoCloseTimer!==null){window.clearTimeout(autoCloseTimer)}message.value='通知发送失败,请检查浏览器通知设置'}message.value=`通知已发送:${options.title}`}catch(error){message.value='创建通知失败,请检查浏览器设置'console.error('Notification constructor failed:',error)}})().catch((error:unknown)=>{message.value='创建通知失败,请检查浏览器设置'console.error('sendBrowserNotification failed:',error)})}return{message,isSupported,permissionState,supportText,permissionLabel,requestNotifyPermission,sendBrowserNotification}}public/notification-sw.js全量源码
self.addEventListener('notificationclick',(event)=>{event.notification.close()consttargetUrl=String(event.notification?.data?.url||'').trim()if(!targetUrl)returnevent.waitUntil(self.clients.matchAll({type:'window',includeUncontrolled:true}).then((clients)=>{for(constclientofclients){if(client.url===targetUrl&&'focus'inclient){returnclient.focus()}}returnself.clients.openWindow(targetUrl)}),)})