《Web 推送通知》系列翻譯 | 第九篇:通知行為 && 第十篇:常用的通知模式

閱文前端團隊發表於2019-02-28

第九篇:通知行為

原文地址:notification behaviour

譯文地址:通知行為

譯者:任家樂

校對者:劉文濤楊芯芯

到此為止,我們已經瀏覽了可以改變通知樣式的選項,除了樣式,我們還可以通過一些選項來改變通知的行為。

預設情況下,如果只設定視覺相關選項,呼叫 showNotification() 會出現以下行為:

  • 點選通知不會觸發任何事件。
  • 每個新的通知會逐一有序地展示,瀏覽器不會以任何方式疊加展示通知。
  • 系統會以音效或震動的方式提示使用者(具體方式則取決於裝置系統)。
  • 在某些系統上,通知會在短時間展示後消失,而其他系統則會一直展示通知直到使用者對其進行操作。(可以對比安卓和桌面的通知行為)

在這一節中,我們會探討如何單獨使用一些選項改變預設的通知行為,這相對來說比較容易實施和利用。

通知的點選事件

當使用者點選通知時,預設不會觸發任何事件,它並不會關閉或移除通知。

通知點選事件的常見用法是呼叫它來關閉通知、同時執行一些其他的邏輯(例如,開啟一個視窗或對應用程式進行一些API呼叫)

為此,我們需要在 service worker 中新增一個 “notificationclick” 事件監聽器。 這個事件將在點選通知時被呼叫。

self.addEventListener('notificationclick', function(event) {
  const clickedNotification = event.notification;
  clickedNotification.close();

  // 點選通知後做些什麼
  const promiseChain = doSomething();
  event.waitUntil(promiseChain);
});
複製程式碼

正如你在此示例中所看到的,被點選的通知可以通過 event.notification 引數來訪問。通過這個引數我們可以獲得通知的屬性和方法,因此我們能夠呼叫通知的 close() 方法,同時執行一些額外的操作。

提示:在程式執行高峰期,你仍然需要呼叫 event.waitUntil() 保證 service worker 的持續執行。

Actions

相比於之前的普通點選行為,actions 的使用可以提供給使用者更高階別的互動體驗。

在上一節中,我們知道了如何呼叫 showNotification() 來定義 actions

    const title = 'Actions Notification';
    const options = {
      actions: [
        {
          action: 'coffee-action',
          title: 'Coffee',
          icon: '/images/demos/action-1-128x128.png'
        },
        {
          action: 'doughnut-action',
          title: 'Doughnut',
          icon: '/images/demos/action-2-128x128.png'
        },
        {
          action: 'gramophone-action',
          title: 'gramophone',
          icon: '/images/demos/action-3-128x128.png'
        },
        {
          action: 'atom-action',
          title: 'Atom',
          icon: '/images/demos/action-4-128x128.png'
        }
      ]
    };

    const maxVisibleActions = Notification.maxActions;
    if (maxVisibleActions < 4) {
      options.body = `This notification will only display ` +
        `${maxVisibleActions} actions.`;
    } else {
      options.body = `This notification can display up to ` +
        `${maxVisibleActions} actions.`;
    }

    registration.showNotification(title, options);
複製程式碼

如果使用者點選了 action 按鈕,通過 notificationclick 回撥中返回的 event.action 就可以知道被點選的按鈕是哪個。

event.action 會包含所有選項中有關 action 的值的集合。在上面的例子中,event.action 的值則會是: “coffee-action”、 “doughnut-action”、 “gramophone-action” 或 “atom-action” 的其中一個。

因此通過 event.action,我們可以檢測到通知或 action 的點選,程式碼如下:

self.addEventListener('notificationclick', function(event) {
  if (!event.action) {
    // 正常的通知點選事件
    console.log('Notification Click.');
    return;
  }

  switch (event.action) {
    case 'coffee-action':
      console.log('User ❤️️\'s coffee.');
      break;
    case 'doughnut-action':
      console.log('User ❤️️\'s doughnuts.');
      break;
    case 'gramophone-action':
      console.log('User ❤️️\'s music.');
      break;
    case 'atom-action':
      console.log('User ❤️️\'s science.');
      break;
    default:
      console.log(`Unknown action clicked: '${event.action}'`);
      break;
  }
});
複製程式碼
Logs for action button clicks and notification click.

Tag(標籤)

tag 選項的本質是一個字串型別的 ID,以此將通知 “分組” 在一起,並提供了一種簡單的方法來向使用者顯示多個通知,這裡可能用示例來解釋最為簡單:

讓我們來展示一個通知,並給它標記一個 tag,例如 “message-group-1”。 我們可以按照如下程式碼來展示這個通知:

    const title = 'Notification 1 of 3';
    const options = {
      body: 'With \'tag\' of \'message-group-1\'',
      tag: 'message-group-1'
    };
    registration.showNotification(title, options);
複製程式碼

這會展示我們定義好的第一個通知。

First notification with tag of message group 1.

我們再用一個新的 tag “message-group-2” 來標記並展示第二個通知,程式碼如下:

        const title = 'Notification 2 of 3';
        const options = {
          body: 'With \'tag\' of \'message-group-2\'',
          tag: 'message-group-2'
        };
        registration.showNotification(title, options);
複製程式碼

這樣會展示給使用者第二個通知。

Two notifications where the second tag is message group 2.

現在讓我們展示第三個通知,但不新增 tag,而是重用我們第一次定義的 tag “message-group-1”。這樣操作會關閉之前的第一個通知並將其替換成新定義的通知。

        const title = 'Notification 3 of 3';
        const options = {
          body: 'With \'tag\' of \'message-group-1\'',
          tag: 'message-group-1'
        };
        registration.showNotification(title, options);
複製程式碼

現在即使我們連續 3 次呼叫 showNotification() 也只會展示 2 個通知。

Two notifications where the first notification is replaced by a third notification.

tag 這個選項簡單來看就是一個用於資訊分組的方式,因此在新通知與已有通知標記為同一個tag時,當前被展示的所有舊通知將會被關閉。

使用 tag 有一個容易被忽略的小細節:當它替換了一個通知時,是沒有音效和震動提醒的。

此時 Renotify 選項就有了用武之地。

Renotify(是否替換之前的通知)

在寫此文時,這個選項大多數應用於移動裝置。通過設定它,接收到新的通知時,系統會震動並播放系統音效。

某些場景下,你可能更希望替換通知時能夠提醒到使用者,而不是默默地進行。聊天應用則是一個很好的例子。這種情況你需要同時使用 tagRenotify 選項。

        const title = 'Notification 2 of 2';
        const options = {
          tag: 'renotify',
          renotify: true
        };
        registration.showNotification(title, options);

TypeError: Failed to execute 'showNotification' on 'ServiceWorkerRegistration':
Notifications which set the renotify flag must specify a non-empty tag
複製程式碼

注意: 如果你設定了 Renotify: true 但卻沒有設定標籤,會出現以下報錯資訊:

型別錯誤:不能夠在 “ServiceWorkerRegistration” 上執行 “showNotification” 方法:設定了 renotify 標識的通知必須宣告一個不為空的標籤。(TypeError: Failed to execute 'showNotification' on 'ServiceWorkerRegistration':Notifications which set the renotify flag must specify a non-empty tag)

Silent(靜音)

這一選項可以阻止裝置震動、音效以及螢幕亮起的預設行為。如果你的通知不需要立馬讓使用者注意到,這個選項是最合適的。

    const title = 'Silent Notification';
    const options = {
      silent: true
    };
    registration.showNotification(title, options);
複製程式碼

注意: 如果同時設定了 silentRenotify,silent 選項會取得更高的優先順序。

與通知進行互動

桌面 chrome 瀏覽器會展示通知一段時間後將其隱藏,而安卓裝置的 chrome 瀏覽器不會有這種行為,通知會一直展示,直到使用者對其進行操作。

如果要強制讓通知持續展示直到使用者對其操作,需要新增 requireInteraction 選項,此選項會展示通知直到使用者消除或點選它。

    const title = 'Require Interaction Notification';
    const options = {
      body: 'With "requireInteraction: \'true\'".',
      requireInteraction: true
    };
    registration.showNotification(title, options);
複製程式碼

請謹慎使用這個選項,因為一直展示通知、並強制讓使用者停下手頭的事情來忽略通知可能會干擾到使用者。

在下一節中,我們會瀏覽一些 web 上適用的用於管理通知的常見模式,以及如何執行一些常見的 actions,例如在點選通知時執行開啟網頁的行為。

第十篇:常用的通知模式

原文地址:common notification patterns

譯文地址:常用的通知模式

譯者:任家樂

校對者:劉文濤楊芯芯

此篇我們將會探索 Web 推送的一些常用模式,包括使用一些 service worker 提供的 API。

通知的關閉事件

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

除了 notificationclick 事件,我們還可以監聽 notificationclose 事件,它會在使用者忽略其中一個通知(例如,使用者點選了關閉按鈕或劃掉了通知,而不是點選了它)時被呼叫。

這個事件通常被用作資料分析,以此追蹤使用者與通知的互動情況。

self.addEventListener('notificationclose', function(event) {
  const dismissedNotification = event.notification;

  const promiseChain = notificationCloseAnalytics();
  event.waitUntil(promiseChain);
});
複製程式碼

給通知新增資料

當收到推送的資訊時,通常只需要獲取使用者點選後的有用資料。例如,獲取使用者點選通知時開啟的頁面地址。

如果需要將推送事件中獲取的資料傳遞給通知,最簡單的方式就是在呼叫 showNotification() 時,給引數 options 物件新增一個 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 data notification had the following parameters:');
  Object.keys(notificationData).forEach((key) => {
    console.log(`  ${key}: ${notificationData[key]}`);
  });
  console.log('');
複製程式碼

開啟一個視窗

對一個通知來說,開啟指定地址的視窗/標籤頁可以說是一種最常見的反饋,這個我們可以通過 clients.openWindow() 來實現。

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 那學到的一個巧妙的計策。 呼叫 new URL() 並傳入 location 物件,如果傳入的第一個引數是相對地址,則會返回頁面的絕對地址(例如,“/” 會變成 “https://站點域名” )。

我們將地址轉成了絕對地址則是為了之後與視窗的地址作對比。

  const urlToOpen = new URL(examplePage, self.location.origin).href;
複製程式碼

之後,我們會通過呼叫 matchAll() 得到一系列 WindowClient 物件,包含了當前開啟的標籤頁和視窗。(記住,這些標籤頁只是你域名下的頁面)

  const promiseChain = clients.matchAll({
    type: 'window',
    includeUncontrolled: true
  })
複製程式碼

matchAll 方法中傳入的 options 物件則告訴瀏覽器我們只想獲取 “window” 型別的物件(例如,只檢視標籤頁、視窗,不包含 web workers [瀏覽器的其他工作執行緒])。 includeUncontrolled 屬性表示我們只能獲取沒有被當前 service worker 控制的所有標籤頁(本域下),例如 service worker 正在執行當前程式碼。一般來說,在呼叫 matchAll() 時,你通常會將 includeUncontrolled 設定為 true。

我們 以promiseChain(promise 鏈式呼叫)的形式捕獲返回的 promise 物件,因此之後可以將其傳 入event.waitUntil() 方法中以此保持我們的 service worker 持續工作。

當上一步的 matchAll() 返回的 promise 物件已完成非同步操作,我們就可以開始遍歷返回的 window 物件,並將這些物件的 URL 和想要開啟的目標 URL 進行對比,如果發現有匹配的,則呼叫 matchingClient.focus() 方法,它會呼起匹配的視窗,引起使用者的注意。

如果沒有與之匹配的 URL,我們則採用上一節的方式新開視窗開啟地址。

  .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);
    }
  });
複製程式碼

注意: 我們會返回 matchingClient.focus()clients.openWindow() 方法執行後返回的 promise 物件, 這樣 promise 物件就可以組成我們的 promise 呼叫鏈了。

合併通知

我們已經看到,給一個通知新增標籤後會導致用同一個標籤標識的已有通知被替代。

但通過使用通知相關的 API,你可以更靈活地覆蓋展示通知。比如一個聊天應用,開發者可能更希望用新的通知來展示"你有 2 條未讀資訊"等類似資訊,而不是隻展示最新接收到的資訊。

你可以利用新的通知,或以其他方式操作當前已有通知,使用 registration.getNotifications()API 能夠獲得到你 APP 中所有當前展示的通知。

讓我們看看如何使用這個 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) {
        // 我們有一個已經開啟的通知,讓我們利用它來做些什麼
        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}`;

        // 記得關閉舊的通知
        currentNotification.close();
      } else {
        options.body = `"${userMessage}"`;
        options.data = {
          userName: userName,
          newMessageCount: 1
        };
        notificationTitle = `New Message from ${userName}`;
      }

      return registration.showNotification(
        notificationTitle,
        options
      );
    });
複製程式碼

我們會累加當前展示的通知的資訊數,同時依據這個資料來設定通知的主題和內容資訊。如果當前沒有展示通知,我們則會展示一個新的通知,其資料中 newMessageCount 的值為 1。

那麼第一條資訊的通知會是以下這樣:

第二條通知會以這樣的方式覆蓋已有的通知:

Second notification with merging.

這種方法的好處是,如果你的使用者目睹了通知一個接著一個的出現,相比於只用最新的資訊替代當前的通知內容,訊息看起來則更緊密相連。

各種規則的例外

我一直強調的是,你必須在收到推送資訊時展示通知,這個規則在大多數情況下是正確的。只有在一種場景下你不需要展示通知, 那就是使用者已經開啟了你的站點,並且站點已經是呼起的狀態。

在你的推送事件中,你可以通過檢測目標視窗是否已被開啟並呼起,來決定是否需要展示通知。

獲得瀏覽器所有視窗、查詢當前已呼起視窗的程式碼可以參考如下:

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;

    }

    // 視窗並沒有被呼起,我們需要展示通知
    return self.registration.showNotification('Had to show a notification.');
  });

  event.waitUntil(promiseChain);
複製程式碼

通過推送事件給頁面傳送訊息

我們已知,可以在使用者正在瀏覽我們站點的時候不進行通知。但是,如果你仍然想讓使用者知道這個推送事件已經發生,但又覺得進行通知太過強硬,應該如何處理?

其中一個方法就是利用 service worker 給頁面傳送訊息,這種情況下頁面能夠給使用者展示通知或更新,以此讓使用者知曉到這個推送事件的發生。當然,只有當使用者對於輕量級通知感到更友好時,這種做法才有用。

如果我們接收到了一個推送,並且檢測到了我們的 APP 已經被開啟了,那麼我們就可以"傳送訊息"給每個開啟的頁面,就像以下這樣:

  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);
複製程式碼

在每個頁面中,我們通過監聽訊息(message)事件來接收訊息:

    navigator.serviceWorker.addEventListener('message', function(event) {
      console.log('Received a message from service worker: ', event.data);
    });
複製程式碼

在這個訊息監聽器中,你可以做任何你想做的事,例如展示自定義的檢視,或者完全忽略這個訊息。

值得注意的是,如果你沒有在你的網頁中定義訊息監聽器,那麼 service worker 推送的訊息將不會做任何事。

快取頁面和視窗物件

有一個場景可能超出了這本書的範疇,但是依然值得探討,那就是快取你希望使用者點選通知後訪問的網頁,以此來提升web應用的整體使用者體驗。

這就需要設定你的 service worker 來處理這些 fetch 事件,但如果你監聽了 fetch 事件,請確保在展示你的通知之前,通過快取你需要的頁面和資源來充分利用它在 push 事件中的優勢。

想要了解更多快取相關資訊,請參考服務工作執行緒:簡介

更多分享,請關注YFE:

《Web 推送通知》系列翻譯 | 第九篇:通知行為 && 第十篇:常用的通知模式

上一篇:《Web 推送通知》系列翻譯 | 第七篇:推送事件 && 第八篇:顯示一個通知

下一篇:《Web 推送通知》系列翻譯 | 第十一篇:FAQ && 第十二篇:常見問題以及錯誤反饋

相關文章