通知推送 (Notifications API)
在現代 Web 應用中,即時通知是提升使用者黏著度的關鍵功能。無論是新訊息提醒、任務完成通知,還是行事曆活動,Web Notifications API 讓我們能夠跳脫瀏覽器視窗,發送系統層級的通知訊息。
本文將帶你深入了解如何使用原生的 Notification API,並將其封裝成一個優雅的 Vue 3 Composable —— useNotification。
一、 Web Notifications API 簡介
Notification API 允許網頁向使用者顯示系統通知。這些通知通常會出現在作業系統的通知中心,即使使用者當前不在該分頁(甚至最小化瀏覽器),也能收到訊息。
1.1 核心流程
使用通知 API 主要分為三個步驟:
- 檢查支援度:確認瀏覽器是否支援。
- 請求權限:必須獲得使用者明確授權 (
granted)。 - 發送通知:建立
Notification實例。
1.2 權限狀態 (Notification.permission)
default:使用者尚未做出選擇(瀏覽器行為通常等同於拒絕,但可請求權限)。granted:使用者已允許通知。denied:使用者已封鎖通知。
二、 設計 Composable 介面
我們需要一個能夠處理權限請求、狀態管理與通知發送的介面:
typescript
// types/useNotification.ts
import type { Ref, ComputedRef } from "vue";
export interface NotificationOptions {
/** 通知內文 */
body?: string;
/** 通知圖示 URL */
icon?: string;
/** 通知圖片 URL (部分系統支援) */
image?: string;
/** 分組標籤 (相同 tag 的通知會被覆蓋而非堆疊) */
tag?: string;
/** 是否重新通知 (當覆蓋舊通知時是否再次震動/發聲) */
renotify?: boolean;
/** 點擊通知時的回呼函式 */
onClick?: (e: Event) => void;
}
export interface UseNotificationReturn {
/** 是否支援 Notification API */
isSupported: ComputedRef<boolean>;
/** 當前權限狀態 */
permission: Ref<NotificationPermission>;
/** 請求權限 */
requestPermission: () => Promise<NotificationPermission>;
/** 發送通知 */
show: (title: string, options?: NotificationOptions) => Notification | null;
/** 關閉所有此頁面產生的通知 (選擇性實作) */
closeAll: () => void;
}三、 實作 useNotification Composable
3.1 核心實作
typescript
// composables/useNotification.ts
import { ref, computed, onMounted, onUnmounted } from "vue";
import type {
UseNotificationReturn,
NotificationOptions,
} from "@/types/useNotification";
export function useNotification(): UseNotificationReturn {
const isSupported = computed(
() => typeof window !== "undefined" && "Notification" in window,
);
const permission = ref<NotificationPermission>("default");
// 追蹤活躍的通知實例,以便組件卸載時清理
const activeNotifications: Notification[] = [];
// 更新權限狀態
function updatePermission() {
if (isSupported.value) {
permission.value = Notification.permission;
}
}
// 請求權限
async function requestPermission() {
if (!isSupported.value) return "denied";
if (permission.value === "default") {
const status = await Notification.requestPermission();
permission.value = status;
return status;
}
return permission.value;
}
// 發送通知
function show(
title: string,
options: NotificationOptions = {},
): Notification | null {
if (!isSupported.value) return null;
// 如果尚未授權,先嘗試請求
if (permission.value === "default") {
requestPermission().then((p) => {
if (p === "granted") show(title, options);
});
return null;
}
if (permission.value !== "granted") return null;
// 建立通知
const notification = new Notification(title, {
body: options.body,
icon: options.icon,
image: options.image,
tag: options.tag,
renotify: options.renotify,
});
// 綁定點擊事件
if (options.onClick) {
notification.onclick = options.onClick;
}
// 加入管理列表
activeNotifications.push(notification);
// 監聽關閉事件以從列表中移除
notification.onclose = () => {
const index = activeNotifications.indexOf(notification);
if (index > -1) activeNotifications.splice(index, 1);
};
return notification;
}
function closeAll() {
activeNotifications.forEach((n) => n.close());
activeNotifications.length = 0;
}
onMounted(() => {
updatePermission();
});
// 組件卸載時自動關閉通知(可選,視需求而定)
onUnmounted(() => {
closeAll();
});
return {
isSupported,
permission,
requestPermission,
show,
closeAll,
};
}NOTE
HTTPS 要求:現代瀏覽器(尤其是 Chrome)要求網站必須在安全環境(HTTPS)下才能請求通知權限。Localhost 是例外。
四、 實際應用範例
4.1 基本通知與權限按鈕
最基本的用法:一個按鈕請求權限,另一個按鈕發送通知。
vue
<script setup lang="ts">
import { useNotification } from "@/composables/useNotification";
const { isSupported, permission, requestPermission, show } = useNotification();
function sendTestNotification() {
show("哈囉!Vue 3", {
body: "這是一則來自 Composable 的測試通知 🚀",
icon: "/logo.png",
});
}
</script>
<template>
<div v-if="isSupported">
<p>
權限狀態: <strong>{{ permission }}</strong>
</p>
<button v-if="permission === 'default'" @click="requestPermission">
🔔 啟用通知
</button>
<button v-else-if="permission === 'granted'" @click="sendTestNotification">
📨 發送測試訊息
</button>
<div v-else class="error">❌ 通知權限被拒絕,請至瀏覽器設定開啟。</div>
</div>
<div v-else>您的瀏覽器不支援通知功能。</div>
</template>4.2 倒數計時器結束通知
結合計時器,在倒數結束時發送通知,這是非常實用的場景(例如番茄鐘)。
vue
<script setup lang="ts">
import { ref } from "vue";
import { useNotification } from "@/composables/useNotification";
const { show, permission, requestPermission } = useNotification();
const timeLeft = ref(5);
function startTimer() {
// 確保先取得權限
if (permission.value === "default") requestPermission();
const timer = setInterval(() => {
timeLeft.value--;
if (timeLeft.value <= 0) {
clearInterval(timer);
// 發送通知
show("時間到!", {
body: "您的番茄鐘專注時間已結束,起來動一動吧!",
icon: "/timer.png",
tag: "pomodoro-timer", // 確保不會重複堆疊
renotify: true,
});
timeLeft.value = 5; // 重置
}
}, 1000);
}
</script>五、 行動版與 PWA 的注意事項
Web Notifications API 在桌機版瀏覽器(Chrome, Firefox, Safari, Edge)支援度很好。但在行動裝置上,情況較為複雜:
- Android (Chrome): 完整支援。
- iOS (Safari): 傳統網頁(在 Safari 中開啟)不支援標準 Notification API。
- 解決方案:從 iOS 16.4 開始,如果使用者將網站加入主畫面 (Add to Home Screen) 成為 PWA,則支援 Web Push 與 Notifications API。
- 實作差異:iOS PWA 的通知通常需要配合 Service Worker 與 Push API,單純的前端
new Notification()可能無法運作。
WARNING
如果你的目標受眾包含大量 iOS 使用者,請務必了解 PWA 與 Push API 的相關知識,單純依賴本篇介紹的 Notification API 在 iOS Safari 上是無效的。
總結
| 概念 | 說明 |
|---|---|
| Notification | 用於建立與顯示通知的建構函式 |
| requestPermission | 非同步方法,回傳 granted, denied 或 default |
| tag | 用於分組/取代舊通知,避免通知中心被洗版 |
| Service Worker | 背景推送通知(Push API)的基礎,比單純 Notification 更強大 |
TIP
最佳體驗設計:不要在使用者一進入網站就跳出權限請求(這很煩人且容易被拒絕)。應該設計一個「開啟通知」的文字或按鈕,當使用者點擊並了解用途後,再呼叫 requestPermission。
進階挑戰
- 點擊互動:實作點擊通知後 (
onClick),自動將瀏覽器視窗聚焦 (window.focus()) 並跳轉到特定頁面。 - Service Worker 整合:研究如何使用 Service Worker 發送離線推播通知 (Push Notifications)。
- 自訂音效:雖然 Notification API 不直接支援自訂音效,但可以在顯示通知時同時使用
AudioAPI 播放提示音。