大致上的流程
1.前端發起接受通知
2.同意後同意後取得通知的相關資料並保存到後端
3.後端發起通知
4.前端處理收到通知後的事情
先處理後端
因為要先取得key才給以給前端去取得訂閱通知的資料
後端的架構為node.js+Koa
先做一個Class來管理會用到的功能
先安裝web-push套件
import webpush from "web-push";
class WebPushService {
constructor({ publicKey, privateKey, contactEmail }) {
if (!publicKey || !privateKey || !contactEmail) {
throw new Error("Missing VAPID configuration");
}
this.vapidDetails = {
subject: `mailto:${contactEmail}`,
publicKey,
privateKey,
};
webpush.setVapidDetails(
this.vapidDetails.subject,
this.vapidDetails.publicKey,
this.vapidDetails.privateKey
);
}
/**
* 回傳 Public Key,給前端註冊用
*/
getPublicKey() {
return this.vapidDetails.publicKey;
}
/**
* 產生Key
*/
generateVapidKeys() {
const vapidKeys = webpush.generateVAPIDKeys();
return {
publicKey: vapidKeys.publicKey,
privateKey: vapidKeys.privateKey,
};
}
/**
* 發送通知給特定訂閱者
* @param {Object} subscription 前端儲存的 PushSubscription JSON
* @param {Object} payload 推播內容,如 { title, body, icon, data }
*/
async sendNotification(subscription, payload = {}) {
const notificationPayload = JSON.stringify(payload);
try {
await webpush.sendNotification(subscription, notificationPayload);
return { success: true };
} catch (error) {
console.error("Error sending notification:", error);
return { success: false, error };
}
}
}
export default WebPushService;
接著到router的地方準備四個路徑,其實三個也可以,就看要怎麼取得web-push的key
import WebPush from "../services/web-push.js";
const webPush = new WebPush({
publicKey: "xxxxxxx", // 透過 generateVapidKeys 產生出來的
privateKey: "xxxxxxx", // 透過 generateVapidKeys 產生出來的
contactEmail: "xxxxxx@email.com"
});
router.get("/subscription/getPubkey", verifyToken, async (ctx) => {
const pubkey = webPush.getPublicKey();
ctx.status = 200;
ctx.body = new Response({
meta: null,
data: pubkey,
});
});
// 處理使用者訂閱的邏輯
router.post("/subscription/subscribe", verifyToken, async (ctx) => {
const id = ctx.state.userId;
const subscription = ctx.request.body;
// 此處為保存使用者的訂閱資料,因架構程式碼因人而異
const { status, meta, data } = await service
.setDriver(ctx.nc)
.call("admins", "update", { id, subscription });
ctx.status = status;
ctx.body = new Response({
meta,
data,
});
});
// generate vapid keys
router.get("/generate-vapid-keys", async (ctx) => {
const { publicKey, privateKey } = webPush.generateVapidKeys();
ctx.status = 200;
ctx.body = new Response({
meta: {},
data: {
publicKey,
privateKey,
},
});
});
// 測試發送notification
router.get("/send-notification", async (ctx) => {
const { memberId } = ctx.query;
ctx.assert(memberId, 400, "subscription is required");
const payload = {
title: "測試通知",
body: "這是一個測試通知",
};
// 此處為取得使用者的訂閱資料,因架構程式碼因人而異
const { data:member } = await service
.setDriver(ctx.nc)
.call("admins", "get", { id: memberId });
if (!member) {
ctx.status = 400;
ctx.body = new Response({
meta: {},
data: "Member not found",
});
return;
}
const res = await webPush.sendNotification(
member.subscription,
payload
);
ctx.status = 200;
ctx.body = new Response({
meta: null,
data: res,
});
});
程式碼好了之後就去執行 /generate-vapid-keys 這個路由,把產生出來的code放到該放的地方
不過我記得好像在router引入webPush初始化的時候就要有key,如果沒有會報錯,因此我有準備另一個檔案
generate-vapid.js
import webpush from "web-push";
const keys = webpush.generateVAPIDKeys();
console.log(keys);
然後執行他node generate-vapid.js
之後取得key再貼到程式碼裡面,我在本地開發的時候是這樣做,到了線上因為環境原因沒有node可以用,才會再寫一隻api去取得key
到這邊後端的部分就處理好了
接著處理前端的部分
我的前端是使用vite作為打包工具的vue spa網站
本來vite有提供一個套件是vite-plugin-pwa
但是我在使用的過程遇到億堆問題...
打包一直不過,索性就不用了
首先準備一個icon把他放到public的資料夾,接著一樣在public資料夾建立一個manifest.json
{
"name": "xxxx",
"short_name": "xxxx",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#000000",
"icons": [
{
"src": "/favicon-192.png",
"sizes": "192x192",
"type": "image/png"
}
]
}
裡面的東西就不深究了,我只放了幾個看起來似乎是基礎的東西,需要深究的話就看官方介紹
然後在public目錄創建sw.js
// public/sw.js
self.addEventListener("install", (event) => {
console.log("[ServiceWorker] Installed")
// 可加快取邏輯
})
self.addEventListener("activate", (event) => {
console.log("[ServiceWorker] Activated")
})
self.addEventListener("fetch", (event) => {
// 可選擇攔截 fetch,實作離線模式
})
self.addEventListener("push", (event) => {
console.log("[ServiceWorker] Push Received")
let data = {}
try {
data = event.data?.json() || {}
console.log("🔔 通知資料:", data)
} catch (err) {
console.error("❌ JSON 解析錯誤", err)
}
const title = data.title || "無標題"
const body = data.body || "您有一則通知"
event.waitUntil(
(async () => {
await self.registration.showNotification(title, {
body,
icon: "/favicon-192.png"
})
})()
)
console.log("done")
})
接著在你的根頁面(可以是App.vue也可以是你的layout裡面的index.vue)貼上代碼
async function subscribeUser() {
if ("serviceWorker" in navigator && "PushManager" in window) {
try {
// 1. 註冊 service worker
// register裡的/sw.js路徑會是public裡面的路徑,因為打包過後根目錄就是public
// 因此這邊的sw.js就是指定到public路徑裡面的sw.js
const registration = await navigator.serviceWorker.register("/sw.js")
// 2. 等待使用者同意通知
const permission = await Notification.requestPermission()
if (permission !== "granted") {
console.log("通知權限被拒絕")
return
}
// 3. 從後端取得 VAPID public key
const res = await getPubkey() // 這個function是自定義的
console.log("取得public key ===> ", res)
const { data: publicKey } = res
// 4. 使用 PushManager 訂閱
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(publicKey)
})
// // 5. 將 subscription 傳給後端儲存
const res2 = await subscribeNotification(subscription) // 這個function是自定義的
console.log("✅ 已成功訂閱推播!", res2)
ElMessage.success("訂閱成功!")
} catch (err) {
console.error("❌ 訂閱失敗:", err)
}
} else {
console.warn("當前瀏覽器不支援 Web Push")
}
}
// helper: 將 base64 轉為 Uint8Array
function urlBase64ToUint8Array(base64String) {
const padding = "=".repeat((4 - (base64String.length % 4)) % 4)
const base64 = (base64String + padding).replace(/\-/g, "+").replace(/_/g, "/")
const rawData = atob(base64)
return Uint8Array.from([...rawData].map((char) => char.charCodeAt(0)))
}
// 判斷瀏覽器是否支援,如果不處理的話網頁會報錯,頁面會跑不出來
if ("Notification" in window) {
console.log("userStore.subscription ===> ", userStore.subscription)
console.log("通知狀態", Notification.permission)
if (userStore && !userStore.subscription) {
subscribeUser()
}
} else {
console.log("當前瀏覽器不支援 Web Push")
}
到了這邊程式碼的部分就算完成了
說明幾個需要了解的幾個點
1.android和pc(windows、mac)的部分,都可以在瀏覽器下運作web-push功能,但是在iphone時一定要用safari的瀏覽器且要把網站加到主畫面後開啟的網頁才支援web-push的功能
加到主畫面的方法是連到網頁後點下面的分享按鈕,往下滑就會看到加到主畫面的按鈕了
2.在電腦上可以打開開發者工具(在網頁右鍵->檢查),選擇Application -> Service workers就可以看到是不是有連結成功

3.在Application -> Service workers 裡面有一個地方可以測試,點擊push就可以發送一個假通知,這個能知道你的前端是否有處理好接收到通知的邏輯,記得push的資料要是json的格式
4.我在上述的程式碼完成後一開始其實是不成功的,但不知道為啥過了一陣子就可以用了,或許是網頁需要一點時間做心理準備?
5.push的data裡面不僅限於title和body,還有其他東西,但是還沒研究所以就先不細寫,先暫時放在這邊以後再研究
var notification = {
'title': 'web push標題',
'body': 'web push內文',
'badge': 'logo圖檔路徑',
'icon': 'logo圖檔路徑',
'click_action': 'https://www.domain.com.tw',
'data': {
'url': 'https://www.domain.com.tw'
}
};