一般的な通知パターン

ウェブプッシュの一般的な実装パターンを見ていきます。

そのためには、Service Worker で利用可能な API をいくつか使用します。

通知クローズ イベント

前のセクションでは、notificationclick イベントをリッスンする方法を確認しました。

また、ユーザーが通知の 1 つを閉じた場合(ユーザーが通知をクリックしない場合、クロスをクリックするか通知をスワイプして閉じた場合)に呼び出される notificationclose イベントもあります。

このイベントは通常、通知に対するユーザー エンゲージメントをトラッキングするためのアナリティクスに使用されます。

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

  const promiseChain = notificationCloseAnalytics();
  event.waitUntil(promiseChain);
});

通知へのデータの追加

プッシュ メッセージの受信時には、ユーザーが通知をクリックした場合にのみ役立つデータを持つのが一般的です。たとえば、通知がクリックされたときに開く URL などです。

push イベントからデータを取得して通知に関連付ける最も簡単な方法は、次のように、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('');

ウィンドウを開く

通知に対する最も一般的な対応の一つは、特定の URL に対してウィンドウやタブを開くことです。これは、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 から得た素晴らしい手法です。location オブジェクトを指定して new URL() を呼び出すと、渡された文字列が相対パスである場合に絶対 URL が返されます(つまり、/https://example.com/ になります)。

後でウィンドウ URL と照合できるように、URL を絶対 URL にします。

const urlToOpen = new URL(examplePage, self.location.origin).href;

次に、WindowClient オブジェクトのリストを取得します。これは、現在開いているタブとウィンドウのリストです。(これらのタブは元のページ専用です)。

const promiseChain = clients.matchAll({
  type: 'window',
  includeUncontrolled: true,
});

matchAll に渡されるオプションにより、「ウィンドウ」タイプのクライアントのみを検索する(つまり、タブとウィンドウを探してウェブワーカーを除外する)ことをブラウザに通知します。includeUncontrolled を使用すると、現在の Service Worker(つまり、このコードを実行している Service Worker)によって制御されていないすべてのタブをオリジンから検索できます。一般に、matchAll() を呼び出すときは常に includeUncontrolled を true にする必要があります。

返された Promise を promiseChain としてキャプチャし、後で event.waitUntil() に渡して Service Worker を存続させられるようにします。

matchAll() Promise が解決されると、返されたウィンドウ クライアントを反復処理し、その URL と開く URL を比較します。一致するものが見つかった場合は、そのクライアントにフォーカスして、ウィンドウをユーザーの注意に向けさせます。フォーカスは 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 からのメッセージが 2 件あります」のようなメッセージを表示するとします。

ウェブアプリで現在表示されているすべての通知にアクセスできる 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 の新しい通知が作成されます。

その結果、最初のメッセージは次のようになります。

統合なしの最初の通知。

2 つ目の通知では、通知が次のように折りたたまれます。

統合を含む 2 つ目の通知。

このアプローチの利点は、通知が交互に表示されるのをユーザーが確認した場合、単に通知を最新のメッセージに置き換えるよりも、見た目と雰囲気が統一されることです。

ルールの例外

「プッシュを受け取ったら通知を表示する必要」は、たいていと述べてきました。通知を表示する必要がないシナリオの 1 つは、ユーザーがサイトを開いてフォーカスしている場合です。

push イベント内で、ウィンドウ クライアントを調べてフォーカスされたウィンドウを探すことで、通知を表示する必要があるかどうかを確認できます。

すべてのウィンドウを取得してフォーカスされているウィンドウを探すためのコードは次のようになります。

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 パラメータをチェックしてループします。

push イベント内で、この関数を使用して通知を表示する必要があるかどうかを判断します。

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);

push イベントからページにメッセージを送信する

ユーザーが現在サイトにアクセスしている場合は、通知の表示をスキップできることがわかりました。しかし、イベントが発生したことをユーザーに知らせたいが、通知の負担が大きすぎる場合はどうすればよいでしょうか。

1 つの方法は、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 を表示するだけでなく、メッセージを完全に無視することもできます。

また、ウェブページでメッセージ リスナーを定義しないと、Service Worker からのメッセージは何も実行されません。

ページをキャッシュに保存してウィンドウを開く

このガイドの範囲外ですが、説明する価値のあるシナリオの 1 つは、ユーザーが通知をクリックしたときにアクセスするウェブページをキャッシュに保存することで、ウェブアプリの全体的な UX を改善できるというものです。

そのためには、fetch イベントを処理するように Service Worker をセットアップする必要がありますが、fetch イベント リスナーを実装する場合は、通知を表示する前に必要なページとアセットをキャッシュして、push イベントで活用してください。

ブラウザの互換性

notificationclose イベント

対応ブラウザ

  • 50
  • 17
  • 44
  • 16

ソース

Clients.openWindow()

対応ブラウザ

  • 40
  • 17
  • 44
  • 11.1

ソース

ServiceWorkerRegistration.getNotifications()

対応ブラウザ

  • 40
  • 17
  • 44
  • 16

ソース

clients.matchAll()

対応ブラウザ

  • 42
  • 17
  • 54
  • 11.1

ソース

詳細については、Service Worker の概要に関する投稿をご覧ください。

次のステップ

Codelab