Vue3加入PWA 製作WebPush

更新於 發佈於 閱讀時間約 18 分鐘

大致上的流程

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就可以看到是不是有連結成功

raw-image


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'

}

};





留言
avatar-img
留言分享你的想法!
avatar-img
開發筆記
0會員
1內容數
主要是我在工作上自己遇到的問題及開發的過程,寫起來自己紀錄來看順便給有需要的人
你可能也想看
Thumbnail
「欸!這是在哪裡買的?求連結 🥺」 誰叫你太有品味,一發就讓大家跟著剁手手? 讓你回購再回購的生活好物,是時候該介紹出場了吧! 「開箱你的美好生活」現正召喚各路好物的開箱使者 🤩
Thumbnail
「欸!這是在哪裡買的?求連結 🥺」 誰叫你太有品味,一發就讓大家跟著剁手手? 讓你回購再回購的生活好物,是時候該介紹出場了吧! 「開箱你的美好生活」現正召喚各路好物的開箱使者 🤩
Thumbnail
介紹朋友新開的蝦皮選物店『10樓2選物店』,並分享方格子與蝦皮合作的分潤計畫,註冊流程簡單,0成本、無綁約,推薦給想增加收入的讀者。
Thumbnail
介紹朋友新開的蝦皮選物店『10樓2選物店』,並分享方格子與蝦皮合作的分潤計畫,註冊流程簡單,0成本、無綁約,推薦給想增加收入的讀者。
Thumbnail
當你邊吃粽子邊看龍舟競賽直播的時候,可能會順道悼念一下2300多年前投江的屈原。但你知道端午節及其活動原先都與屈原毫無關係嗎?這是怎麼回事呢? 本文深入探討端午節設立初衷、粽子、龍舟競渡與屈原自沉四者。看完這篇文章,你就會對端午、粽子、龍舟和屈原的四角關係有新的認識喔。那就讓我們一起解開謎團吧!
Thumbnail
當你邊吃粽子邊看龍舟競賽直播的時候,可能會順道悼念一下2300多年前投江的屈原。但你知道端午節及其活動原先都與屈原毫無關係嗎?這是怎麼回事呢? 本文深入探討端午節設立初衷、粽子、龍舟競渡與屈原自沉四者。看完這篇文章,你就會對端午、粽子、龍舟和屈原的四角關係有新的認識喔。那就讓我們一起解開謎團吧!
Thumbnail
先前提到 Quasar 的 Dialog Plugin 很好用,再讓我補充一個用法。
Thumbnail
先前提到 Quasar 的 Dialog Plugin 很好用,再讓我補充一個用法。
Thumbnail
各位使用 Vue.js 開發的小夥伴們,你們都怎麼實作父子層組件資料的雙向綁定呢?如果你還在寫 prop + emit 的話,不妨進來看看吧。
Thumbnail
各位使用 Vue.js 開發的小夥伴們,你們都怎麼實作父子層組件資料的雙向綁定呢?如果你還在寫 prop + emit 的話,不妨進來看看吧。
Thumbnail
一開始你先把你的專案push上去後,修改vite.config.ts ,要在裡面新增  base: "/Cart/" (/放自己的專案名稱/) build: {outDir: "docs"}, 接下來你要去你的github setting 裡面 -> Page ->選Deploy fro
Thumbnail
一開始你先把你的專案push上去後,修改vite.config.ts ,要在裡面新增  base: "/Cart/" (/放自己的專案名稱/) build: {outDir: "docs"}, 接下來你要去你的github setting 裡面 -> Page ->選Deploy fro
Thumbnail
平常我們在 html 上常看到的例如 v-for、v-model 等等... 也是VUE已經幫我們定義好的指令,而這次我們可以依這自己的需求來建立。 此功能屬於較進階的功能,因此實戰中會比較少見,市面上還是有不少完善的套件能達到同樣效果,建議可以先往這方面察找
Thumbnail
平常我們在 html 上常看到的例如 v-for、v-model 等等... 也是VUE已經幫我們定義好的指令,而這次我們可以依這自己的需求來建立。 此功能屬於較進階的功能,因此實戰中會比較少見,市面上還是有不少完善的套件能達到同樣效果,建議可以先往這方面察找
Thumbnail
VUE為單向資料流的框架,在鄰近層級之間我們可以依靠 props 由父層向子層來傳遞需要的資料,然而遇到跨層級的架構時,雖然也是可以一層層傳進去,只是這會造成多餘的處理及凌亂的程式碼,因此才有了 "provide" 來解決我們跨層級的需求。 層級展示圖
Thumbnail
VUE為單向資料流的框架,在鄰近層級之間我們可以依靠 props 由父層向子層來傳遞需要的資料,然而遇到跨層級的架構時,雖然也是可以一層層傳進去,只是這會造成多餘的處理及凌亂的程式碼,因此才有了 "provide" 來解決我們跨層級的需求。 層級展示圖
Thumbnail
自訂元件生成位置顧名思義就是可以指定部分HTML區塊渲染在特定的畫面上,即使在不同組件也能把A組件內的部分畫面,展現在B組件上,以下方程式舉例。
Thumbnail
自訂元件生成位置顧名思義就是可以指定部分HTML區塊渲染在特定的畫面上,即使在不同組件也能把A組件內的部分畫面,展現在B組件上,以下方程式舉例。
Thumbnail
Vue.js是一種基於MVVM的前端JavaScript框架,類似的框架有React、Angular等。 架設環境 安裝Visual Studio Code(https://code.visualstudio.com/) 安裝Node.js(https://nodejs.org/en/
Thumbnail
Vue.js是一種基於MVVM的前端JavaScript框架,類似的框架有React、Angular等。 架設環境 安裝Visual Studio Code(https://code.visualstudio.com/) 安裝Node.js(https://nodejs.org/en/
Thumbnail
2023 Vue直播班筆記 - 動態路由Props,接續之前的一般動態路由。分為 "寫死" 及 "彈性" 兩種。
Thumbnail
2023 Vue直播班筆記 - 動態路由Props,接續之前的一般動態路由。分為 "寫死" 及 "彈性" 兩種。
Thumbnail
Vue Router 及 具名視圖,擺脫以往切換依賴 CSS display:none 跟 display:block 互相配合,有時還得搭配 z-index 來調整層級跟 opacity 透明度的麻煩,而 Vue Router 完美的解決了這棘手的問題,且能客製頁面想要呈現的擺飾。
Thumbnail
Vue Router 及 具名視圖,擺脫以往切換依賴 CSS display:none 跟 display:block 互相配合,有時還得搭配 z-index 來調整層級跟 opacity 透明度的麻煩,而 Vue Router 完美的解決了這棘手的問題,且能客製頁面想要呈現的擺飾。
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News