Vue3 使用 Notification 浏览器通知,解决页面关闭后旧通知点击无法跳转问题
2026/4/24 23:22:20 网站建设 项目流程
  1. Notification API介绍。
  2. 关闭浏览器后,点击历史通知仍能打开站点并跳转目标页,如何实现。

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
  • requireInteractiontrue表示通知不自动关闭(浏览器行为可能有差异)
  • 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.permissiondefault/granted/denied
  • Notification.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)}),)})

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询