Vue3利用Notification API實(shí)現(xiàn)瀏覽器通知功能
Notification API介紹。- 關(guān)閉瀏覽器后,點(diǎn)擊歷史通知仍能打開站點(diǎn)并跳轉(zhuǎn)目標(biāo)頁,如何實(shí)現(xiàn)。
1. 先說結(jié)論
只用 new Notification() 不夠。要覆蓋“舊通知點(diǎn)擊跳轉(zhuǎn)”,必須:
- 發(fā)送階段:優(yōu)先
ServiceWorkerRegistration.showNotification() - 點(diǎn)擊階段:在
public/notification-sw.js監(jiān)聽notificationclick - 跳轉(zhuǎn)策略:先找已有窗口并
focus(),沒有再openWindow()
一句話:把點(diǎn)擊處理從頁面 JS 移到 Service Worker。
2. Notification API 參數(shù)
2.1new Notification(title, options)的核心參數(shù)
title:通知標(biāo)題(必填)body:正文內(nèi)容icon:大圖標(biāo)(建議 192x192 或 256x256)badge:小徽標(biāo)(Android 常見,建議單色清晰圖)tag:通知分組標(biāo)識(shí);相同tag會(huì)覆蓋舊通知data:自定義數(shù)據(jù)載荷(本方案用來傳url)requireInteraction:true表示通知不自動(dòng)關(guān)閉(瀏覽器行為可能有差異)silent:是否靜音(不同瀏覽器支持度不同)
示例(占位鏈接):
new Notification('系統(tǒng)提醒', {
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:頁面存活時(shí)可用notification.onclose:通知關(guān)閉回調(diào)notification.onerror:創(chuàng)建或展示失敗回調(diào)
頁面被關(guān)閉后,onclick 不可靠,所以才需要 SW 的 notificationclick。
2.3 權(quán)限相關(guān) API
Notification.permission:default/granted/deniedNotification.requestPermission():請(qǐng)求授權(quán)(需要用戶手勢(shì)觸發(fā)更穩(wěn))
3. 項(xiàng)目落地實(shí)現(xiàn)(3 步)
3.1 注冊(cè)通知 Service Worker
文件:src/main.ts
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/notification-sw.js').catch((error: unknown) => {
console.warn('[NotificationSW] register failed:', error)
})
})
}
作用:讓瀏覽器知道通知點(diǎn)擊事件由 public/notification-sw.js 接管。
3.2 統(tǒng)一封裝通知發(fā)送(優(yōu)先 SW,失敗降級(jí))
文件:src/composables/useBrowserNotification.ts
項(xiàng)目實(shí)現(xiàn)的關(guān)鍵點(diǎn):
- 權(quán)限不是
granted直接攔截 - 先拿 SW registration,再
showNotification - 通過
data.url傳跳轉(zhuǎn)目標(biāo) - SW 發(fā)送失敗再降級(jí)到
new Notification
核心片段:
const notificationOptions: NotificationOptions = {
body: options.body,
icon: notificationIcon,
badge: notificationBadgeIcon,
requireInteraction: options.requireInteraction ?? false,
tag: options.tag,
data: {
url: options.clickUrl ?? ''
}
}
await registration.showNotification(options.title, notificationOptions)
3.3 在 SW 中處理點(diǎn)擊(關(guān)鍵中的關(guān)鍵)
文件:public/notification-sw.js
self.addEventListener('notificationclick', (event) => {
event.notification.close()
const targetUrl = String(event.notification?.data?.url || '').trim()
if (!targetUrl) return
event.waitUntil(
self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clients) => {
for (const client of clients) {
if (client.url === targetUrl && 'focus' in client) {
return client.focus()
}
}
return self.clients.openWindow(targetUrl)
})
)
})
這段邏輯保證:
- 有現(xiàn)成頁面:聚焦現(xiàn)有頁
- 沒有頁面:新開頁并跳轉(zhuǎn)
- 瀏覽器關(guān)閉后點(diǎn)擊歷史通知:依然可回站
4. 為什么“舊通知點(diǎn)擊可跳轉(zhuǎn)”
點(diǎn)擊系統(tǒng)通知時(shí),事件發(fā)給的是 Service Worker,不依賴頁面是否還活著。
因此即使用戶關(guān)了頁面,只要 SW 生效,仍可完成 focus/openWindow。
5. 注意
- 使用 HTTPS 或
localhost,否則 Notification/SW 都可能不可用 clickUrl建議絕對(duì)地址,避免路由 base 造成解析偏差tag要按業(yè)務(wù)維度設(shè)計(jì)(例如module-item-123),防止通知刷屏- 對(duì)
denied狀態(tài)給出 UI 引導(dǎo),提示去瀏覽器設(shè)置中手動(dòng)開啟 requireInteraction行為在不同瀏覽器有差異,需實(shí)機(jī)驗(yàn)證
useBrowserNotification全量源碼
import { computed, ref, type ComputedRef, type Ref } from 'vue'
import notificationBadgeIcon from '@/assets/images/notify-badge-placeholder.png'
import notificationIcon from '@/assets/images/notify-icon-placeholder.png'
type NotifyPermission = NotificationPermission | 'unsupported'
interface SendBrowserNotificationOptions {
title: string
body: string
clickUrl?: string
tag?: string
requireInteraction?: boolean
autoCloseMs?: number
onClick?: () => void
}
interface UseBrowserNotification {
message: Ref<string>
isSupported: Ref<boolean>
permissionState: Ref<NotifyPermission>
supportText: ComputedRef<string>
permissionLabel: ComputedRef<string>
requestNotifyPermission: () => Promise<void>
sendBrowserNotification: (options: SendBrowserNotificationOptions) => void
}
export const useBrowserNotification = (): UseBrowserNotification => {
const message = ref('等待操作')
const isSupported = ref<boolean>(typeof window !== 'undefined' && 'Notification' in window)
const permissionState = ref<NotifyPermission>(isSupported.value ? Notification.permission : 'unsupported')
const supportText = computed(() => (isSupported.value ? '是' : '否'))
const permissionLabel = computed(() => {
if (permissionState.value === 'unsupported') return '瀏覽器不支持'
if (permissionState.value === 'granted') return '已授權(quán)'
if (permissionState.value === 'denied') return '已拒絕'
return '未授權(quán)(default)'
})
const updatePermissionState = (): void => {
permissionState.value = isSupported.value ? Notification.permission : 'unsupported'
}
const requestNotifyPermission = async (): Promise<void> => {
if (!isSupported.value) {
message.value = '當(dāng)前瀏覽器不支持 Notification API'
return
}
try {
const result = await Notification.requestPermission()
permissionState.value = result
message.value = `權(quán)限申請(qǐng)結(jié)果:${result}`
} catch (error) {
message.value = '申請(qǐng)通知權(quán)限失敗,請(qǐng)稍后重試'
console.error('Notification.requestPermission failed:', error)
}
}
const getServiceWorkerRegistration = async (): Promise<ServiceWorkerRegistration | null> => {
if (typeof window === 'undefined' || !('serviceWorker' in navigator)) return null
try {
return await navigator.serviceWorker.getRegistration()
} catch {
return null
}
}
const sendBrowserNotification = (options: SendBrowserNotificationOptions): void => {
if (!isSupported.value) {
message.value = '當(dāng)前瀏覽器不支持 Notification API'
return
}
updatePermissionState()
if (permissionState.value !== 'granted') {
message.value = '請(qǐng)先授權(quán)通知權(quán)限后再發(fā)送'
return
}
const autoCloseMs = options.autoCloseMs ?? 4000
;(async () => {
const registration = await getServiceWorkerRegistration()
if (registration) {
try {
const notificationOptions: NotificationOptions = {
body: options.body,
icon: notificationIcon,
badge: notificationBadgeIcon,
requireInteraction: options.requireInteraction ?? false,
tag: options.tag,
data: {
url: options.clickUrl ?? ''
}
}
await registration.showNotification(options.title, notificationOptions)
message.value = `通知已發(fā)送:${options.title}`
return
} catch (error) {
console.warn('ServiceWorker showNotification failed, fallback to page notification:', error)
}
}
try {
const notificationOptions: NotificationOptions = {
body: options.body,
icon: notificationIcon,
badge: notificationBadgeIcon,
requireInteraction: options.requireInteraction ?? false
}
// 不傳 tag 時(shí)允許系統(tǒng)通知疊加顯示;傳 tag 時(shí)按 tag 覆蓋同組通知
if (options.tag) {
notificationOptions.tag = options.tag
}
const notice = new Notification(options.title, notificationOptions)
const shouldAutoClose = !(options.requireInteraction ?? false)
const autoCloseTimer = shouldAutoClose
? window.setTimeout(() => {
notice.close()
}, autoCloseMs)
: null
notice.onclick = () => {
window.focus()
notice.close()
if (options.clickUrl) {
window.open(options.clickUrl, '_blank', 'noopener,noreferrer')
}
options.onClick?.()
message.value = '已點(diǎn)擊通知,窗口已嘗試聚焦'
}
notice.onclose = () => {
if (autoCloseTimer !== null) {
window.clearTimeout(autoCloseTimer)
}
}
notice.onerror = () => {
if (autoCloseTimer !== null) {
window.clearTimeout(autoCloseTimer)
}
message.value = '通知發(fā)送失敗,請(qǐng)檢查瀏覽器通知設(shè)置'
}
message.value = `通知已發(fā)送:${options.title}`
} catch (error) {
message.value = '創(chuàng)建通知失敗,請(qǐng)檢查瀏覽器設(shè)置'
console.error('Notification constructor failed:', error)
}
})().catch((error: unknown) => {
message.value = '創(chuàng)建通知失敗,請(qǐng)檢查瀏覽器設(shè)置'
console.error('sendBrowserNotification failed:', error)
})
}
return {
message,
isSupported,
permissionState,
supportText,
permissionLabel,
requestNotifyPermission,
sendBrowserNotification
}
}
public/notification-sw.js全量源碼
self.addEventListener('notificationclick', (event) => {
event.notification.close()
const targetUrl = String(event.notification?.data?.url || '').trim()
if (!targetUrl) return
event.waitUntil(
self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clients) => {
for (const client of clients) {
if (client.url === targetUrl && 'focus' in client) {
return client.focus()
}
}
return self.clients.openWindow(targetUrl)
}),
)
})
以上就是Vue3利用Notification API實(shí)現(xiàn)瀏覽器通知功能的詳細(xì)內(nèi)容,更多關(guān)于Vue3 Notification瀏覽器通知的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue元素實(shí)現(xiàn)動(dòng)畫過渡效果
這篇文章主要介紹了vue元素實(shí)現(xiàn)動(dòng)畫過渡效果,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-07-07
vue輪播圖插件vue-awesome-swiper的使用代碼實(shí)例
本篇文章主要介紹了vue輪播圖插件vue-awesome-swiper的使用代碼實(shí)例,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-07-07
vue ssr+koa2構(gòu)建服務(wù)端渲染的示例代碼
這篇文章主要介紹了vue ssr+koa2構(gòu)建服務(wù)端渲染的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-03-03
vue里如何主動(dòng)銷毀keep-alive緩存的組件
這篇文章主要介紹了vue里如何主動(dòng)銷毀keep-alive緩存的組件,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03
vue.js響應(yīng)式原理解析與實(shí)現(xiàn)
這篇文章主要為大家詳細(xì)介紹了vue.js響應(yīng)式原理解析與實(shí)現(xiàn),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-08-08
Vue中使用Canvas實(shí)現(xiàn)繪制二維碼
這篇文章主要為大家詳細(xì)介紹了如何在Vue中使用Canvas實(shí)現(xiàn)繪制二維碼,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2007-02-02

