Häufige Benachrichtigungsmuster

Matt Gaunt

Wir sehen uns einige gängige Implementierungsmuster für Web-Push an.

Dazu müssen verschiedene APIs verwendet werden, die im Service Worker verfügbar sind.

Benachrichtigungs-Schließen-Ereignis

Im letzten Abschnitt haben wir gesehen, wie wir auf notificationclick-Ereignisse warten können.

Es gibt auch ein notificationclose-Ereignis, das aufgerufen wird, wenn der Nutzer eine Ihrer Benachrichtigungen schließt. Anstatt auf die Benachrichtigung zu klicken, klickt der Nutzer auf das Kreuz oder wischt die Benachrichtigung weg.

Dieses Ereignis wird normalerweise für Analysen verwendet, um das Nutzer-Engagement mit Benachrichtigungen zu erfassen.

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

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

Daten zu einer Benachrichtigung hinzufügen

Wenn eine Push-Nachricht empfangen wird, sind in der Regel Daten vorhanden, die nur dann nützlich sind, wenn der Nutzer auf die Benachrichtigung geklickt hat. Beispielsweise die URL, die geöffnet werden soll, wenn auf eine Benachrichtigung geklickt wird.

Die einfachste Möglichkeit, Daten aus einem Push-Ereignis zu übernehmen und an eine Benachrichtigung anzuhängen, besteht darin, dem Optionsobjekt, das an showNotification() übergeben wird, einen data-Parameter hinzuzufügen. Das geht so:

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

Innerhalb eines Klick-Handlers kann mit event.notification.data auf die Daten zugegriffen werden.

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

Fenster öffnen

Eine der häufigsten Reaktionen auf eine Benachrichtigung ist das Öffnen eines Fensters/Tabs mit einer bestimmten URL. Dafür können Sie die clients.openWindow() API verwenden.

Für das Ereignis notificationclick würden wir folgenden Code ausführen:

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

Im nächsten Abschnitt erfahren Sie, wie Sie prüfen, ob die Seite, zu der Sie den Nutzer weiterleiten möchten, bereits geöffnet ist. So können wir den Fokus auf den geöffneten Tab legen, anstatt neue Tabs zu öffnen.

Ein vorhandenes Fenster aktivieren

Wenn möglich, sollten wir ein Fenster in den Fokus rücken, anstatt jedes Mal, wenn der Nutzer auf eine Benachrichtigung klickt, ein neues Fenster zu öffnen.

Bevor wir uns ansehen, wie das funktioniert, möchte ich noch einmal darauf hinweisen, dass dies nur für Seiten auf deinem Ursprungsserver möglich ist. Das liegt daran, dass wir nur sehen können, welche Seiten unserer Website geöffnet sind. So können Entwickler nicht alle Websites sehen, die ihre Nutzer aufrufen.

Im vorherigen Beispiel ändern wir den Code, um festzustellen, ob /demos/notification-examples/example-page.html bereits geöffnet ist.

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

Sehen wir uns den Code an.

Zuerst parsen wir unsere Beispielseite mit der URL API. Dieser praktische Trick stammt von Jeff Posnick. Wenn du new URL() mit dem location-Objekt aufrufst, wird eine absolute URL zurückgegeben, wenn der übergebene String relativ ist (d. h. / wird zu https://example.com/).

Wir machen die URL absolut, damit wir sie später mit Fenster-URLs abgleichen können.

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

Daraufhin erhalten wir eine Liste der WindowClient-Objekte, also der aktuell geöffneten Tabs und Fenster. Denken Sie daran, dass diese Tabs nur für Ihren Ursprung gelten.

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

Die an matchAll übergebenen Optionen informieren den Browser darüber, dass nur nach Clients vom Typ „window“ gesucht werden soll (d. h. nur nach Tabs und Fenstern suchen und Webworker ausschließen). Mit includeUncontrolled können wir nach allen Tabs von Ihrem Ursprung suchen, die nicht vom aktuellen Service Worker gesteuert werden, d. h. vom Service Worker, der diesen Code ausführt. Im Allgemeinen sollte includeUncontrolled immer wahr sein, wenn matchAll() aufgerufen wird.

Wir speichern das zurückgegebene Versprechen als promiseChain, damit wir es später an event.waitUntil() übergeben können, um unseren Dienst-Worker am Leben zu erhalten.

Wenn das matchAll()-Versprechen aufgelöst wird, durchlaufen wir die zurückgegebenen Fensterclients und vergleichen ihre URLs mit der URL, die wir öffnen möchten. Wenn wir eine Übereinstimmung finden, konzentrieren wir uns auf diesen Kunden, wodurch dieses Fenster die Aufmerksamkeit des Nutzers auf sich zieht. Der Fokus erfolgt mit dem matchingClient.focus()-Aufruf.

Finden wir keinen passenden Kunden, öffnen wir wie im vorherigen Abschnitt ein neues Fenster.

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

Benachrichtigungen zusammenführen

Wir haben festgestellt, dass durch das Hinzufügen eines Tags zu einer Benachrichtigung alle vorhandenen Benachrichtigungen mit demselben Tag ersetzt werden.

Mit der Benachrichtigungs-API können Sie jedoch noch komplexere Benachrichtigungen zum Minimieren erhalten. Stellen Sie sich eine Chat-App vor, in der der Entwickler bei einer neuen Benachrichtigung eine Meldung wie „Sie haben zwei Nachrichten von Max“ sehen möchte, anstatt nur die letzte Nachricht.

Sie können dies oder aktuelle Benachrichtigungen auf andere Weise mit der API registration.getNotifications() tun, die Ihnen Zugriff auf alle derzeit sichtbaren Benachrichtigungen für Ihre Webanwendung gewährt.

Sehen wir uns an, wie wir diese API verwenden könnten, um das Chatbeispiel zu implementieren.

Angenommen, in unserer Chat-App enthält jede Benachrichtigung einige Daten, darunter einen Nutzernamen.

Als Erstes möchten wir alle offenen Benachrichtigungen für einen Nutzer mit einem bestimmten Nutzernamen finden. Wir holen uns registration.getNotifications(), durchlaufen sie und prüfen die notification.data auf einen bestimmten Nutzernamen:

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

Im nächsten Schritt ersetzen Sie diese Benachrichtigung durch eine neue.

In dieser App für gefälschte Nachrichten erfassen wir die Anzahl der neuen Nachrichten, indem wir den Daten der neuen Benachrichtigung eine Zählung hinzufügen und diese mit jeder neuen Benachrichtigung erhöhen.

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

Wenn derzeit eine Benachrichtigung angezeigt wird, erhöhen wir die Anzahl der Nachrichten und legen den Benachrichtigungstitel und den Nachrichtentext entsprechend fest. Wenn keine Benachrichtigungen vorhanden sind, erstellen wir eine neue Benachrichtigung mit einer newMessageCount von 1.

Die erste Nachricht würde dann so aussehen:

Erste Benachrichtigung ohne Zusammenführung.

Bei einer zweiten Benachrichtigung werden die Benachrichtigungen so minimiert:

Zweite Benachrichtigung zur Zusammenführung

Der Vorteil dieses Ansatzes ist, dass die Benachrichtigungen, die Nutzer sehen, übereinander angezeigt werden, kohärenter, als wenn sie nur durch die neueste Nachricht ersetzt werden.

Ausnahme von der Regel

Ich habe bereits erklärt, dass Sie müssen, eine Benachrichtigung anzeigen, wenn Sie eine Push-Mitteilung erhalten. Das ist in den meisten Fällen der Fall. Eine Benachrichtigung muss nur angezeigt werden, wenn der Nutzer Ihre Website geöffnet und im Fokus hat.

Im Push-Ereignis können Sie prüfen, ob eine Benachrichtigung angezeigt werden soll, indem Sie die Fensterclients prüfen und nach einem fokussierten Fenster suchen.

Der Code zum Abrufen aller Fenster und zum Suchen nach einem fokussierten Fenster sieht so aus:

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

Mit clients.matchAll() rufen wir alle Fensterclients ab und durchlaufen sie dann, um den Parameter focused zu prüfen.

In unserem Push-Ereignis verwenden wir diese Funktion, um zu entscheiden, ob eine Benachrichtigung angezeigt werden soll:

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

Eine Seite über ein Push-Ereignis anschreiben

Wir haben festgestellt, dass Sie die Anzeige einer Benachrichtigung überspringen können, wenn sich der Nutzer derzeit auf Ihrer Website befindet. Was aber, wenn Sie den Nutzer trotzdem darüber informieren möchten, dass ein Ereignis aufgetreten ist, eine Benachrichtigung aber zu aufdringlich ist?

Eine Möglichkeit besteht darin, eine Nachricht vom Service Worker an die Seite zu senden. So kann die Webseite dem Nutzer eine Benachrichtigung oder ein Update anzeigen, um ihn über das Ereignis zu informieren. Das ist nützlich, wenn eine dezente Benachrichtigung auf der Seite für den Nutzer besser und nutzerfreundlicher ist.

Angenommen, wir haben eine Push-Mitteilung erhalten und geprüft, ob unsere Webanwendung derzeit im Fokus ist. Dann können wir auf jeder geöffneten Seite eine „Nachricht posten“, z. B. so:

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

Auf jeder Seite überwachen wir Nachrichten, indem wir einen Listener für das Ereignis „message“ hinzufügen:

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

In diesem Message Listener können Sie alles tun, was Sie möchten, z. B. eine benutzerdefinierte Benutzeroberfläche auf Ihrer Seite anzeigen oder die Nachricht vollständig ignorieren.

Wenn Sie auf Ihrer Webseite keinen Nachrichtenempfänger definieren, haben die Nachrichten vom Service Worker keine Auswirkungen.

Seite im Cache speichern und Fenster öffnen

Ein Szenario, das nicht in den Rahmen dieses Leitfadens fällt, aber erwähnenswert ist: Sie können die UX Ihrer Webanwendung insgesamt verbessern, indem Sie Webseiten im Cache speichern, die Nutzer voraussichtlich aufrufen, nachdem sie auf Ihre Benachrichtigung geklickt haben.

Dazu muss Ihr Service Worker so eingerichtet sein, dass er fetch-Ereignisse verarbeitet. Wenn Sie einen fetch-Ereignis-Listener implementieren, sollten Sie ihn in Ihrem push-Ereignis nutzen, indem Sie die Seite und die Assets, die Sie benötigen, im Cache speichern, bevor die Benachrichtigung angezeigt wird.

Browserkompatibilität

Das notificationclose-Ereignis

Unterstützte Browser

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

Quelle

Clients.openWindow()

Unterstützte Browser

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

Quelle

ServiceWorkerRegistration.getNotifications()

Unterstützte Browser

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

Quelle

clients.matchAll()

Unterstützte Browser

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

Quelle

Weitere Informationen finden Sie in diesem Artikel.

Weitere Informationen

Codelabs