以下將說明一些常見的網路推送實作模式。
這牽涉到使用 Service Worker 中可用的幾種不同 API。
通知關閉事件
在上一節中,我們說明瞭如何監聽 notificationclick
事件。
此外,如果使用者關閉您的一則通知 (即不是按一下通知,而是點選交叉或滑開通知),系統也會呼叫 notificationclose
事件。
這個事件通常會用於分析,以便透過通知追蹤使用者的參與度。
self.addEventListener('notificationclose', function (event) {
const dismissedNotification = event.notification;
const promiseChain = notificationCloseAnalytics();
event.waitUntil(promiseChain);
});
在通知中新增資料
收到推送訊息時,如果資料只在使用者點按通知時才實用,是很常見的情況。例如,點選通知後應開啟的網址。
如要從推送事件擷取資料並附加至通知,最簡單的方法就是在傳遞至 showNotification()
的選項物件中加入 data
參數,如下所示:
const options = {
body:
'This notification has data attached to it that is printed ' +
"to the console when it's clicked.",
tag: 'data-notification',
data: {
time: new Date(Date.now()).toString(),
message: 'Hello, World!',
},
};
registration.showNotification('Notification with Data', options);
在點擊處理常式中,可以透過 event.notification.data
存取資料。
const notificationData = event.notification.data;
console.log('');
console.log('The notification data has the following parameters:');
Object.keys(notificationData).forEach((key) => {
console.log(` ${key}: ${notificationData[key]}`);
});
console.log('');
開啟視窗
通知最常見的回應之一,就是開啟特定網址的視窗 / 分頁。我們可以利用 clients.openWindow()
API 完成這項操作。
在 notificationclick
事件中,我們會執行一些程式碼,如下所示:
const examplePage = '/demos/notification-examples/example-page.html';
const promiseChain = clients.openWindow(examplePage);
event.waitUntil(promiseChain);
在下一節中,我們會說明如何檢查使用者要導向的頁面是否已經開啟。如此一來,我們就能專注於開啟的分頁,而不必開啟新分頁。
將焦點移至現有視窗
可以的話,我們應該聚焦於單一視窗,而非每次使用者點選通知時都開啟一個新視窗。
在我們開始說明相關做法前,值得注意的是,您只能針對自己來源的網頁執行這項動作。這是因為我們只能查看屬於我們網站的開啟網頁。以防開發人員無法查看使用者正在瀏覽的所有網站。
就上一個範例來看,我們將修改程式碼,確認 /demos/notification-examples/example-page.html
是否已開啟。
const urlToOpen = new URL(examplePage, self.location.origin).href;
const promiseChain = clients
.matchAll({
type: 'window',
includeUncontrolled: true,
})
.then((windowClients) => {
let matchingClient = null;
for (let i = 0; i < windowClients.length; i++) {
const windowClient = windowClients[i];
if (windowClient.url === urlToOpen) {
matchingClient = windowClient;
break;
}
}
if (matchingClient) {
return matchingClient.focus();
} else {
return clients.openWindow(urlToOpen);
}
});
event.waitUntil(promiseChain);
讓我們逐步瞭解程式碼。
首先,我們使用 URL API 剖析範例網頁。這是我從 Jeff Posnick 學到的超酷技巧。如果傳入的字串具有相對性 (即 /
會變成 https://example.com/
),使用 location
物件呼叫 new URL()
將會傳回絕對網址。
我們會用絕對網址進行網址比對,以便之後與視窗網址進行比對。
const urlToOpen = new URL(examplePage, self.location.origin).href;
接著,我們會取得 WindowClient
物件清單,其中列出目前開啟的分頁和視窗清單。(請注意,這些標籤僅適用於您來源的分頁)。
const promiseChain = clients.matchAll({
type: 'window',
includeUncontrolled: true,
});
傳入 matchAll
的選項會告知瀏覽器我們只搜尋「視窗」類型的用戶端 (也就是只搜尋分頁和視窗,並排除網路工作站)。includeUncontrolled
可讓我們搜尋來源中所有並非由目前服務工作站控管的分頁,也就是執行此程式碼的 Service Worker。一般來說,呼叫 matchAll()
時,您應一律將 includeUncontrolled
設為 true。
我們會擷取傳回的承諾為 promiseChain
,以便稍後將其傳遞至 event.waitUntil()
,讓服務工作站保持運作。
當 matchAll()
承諾解析時,我們會疊代傳回的視窗用戶端,並將其網址與要開啟的網址進行比較。找到相符的結果時,就會聚焦在用戶端
這樣能吸引使用者註意可透過 matchingClient.focus()
呼叫完成對焦。
如果找不到相符的用戶端,系統會開啟新視窗 (與上一個部分相同)。
.then((windowClients) => {
let matchingClient = null;
for (let i = 0; i < windowClients.length; i++) {
const windowClient = windowClients[i];
if (windowClient.url === urlToOpen) {
matchingClient = windowClient;
break;
}
}
if (matchingClient) {
return matchingClient.focus();
} else {
return clients.openWindow(urlToOpen);
}
});
合併通知
我們發現,在通知中新增標記後,便會選擇取代任何含有相同標記的現有通知。
不過,只要使用 Notifications API 收合通知,就能更複雜。假設有一款即時通訊應用程式,開發人員可能希望新通知顯示類似「您有兩則來自 Matt 的訊息」,而不是只顯示最新的訊息。
如要執行這項操作,或者以其他方式操控目前的通知,請使用 registration.getNotifications() API,讓您可以存取網頁應用程式目前顯示的所有通知。
一起來看看如何使用這個 API 實作即時通訊範例。
在我們的即時通訊應用程式中,假設每則通知都包含使用者名稱,包括使用者名稱。
首先,請找出具有特定使用者名稱的使用者尚未開啟的通知。我們會取得 registration.getNotifications()
並循環播放,然後檢查特定使用者名稱的 notification.data
:
const promiseChain = registration.getNotifications().then((notifications) => {
let currentNotification;
for (let i = 0; i < notifications.length; i++) {
if (notifications[i].data && notifications[i].data.userName === userName) {
currentNotification = notifications[i];
}
}
return currentNotification;
});
下一步是以新通知取代這則通知。
在這個假訊息應用程式中,我們會追蹤新訊息的數量,方法是在新通知的資料中加上計數,然後隨著新通知逐漸增加。
.then((currentNotification) => {
let notificationTitle;
const options = {
icon: userIcon,
}
if (currentNotification) {
// We have an open notification, let's do something with it.
const messageCount = currentNotification.data.newMessageCount + 1;
options.body = `You have ${messageCount} new messages from ${userName}.`;
options.data = {
userName: userName,
newMessageCount: messageCount
};
notificationTitle = `New Messages from ${userName}`;
// Remember to close the old notification.
currentNotification.close();
} else {
options.body = `"${userMessage}"`;
options.data = {
userName: userName,
newMessageCount: 1
};
notificationTitle = `New Message from ${userName}`;
}
return registration.showNotification(
notificationTitle,
options
);
});
如果有目前顯示的通知,我們會增加訊息數,並據此設定通知標題和內文訊息。如果沒有通知,系統會建立新通知,newMessageCount
為 1。
如此一來,第一則訊息看起來會像這樣:
第二次通知會把通知收合成下方:
這個做法特別適合使用者查看通知彼此間的情況,而不只是將通知換成最新訊息,看起來會更一致。
例外狀況
我已表明,在收到推送時必須顯示通知,在「大部分」下,這是真的。在以下其中一種情況中,您不必顯示通知,就是使用者開啟了專注的網站。
在推送事件中,您可以檢查視窗用戶端,找出聚焦視窗,藉此檢查是否需要顯示通知。
取得所有視窗並尋找聚焦視窗的程式碼如下所示:
function isClientFocused() {
return clients
.matchAll({
type: 'window',
includeUncontrolled: true,
})
.then((windowClients) => {
let clientIsFocused = false;
for (let i = 0; i < windowClients.length; i++) {
const windowClient = windowClients[i];
if (windowClient.focused) {
clientIsFocused = true;
break;
}
}
return clientIsFocused;
});
}
我們會使用 clients.matchAll()
取得所有視窗用戶端,然後循環檢查 focused
參數。
在推送事件中,我們會使用這個函式來決定是否需要顯示通知:
const promiseChain = isClientFocused().then((clientIsFocused) => {
if (clientIsFocused) {
console.log("Don't need to show a notification.");
return;
}
// Client isn't focused, we need to show a notification.
return self.registration.showNotification('Had to show a notification.');
});
event.waitUntil(promiseChain);
透過推送事件傳送訊息給網頁
我們發現,如果使用者目前瀏覽網站,您可以選擇略過顯示通知。但是,如果您想讓使用者知道事件已發生,但通知內容太過繁重,該怎麼辦?
其中一個方法是將訊息從 Service Worker 傳送至網頁,這樣網頁就能向使用者顯示通知或更新,通知事件。如果網頁中有些細微的通知對使用者更友善,這個方法就能派上用場。
假設我們收到了推文,檢查我們的網頁應用程式目前位於焦點中,然後在每個開啟的頁面「張貼訊息」,如下所示:
const promiseChain = isClientFocused().then((clientIsFocused) => {
if (clientIsFocused) {
windowClients.forEach((windowClient) => {
windowClient.postMessage({
message: 'Received a push message.',
time: new Date().toString(),
});
});
} else {
return self.registration.showNotification('No focused windows', {
body: 'Had to show a notification instead of messaging each page.',
});
}
});
event.waitUntil(promiseChain);
我們會在每個頁面中新增訊息事件事件監聽器,藉此監聽訊息:
navigator.serviceWorker.addEventListener('message', function (event) {
console.log('Received a message from service worker: ', event.data);
});
在這個訊息事件監聽器中,您可以執行任何動作、在網頁上顯示自訂 UI,或完全忽略該訊息。
另外值得注意的是,如果您沒有在網頁中定義訊息事件監聽器,服務工作站傳送的訊息就不會執行任何動作。
快取網頁並開啟視窗
本指南不在本指南的說明範圍內,但值得注意的是,您可以快取預期使用者在點選通知後會造訪的網頁,以改善網頁應用程式的整體使用者體驗。
您必須設定服務工作站來處理 fetch
事件,但如果您實作了 fetch
事件監聽器,請務必在顯示通知前快取所需的頁面和資產,以充分利用 push
事件中的效能。
瀏覽器相容性
notificationclose
事件
Clients.openWindow()
ServiceWorkerRegistration.getNotifications()
clients.matchAll()
詳情請參閱這篇服務工作站簡介文章。