常見的通知模式

Matt Gaunt

我們將探討幾種常見的網路推送實作模式。

這會涉及使用服務工作者中提供的幾個不同 API。

通知關閉事件

在上一節中,我們瞭解了如何監聽 notificationclick 事件。

如果使用者關閉其中一個通知 (即使用者按下 X 或滑動關閉通知,而非點選通知),系統也會呼叫 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 的選項會告知瀏覽器,我們只想搜尋「window」類型的用戶端 (也就是只搜尋分頁和視窗,並排除網路工作者)。includeUncontrolled 可讓我們搜尋來源中所有未受目前服務工作站 (即執行此程式碼的服務工作站) 控制的分頁。一般來說,呼叫 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 可讓您存取網頁應用程式目前顯示的所有通知。

我們來看看如何使用這個 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); 

透過推播事件傳送訊息給網頁

我們發現,如果使用者目前位於您的網站,您可以略過顯示通知。不過,如果您仍想讓使用者知道事件已發生,但通知太過強硬,該怎麼辦呢?

其中一種方法是從服務工作者傳送訊息至網頁,這樣網頁就能向使用者顯示通知或更新,告知他們事件發生的時間。在網頁中顯示較不顯眼的通知,對使用者來說會更友善,這時就很適合使用這項功能。

假設我們收到推播,並確認目前已將焦點放在網頁應用程式上,就可以向每個已開啟的網頁「發布訊息」,如下所示:

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 事件

Browser Support

  • Chrome: 50.
  • Edge: 17.
  • Firefox: 44.
  • Safari: 16.

Source

Clients.openWindow()

Browser Support

  • Chrome: 40.
  • Edge: 17.
  • Firefox: 44.
  • Safari: 11.1.

Source

ServiceWorkerRegistration.getNotifications()

Browser Support

  • Chrome: 40.
  • Edge: 17.
  • Firefox: 44.
  • Safari: 16.

Source

clients.matchAll()

Browser Support

  • Chrome: 42.
  • Edge: 17.
  • Firefox: 54.
  • Safari: 11.1.

Source

如需更多資訊,請參閱這篇服務工作者文章簡介

後續步驟

程式碼研究室