在製作 Prototype 產品時,例如電商網站的促銷活動、新聞網站的即時通知等,推播是一項必要且方便的技術,以往只能使用 email 作為推播的手段,現在有了 Web Push 功能,推播就更方便了。
Web Push 支援程度
Web Push 技術已行之有年,但各家瀏覽器支援的時間卻不一定,Safari on iOS 在 2018
開始支援後,推播技術也算是達到了真正的跨平台技術。以下是最早支援 Web Push
的瀏覽器版本。
Minimal version | Supported year | |
---|---|---|
Safari on iOS | 11.3 | iPhone XR (2018) |
Chrome Android | 40 | 2012 |
Firefox Android | 44 | 2011 |
Chrome / Edge | 40 / 17 | 2015 |
Firefox | 44 | 2016 |
雖然知道現在多數的瀏覽器都支援,在實作上,仍然需要先進行檢查,因為很難保證不會有使用者仍然使用 IE 瀏覽器。
先以建立 Express.js 服務作為開始吧!這裡要謝謝 Whien 寫的這篇「建立 Service Worker Web Push Notification — (Web Notification 實作紀錄)」 文章,本篇文章的程式碼大多參考自該篇文章。檔案目錄結構如下所示:
├── server
│ └── server.js
└── js
├── main.js
└── sw.js
server.js 的程式碼如下:
const path = require('path'); const express = require('express'); const app = express(); app.use('/js', express.static(path.resolve(__dirname, '../js'))); app.get('/', (req, res) => { res.send(` <html> <head></head> <body> <h1>Push Notification</h1> <script src='js/main.js'></script> </body> </html> `); });app.listen(8998);
檢查 Web Push 機能
在實作 Web Push 前,要先確認瀏覽器是否支援這項技術。瀏覽器的 Web Push 標準是以 "PushManager" 和 "ServiceWorker" 兩個 API 架構而成。
- PushManager:瀏覽器提供的推播管理 API,負責處理推播訊息的訂閱和接收
- ServiceWorker:瀏覽器提供的 Service Worker API,允許在背景執行 JavaScript 程式碼,即使網頁關閉也能接收推播訊息
ServiceWorker 具體實作內容交由開發者自行決定。由於 serviceWorker 比 PushManager 還要晚推出,所以要各別檢查 (又或是檢查 serviceWorker 即可)。
使用以下程式碼作為 main.js 程式碼的起始:
if ('serviceWorker' in navigator && 'PushManager' in window) { console.log('Service Worker and Push is supported') } else { console.log('Push is not supported') }
註冊 ServiceWorker
Service Worker 是在背景執行的 JavaScript 檔案,負責接收和處理推播訊息。必須先向瀏覽器註冊 Service Worker 才能使用 Web Push。以下程式碼示範如何註冊 Service Worker:
if ('serviceWorker' in navigator && 'PushManager' in window) { //... navigator.serviceWorker .register('/js/sw.js') .then(swReg => { console.log('Service Worker is registered', swReg); }) .catch(err => console.log('Service Worker register error.', err)); //...
-
navigator.serviceWorker.register('/js/sw.js')
會向瀏覽器註冊位於 /js/sw.js 的 Service Worker 檔案。 -
.then(swReg => { ... })
會在 Service Worker 註冊成功後執行,swReg
參數包含 Service Worker 的註冊資訊。 -
.catch(err => console.log('Service Worker register error.', err))
會在 Service Worker 註冊失敗時執行,err
參數包含錯誤資訊。
建立 Web Push 金鑰
Web Push 規範中對資料的加密有嚴格的要求,這部份可以透過 node.js 裡的 web-push
函式庫產生 VAPID 金鑰:
npx web-push generate-vapid-keys
=======================================
Public Key:
BNuUKzbcqU288QpgHA4PMSAfw8I________________XYLHBZCuPH27DMlLJMhXTlr84fpHtKs3g
Private Key:
ywMS_d68vai________________lrNSOz3IjxU22ks-IFhGc
=======================================
接著把 Public Key 在 main.js 最上層放入變數:
const applicationServerPublicKey = "BNuUKzbcqU288QpgHA4PMSAfw8I________________XYLHBZCuPH27DMlLJMhXTlr84fpHtKs3g"
檢查使用者是否已訂閱
接下來在訂閱推播訊息之前,我們需要先檢查使用者是否已經訂閱,避免重複訂閱。程式碼變化後如下所示:
navigator.serviceWorker
.register('/js/sw.js')
.then(swReg => {
console.log('Service Worker is registered', swReg);
let swRegistration = swReg;
return Promise.resolve(swRegistration)
})
.then(swRegistration => {
inititalUI(swRegistration)
})
return Promise.resolve(swRegistration)
將
swRegistration
包裝成一個 Promise 物件,並返回。 這樣可以將
Service Worker 註冊的結果傳遞給後續的
.then()
方法,以便進行訂閱等操作。如此使用 Promise
可以把註冊、訂閱程式碼邏輯拆開,方便日後程式碼的維護。
initialUI 可以實作顯示訂閱的按鈕,範例程式碼如下所示:
function inititalUI(swRegistration) {
const pushButton = document.createElement('button');
pushButton.textContent = '啟用推播';
document.body.appendChild(pushButton);
swRegistration.pushManager.getSubscription().then(subscription => {
updatePushButton(subscription, pushButton);
pushButton.addEventListener('click', () => {
if (subscription) {
unsubscribeUser(subscription, pushButton, swRegistration);
} else {
// 檢查通知權限狀態
if (Notification.permission === 'denied') {
// 如果已封鎖,引導使用者前往瀏覽器設定頁面
alert('您已封鎖通知權限,請前往瀏覽器設定頁面開啟通知。');
// (Optional) 您可以根據瀏覽器類型,提供更具體的引導方式
// window.open('chrome://settings/content/notifications'); // Chrome
// window.open('about:preferences#privacy'); // Firefox
} else {
subscribeUser(swRegistration, pushButton);
}
}
});
});
}
function updatePushButton(subscription, pushButton) {
if (subscription) {
console.log('使用者已訂閱.');
pushButton.textContent = '取消推播';
} else {
console.log('使用者未訂閱.');
pushButton.textContent = '啟用推播';
}
}
- initialUI 所建立的「啟用推播」按鈕按下後,實際上只會詢問使用者一次 (瀏覽器預設為「詢問」), 按下允許 / 封鎖後,就不會再從瀏覽器跳出,要使用者自行進瀏覽器變更設定。
-
swRegistration.pushManager.getSubscription()
方法會返回一個 Promise,其中包含使用者的訂閱資訊。如果使用者尚未訂閱,則 Promise 的值為 null。
啟用推播和取消推播的程式碼如下:
function urlB64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/\-/g, '+')
.replace(/_/g, '/');const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
function subscribeUser(swRegistration) {
const applicationServerKey = urlB64ToUint8Array(applicationServerPublicKey);
swRegistration.pushManager
.subscribe({
userVisibleOnly: true,
applicationServerKey
})
.then(subscription => {
console.log('User is subscribe.');
updatePushButton(subscription, pushButton);
})
.catch(err => console.log('Failed to subscribe the user: ', err));
}
function unsubscribeUser(subscription, pushButton, swRegistration) {
subscription.unsubscribe()
.then(() => {
console.log('使用者已取消訂閱.');
updatePushButton(null, pushButton);
// (Optional) 向伺服器發送取消訂閱請求
// ...
})
.catch(err => console.log('取消訂閱失敗: ', err));
}
-
urlB64ToUint8Array(base64String)
函式:將 Base64 編碼的字串轉換為 Uint8Array 格式。這是 Web Push 訂閱所需的格式。 -
subscribeUser(swRegistration)
函式:訂閱使用者的推播通知。 userVisibleOnly: true
:確保所有推播通知都顯示給使用者。-
unsubscribeUser(subscription, pushButton, swRegistration)
函式:取消訂閱使用者的推播通知。
實作 Service Worker
Chrome / Edge 的 DevTools 的「應用程式」頁籤有提供 Service Worker 測試推播功能,不用送 request 到後端,直接觸發 API 呼叫 push 事件,以下程式碼示範如何在 Service Worker 中處理 push 事件,並顯示推播通知:
const SW = '[Service Worker]';
self.addEventListener('push', event => { // 監聽 push 事件
const eventData = event.data.text(); // 提取推播訊息
console.log(`${SW} Push Received.`);
console.log(`${SW} Push had this data: ${eventData}`);
const title = '推送好消息'; // 設定通知標題
const options = { // 設定通知選項
body: eventData, // 設定通知內容
// icon: '', // 設定通知圖示
// badge: '' // 設定通知徽章
};
// 使用 showNotification() 方法顯示推播通知
event.waitUntil(
new Promise(resolve => {
setTimeout(() => {
self.registration.showNotification(title, options).then(resolve);
}, 5000); // 延遲 5 秒顯示通知
})
);
});
這段程式碼會監聽 push 事件,並從事件資料中提取推播訊息。 然後,它會使用 showNotification() 方法顯示推播通知,並且通知的標題和內容中顯示提取到的訊息。
- 監聽 push 事件:self.addEventListener('push', event => { ... });
- 提取推播訊息:const eventData = event.data.text();
- 設定通知標題和選項:const title = '推送好消息'; 和 const options = { ... };
- 顯示推播通知:self.registration.showNotification(title, options);
- 延遲顯示通知:setTimeout(() => { ... }, 5000);
- 使用 waitUntil() 方法確保通知成功顯示:event.waitUntil( ... );
要注意的地方是,某些瀏覽器到現在 2024 年,Service Worker 都還不支援 ECMAScript modules 設計,所以 Servcie Worker 實作上建議盡量使用簡單的方式設計。
總結
呼~終於完成了!這篇文章帶大家用瀏覽器的內建功能,完整體驗了一次 Web Push 的流程,也實際看到了推播通知的效果。相當的簡單方便!
從檢查瀏覽器支援性、註冊 Service Worker、產生 Web Push 金鑰,再到訂閱和取消訂閱推播訊息,最後實作 Service Worker API 進行推播通知的顯示。希望這些步驟都能幫助大家理解 Web Push 的運作方式。
當然,這只是 Web Push 的單機入門介紹,之後如何與後端伺服器整合,發送客製化的推播訊息等相關資訊,我會在之後的文章中陸續跟大家分享,敬請期待!
最後,要再次感謝 Whien 的文章「建立 Service Worker Web Push Notification — (Web Notification 實作紀錄)」提供的程式碼範例和靈感。 也歡迎大家參考 Whien 的文章,了解更多 Web Push 的實作細節。
希望這篇文章對大家有所幫助,也歡迎大家留言分享你的學習心得和問題喔!
See also
- iPhone 各机型的 iOS 和 Safari 版本
- MDN - ServiceWorker
- 建立 Service Worker Web Push Notification — (Web Notification 實作紀錄)
- 頁首圖片攝影師:Liza Summer: https://www.pexels.com/zh-tw/photo/6348093/