一般的な通知パターン

ウェブプッシュの一般的な実装パターンを見てみましょう。

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

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

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

このイベントは通常、通知に対するユーザー エンゲージメントを追跡するための分析に使用されます。

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

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

通知にデータを追加する

プッシュ メッセージが受信されたときに、ユーザーが通知をクリックした場合にのみ有用なデータが存在することがよくあります。たとえば、通知がクリックされたときに開く URL などです。

プッシュ イベントからデータを取得して通知に添付する最も簡単な方法は、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('');

ウィンドウを開く

通知に対する最も一般的な応答の 1 つは、特定の URL へのウィンドウまたはタブを開くことです。これは clients.openWindow() API を使用して行えます。

notificationclick イベントでは、次のようなコードを実行します。

const examplePage = '/demos/notification-examples/example-page.html';
const promiseChain = clients.openWindow(examplePage);
event.waitUntil(promiseChain);

次のセクションでは、ユーザーを誘導するページがすでに開いているかどうかを確認する方法について説明します。これにより、新しいタブを開くのではなく、開いているタブにフォーカスできます。

既存のウィンドウにフォーカスを合わせる

可能であれば、ユーザーが通知をクリックするたびに新しいウィンドウを開くのではなく、ウィンドウにフォーカスを当てる必要があります。

これを実現する方法を確認する前に、これはオリジンのページでのみ可能であることに注意してください。これは、Google が確認できるのは、サイトに属する開いているページのみであるためです。これにより、デベロッパーはユーザーが閲覧しているすべてのサイトを確認できなくなります。

前述の例を基に、/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 と照合できるようにします。

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

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

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

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

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

matchAll() プロミスが解決すると、返されたウィンドウ クライアントを反復処理し、その 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 つ目の通知。

このアプローチの利点は、通知が重なって表示されるため、最新のメッセージに置き換えるだけの場合よりも、統一感のある外観と操作感を実現できることです。

ルールの例外

プッシュを受信したときに通知を表示する必要があると言いましたが、ほとんどの場合、これは当てはまります。通知を表示する必要がないシナリオは、ユーザーがサイトを開いてフォーカスしている場合です。

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

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

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

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

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

1 つの方法は、サービス ワーカーからページにメッセージを送信することです。これにより、ウェブページで通知や更新を表示して、イベントをユーザーに知らせることができます。これは、ページ内の控えめな通知がユーザーにとってより適切で親しみやすい状況に役立ちます。

プッシュを受信し、ウェブアプリが現在フォーカスされていることを確認したら、開いている各ページに「メッセージを投稿」できます。

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 からのメッセージは何もしません。

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

このガイドの範囲外ですが、検討に値するシナリオとして、通知をクリックした後にユーザーがアクセスすることが予想されるウェブページをキャッシュに保存することで、ウェブアプリの全体的な UX を改善できる点が挙げられます。

これには、fetch イベントを処理するようにサービス ワーカーを設定する必要があります。fetch イベント リスナーを実装する場合は、通知を表示する前に必要なページとアセットをキャッシュに保存して、push イベントでそのリスナーを利用するようにしてください。

ブラウザの互換性

notificationclose イベント

対応ブラウザ

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

ソース

Clients.openWindow()

対応ブラウザ

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

ソース

ServiceWorkerRegistration.getNotifications()

対応ブラウザ

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

ソース

clients.matchAll()

対応ブラウザ

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

ソース

詳細については、サービス ワーカーの概要に関する投稿をご覧ください。

次のステップ

Codelab