Typowe wzorce powiadomień

Przyjrzymy się niektórym częstym schematom implementacji web push.

Wymaga to użycia kilku różnych interfejsów API dostępnych w skrypcie service worker.

Zdarzenie dotyczące zamknięcia powiadomienia

W ostatniej sekcji pokazaliśmy, jak można nasłuchiwać zdarzeń notificationclick.

Zdarzenie notificationclose jest też wywoływane, gdy użytkownik odrzuci jedno z powiadomień (czyli zamiast kliknąć powiadomienie, kliknie krzyżyk lub przesunie powiadomienie).

To zdarzenie zwykle służy do celów analitycznych do śledzenia zaangażowania użytkowników w związku z powiadomieniami.

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

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

Dodawanie danych do powiadomienia

Często po otrzymaniu wiadomości push dane są przydatne tylko wtedy, gdy użytkownik kliknie powiadomienie. Może to być np. adres URL, który powinien być otwierany po kliknięciu powiadomienia.

Najprostszym sposobem na przenoszenie danych ze zdarzenia push i dołączenie ich do powiadomienia jest dodanie parametru data do obiektu opcji przekazywanego do showNotification(). Przykład:

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

W ramach modułu obsługi kliknięć dostęp do danych można uzyskać za pomocą funkcji 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('');

Otwieranie okna

Jedną z najczęstszych reakcji na powiadomienia jest otwarcie okna lub karty pod określonym adresem URL. Umożliwia to interfejs API clients.openWindow().

W ramach wydarzenia notificationclick należałoby użyć takiego kodu:

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

W następnej sekcji pokażemy, jak sprawdzić, czy strona, na którą mamy kierować użytkownika, jest już otwarta. Dzięki temu możemy skupić się na otwartej karcie, zamiast otwierać nowe.

Zaznacz istniejące okno

Jeśli to możliwe, lepiej zaznaczaj okno, zamiast otwierać nowe okno za każdym razem, gdy użytkownik kliknie powiadomienie.

Zanim omówimy, jak to zrobić, warto podkreślić, że jest to możliwe tylko w przypadku stron w Twojej witrynie. Dzieje się tak, ponieważ widzimy tylko otwarte strony należące do naszej witryny. W ten sposób programiści nie będą mogli zobaczyć wszystkich witryn przeglądanych przez użytkowników.

Korzystając z poprzedniego przykładu, zmienimy kod, by sprawdzić, czy plik /demos/notification-examples/example-page.html jest już otwarty.

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

Omówmy kod.

Najpierw analizujemy przykładową stronę przy użyciu interfejsu URL API. Oto ciekawa sztuczka, którą zapamiętał Jeff Posnick. Wywołanie new URL() z obiektem location zwróci bezwzględny adres URL, jeśli przekazany ciąg jest względny (np. / stanie się https://example.com/).

Adres URL jest absolutny, dzięki czemu możemy później dopasować go do adresu URL okna.

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

Pojawi się lista obiektów WindowClient, która jest listą aktualnie otwartych kart i okien. (Pamiętaj, że są to karty tylko dla Twojego źródła).

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

Opcje przekazywane do matchAll informują przeglądarkę, że chcemy wyszukać tylko klienty typu „window” (czyli szukać kart i okien, a potem wykluczyć narzędzia internetowe). includeUncontrolled umożliwia nam wyszukanie wszystkich kart z Twojego źródła, które nie są kontrolowane przez obecny skrypt service worker, tj. skryptu service worker, który uruchamia ten kod. Ogólnie rzecz biorąc, przy wywoływaniu funkcji matchAll() zasada includeUncontrolled powinna zawsze mieć wartość Prawda.

Zwróconą obietnicę rejestrujemy jako promiseChain, abyśmy mogli przekazać ją później do usługi event.waitUntil() i zapewnić jej prawidłowe działanie.

Gdy obietnica matchAll() się zakończy, analizujemy zwrócone klienty okna i porównujemy ich adresy URL z adresem URL, który chcemy otworzyć. Jeśli znajdziemy dopasowanie, koncentrujemy się na kliencie, tak aby zwrócić jego uwagę. Skup się dzięki wywołaniu matchingClient.focus().

Jeśli nie możemy znaleźć pasującego klienta, otworzy się nowe okno, tak samo jak w poprzedniej sekcji.

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

Scalanie powiadomień

Zauważyliśmy, że dodanie tagu do powiadomienia jest równoznaczne z zastępowaniem istniejących powiadomień z tym samym tagiem.

Możesz jednak uzyskać bardziej zaawansowane funkcje, zwijając powiadomienia, korzystając z interfejsu Notifications API. Rozważmy aplikację do obsługi czatu, w której deweloper może chcieć, aby nowe powiadomienie wyświetlało komunikat podobny do tego: „Masz 2 wiadomości od Matta” zamiast tylko najnowszej wiadomości.

Możesz to zrobić albo modyfikować bieżące powiadomienia w inny sposób, korzystając z interfejsu API registration.getNotifications(), który daje dostęp do wszystkich aktualnie widocznych powiadomień z aplikacji internetowej.

Przyjrzyjmy się, w jaki sposób możemy użyć tego interfejsu API do zaimplementowania przykładowego czatu.

Załóżmy, że każde powiadomienie w aplikacji do obsługi czatu zawiera dane, w tym nazwę użytkownika.

W pierwszej kolejności musimy znaleźć otwarte powiadomienia dotyczące użytkownika o konkretnej nazwie. Odczytujemy registration.getNotifications(), zapętlę Ci i sprawdzimy, czy notification.data ma konkretną nazwę użytkownika:

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

Następnym krokiem jest zastąpienie tego powiadomienia nowym.

W tej aplikacji do fałszywych wiadomości będziemy śledzić liczbę nowych wiadomości, dodając liczbę do danych nowych powiadomień i zwiększając ją z każdym nowym powiadomieniem.

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

Jeśli w tej chwili jest wyświetlane powiadomienie, zwiększamy liczbę wiadomości i odpowiednio ustawiamy tytuł powiadomienia oraz treść wiadomości. W przypadku braku powiadomień tworzymy nowe powiadomienie z wartością newMessageCount równą 1.

W rezultacie pierwsza wiadomość będzie wyglądać tak:

Pierwsze powiadomienie bez scalania.

Drugie powiadomienie zwinęłoby powiadomienia do takiej postaci:

Drugie powiadomienie dotyczące scalania.

Zaletą tej metody jest to, że jeśli użytkownik zobaczy powiadomienia pojawiające się jedno nad drugim, będą one wyglądać bardziej spójnie niż tylko zastąpienie powiadomienia najnowszą wiadomością.

Wyjątek od reguły

Według naszych informacji musisz pokazać powiadomienie, gdy otrzymasz wypchnięcie, i jest to w większości przypadków. Jedyny scenariusz, w którym powiadomienie nie jest konieczne, to gdy użytkownik otwiera i zaznacza witrynę.

W wydarzeniu push możesz sprawdzić, czy musisz wyświetlać powiadomienie, sprawdzając klienty okien i wyszukując wyróżnione okno.

Kod służący do wyświetlania wszystkich okien i wyszukiwania aktywnego okna wygląda tak:

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

Używamy metody clients.matchAll(), aby pobrać wszystkie klienty okien, a następnie najeżdżamy na nie, sprawdzając parametr focused.

W ramach zdarzenia push użyjemy tej funkcji do określenia, czy musimy wyświetlić powiadomienie:

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

Wysyłanie wiadomości do strony ze zdarzenia push

Zauważyliśmy, że możesz pominąć wyświetlanie powiadomienia, jeśli użytkownik znajduje się obecnie w Twojej witrynie. Co jednak, jeśli nadal chcesz informować użytkownika o wydarzeniu, a powiadomienie nie jest zbyt ciężkie?

Jednym ze sposobów jest wysłanie wiadomości z skryptu service worker na stronę. Dzięki temu strona internetowa może wyświetlić użytkownikowi powiadomienie lub aktualizację z informacją o zdarzeniu. Jest to przydatne, gdy subtelne powiadomienie na stronie jest bardziej przyjazne dla użytkownika.

Powiedzmy, że otrzymaliśmy wiadomość, sprawdziliśmy, że nasza aplikacja internetowa jest obecnie aktywna, możemy „opublikować wiadomość” na każdej otwartej stronie w następujący sposób:

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

Na każdej ze stron nasłuchujemy wiadomości, dodając odbiornik:

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

W tym odbiorniku możesz zrobić, co tylko chcesz, wyświetlić niestandardowy interfejs na stronie lub całkowicie zignorować wiadomość.

Warto też pamiętać, że jeśli nie zdefiniujesz odbiornika wiadomości na swojej stronie internetowej, wiadomości z skryptu service worker nie będą nic robić.

Zapisywanie strony w pamięci podręcznej i otwieranie okna

Jednym ze scenariuszy, który nie mieści się w zakresie tego przewodnika, ale warto o nim porozmawiać, jest poprawa ogólnego komfortu korzystania z aplikacji internetowej przez zapisywanie w pamięci podręcznej stron internetowych, które użytkownicy powinni odwiedzić po kliknięciu powiadomienia.

Wymaga to skonfigurowania skryptu service worker do obsługi zdarzeń fetch. Jeśli jednak zaimplementujesz detektor zdarzeń fetch, wykorzystaj go w zdarzeniu push, umieszczając stronę i zasoby potrzebne do wyświetlenia powiadomienia w pamięci podręcznej.

Zgodność z przeglądarką

Zdarzenie notificationclose

Obsługa przeglądarek

  • 50
  • 17
  • 44
  • 16

Źródło

Clients.openWindow()

Obsługa przeglądarek

  • 40
  • 17
  • 44
  • 11.1

Źródło

ServiceWorkerRegistration.getNotifications()

Obsługa przeglądarek

  • 40
  • 17
  • 44
  • 16

Źródło

clients.matchAll()

Obsługa przeglądarek

  • 42
  • 17
  • 54
  • 11.1

Źródło

Więcej informacji znajdziesz we wprowadzeniu do mechanizmów Service Worker.

Co dalej

Laboratoria kodu