Typowe wzorce powiadomień

Przyjrzymy się kilku typom implementacji powiadomień web push.

Do tego celu będziesz używać kilku różnych interfejsów API dostępnych w workerze usługi.

Zdarzenie zamknięcia powiadomienia

W poprzedniej sekcji omawialiśmy odsłuchiwanie zdarzeń notificationclick.

Jest też zdarzenie notificationclose, które jest wywoływane, gdy użytkownik zamknie jedno z Twoich powiadomień (czyli zamiast kliknąć powiadomienie, kliknie krzyżyk lub przesunie je w bok).

To zdarzenie jest zwykle używane do celów analitycznych do śledzenia zaangażowania użytkowników w powiadomienia.

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

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

Dodawanie danych do powiadomienia

W przypadku otrzymania wiadomości push dane są zwykle potrzebne tylko wtedy, gdy użytkownik kliknął powiadomienie. Na przykład adres URL, który powinien otwierać się po kliknięciu powiadomienia.

Najprostszym sposobem na pobranie danych z zdarzenia push i dołączenie ich do powiadomienia jest dodanie parametru data do obiektu options przekazywanego do showNotification(), na przykład w ten sposób:

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 obiekcie event.notification.data możesz uzyskać dostęp do danych w obiekcie 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 powiadomienie jest otwarcie okna lub karty z określonym adresem URL. Możemy to zrobić za pomocą interfejsu API clients.openWindow().

W przypadku zdarzenia notificationclick uruchomimy kod w takiej postaci:

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

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

Zaznacz istniejące okno

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

Zanim przejdziemy do omawiania tego, jak to zrobić, warto podkreślić, że jest to możliwe tylko w przypadku stron w Twoim domenie. Dzieje się tak, ponieważ widzimy tylko otwarte strony należące do naszej witryny. Dzięki temu deweloperzy nie widzą wszystkich witryn, które przeglądają użytkownicy.

W tym przykładzie zmodyfikujemy kod, aby sprawdzić, czy element /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);

Przeanalizujmy kod.

Najpierw analizujemy stronę przykładową za pomocą interfejsu URL API. To sprytny trik, który podpatrzyłem u Jeffa Posnicka. Wywołanie funkcji new URL() z obiektem location zwróci adres URL bezwzględny, jeśli przekazany ciąg znaków jest bezwzględny (czyli / stanie się https://example.com/).

Adres URL jest bezwzględny, aby można było później dopasować go do adresów URL okien.

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

Następnie otrzymujemy listę obiektów WindowClient, czyli listę aktualnie otwartych kart i okienek. (Pamiętaj, że te karty dotyczą tylko Twojego źródła).

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

Opcje przekazane do matchAll informują przeglądarkę, że chcemy wyszukiwać tylko klientów typu „okno” (czyli tylko karty i okna oraz wykluczyć procesy web worker). Funkcja includeUncontrolled umożliwia nam wyszukiwanie wszystkich kart z Twojego źródła, które nie są kontrolowane przez bieżący skrypt service worker, tj. skrypt service worker uruchamiający ten kod. Zazwyczaj podczas wywoływania funkcji matchAll() chcesz, aby opcja includeUncontrolled była zawsze ustawiona na wartość true.

Zwracaną obietnicę rejestrujemy jako promiseChain, aby później przekazać ją do event.waitUntil(), co pozwoli zachować naszemu pracownikowi usługi.

Gdy obietnica matchAll() się spełni, powtórzymy zwrócone klienty okna i porównujemy ich adresy URL z adresami URL, które chcemy otworzyć. Jeśli znajdziemy dopasowanie, skupimy się na tym kliencie, co zwróci uwagę użytkowników na to okno. Zaznaczenie odbywa się w wywołaniu matchingClient.focus().

Jeśli nie uda nam się znaleźć pasującego klienta, otworzy się nowe okno, tak 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);
  }
});

Scalam powiadomienia

Zauważyliśmy, że dodanie tagu do powiadomienia powoduje zastąpienie wszystkich dotychczasowych powiadomień z tym samym tagiem.

Możesz jednak bardziej szczegółowo stosować funkcję zwijania powiadomień za pomocą interfejsu Notifications API. Weźmy na przykład aplikację do czatu, w której deweloper może chcieć, aby nowe powiadomienie zawierało komunikat podobny do „Masz 2 wiadomości od Matta” zamiast tylko wyświetlać najnowszą wiadomość.

Możesz to zrobić lub manipulować bieżącymi powiadomieniami w inny sposób, korzystając z interfejsu registration.getNotifications() API, który zapewnia dostęp do wszystkich aktualnie widocznych powiadomień w aplikacji internetowej.

Zobaczmy, jak można użyć tego interfejsu API do implementacji przykładowego czatu.

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

Najpierw chcemy znaleźć otwarte powiadomienia dla użytkownika o konkretnym nicku. Pobierzemy registration.getNotifications() i zapętlimy je, aby sprawdzić, czy w polu notification.data znajduje się konkretna nazwa 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;
});

Kolejnym krokiem jest zastąpienie tego powiadomienia nowym.

W tej fałszywej aplikacji do wysyłania wiadomości będziemy śledzić liczbę nowych wiadomości, dodając do danych dotyczących nowych powiadomień nową liczbę i zwiększając ją o 1 przy każdej nowej wiadomości.

.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 jest wyświetlane powiadomienie, zwiększamy liczbę wiadomości i odpowiednio ustawiamy tytuł oraz treść powiadomienia. Jeśli nie ma powiadomień, tworzymy nowe powiadomienie z ustawą newMessageCount o wartości 1.

W rezultacie pierwszy komunikat będzie wyglądał tak:

Pierwsze powiadomienie bez scalania.

Drugie powiadomienie zwinęłoby je w następujący sposób:

Drugie powiadomienie o scalaniu.

Zaletą tego podejścia jest to, że jeśli użytkownik zobaczy, że powiadomienia wyświetlają się jedno po drugim, będzie to wyglądać bardziej spójnie niż zastąpienie powiadomienia najnowszą wiadomością.

Wyjątek od reguły

Moim zdaniem musisz pokazywać powiadomienia, gdy otrzymasz powiadomienie, a to prawda w większości przypadków. Jednym z przykładów sytuacji, w którym nie musisz pokazywać powiadomienia, jest sytuacja, w której użytkownik jest otwarty i zaznaczony.

W wydarzeniu push możesz sprawdzić, czy musisz wyświetlić powiadomienie, przeglądając klienty okien i wyszukując zaznaczone okno.

Kod służący do uzyskiwania wszystkich okien i wyszukiwania okna z fokusem 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 parametru clients.matchAll(), aby uzyskać wszystkich klientów okna, a potem sprawdzamy parametr focused.

W przypadku zdarzenia push używamy tej funkcji, aby zdecydować, czy 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 z wykorzystaniem zdarzenia push

Zauważyliśmy, że możesz pominąć wyświetlanie powiadomienia, jeśli użytkownik jest obecnie w Twojej witrynie. Co jednak w sytuacji, gdy nadal chcesz poinformować użytkownika o wystąpieniu zdarzenia, ale powiadomienie jest zbyt ciężkie?

Jedną z metod jest wysłanie wiadomości z skryptu service worker na stronę, dzięki czemu strona internetowa może wyświetlać użytkownikowi powiadomienie lub aktualizację informującą go o zdarzeniu. Jest to przydatne w sytuacjach, gdy subtelne powiadomienie na stronie jest lepsze i przyjaźniejsze dla użytkownika.

Załóżmy, że otrzymaliśmy powiadomienie push i sprawdziliśmy, że nasza aplikacja internetowa jest aktualnie aktywna. Możemy wtedy „opublikować wiadomość” na każdej otwartej stronie.

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 stronie słuchamy wiadomości, dodając listenera wiadomości:

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

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

Warto też pamiętać, że jeśli na stronie internetowej nie zdefiniujesz odbiornika wiadomości, wiadomości z serwisu workera nie będą działać.

Zapisz stronę w pamięci podręcznej i otwórz okno

Jeden z wydarzeń, które wykracza poza zakres tego poradnika, ale warto o nim wspomnieć, to możliwość poprawy ogólnego UX aplikacji internetowej dzięki umieszczeniu w pamięci podręcznej stron internetowych, które użytkownicy prawdopodobnie odwiedzą po kliknięciu powiadomienia.

Wymaga to skonfigurowania pracownika usługi do obsługi zdarzeń fetch, ale jeśli wdrożesz odbiornik zdarzenia fetch, pamiętaj, aby wykorzystać go w zdarzeniach push, przechowując w pamięci podręcznej stronę i zasoby, których będziesz potrzebować przed wyświetleniem powiadomienia.

Zgodność z przeglądarką

Zdarzenie notificationclose

Obsługa przeglądarek

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

Źródło

Clients.openWindow()

Obsługa przeglądarek

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

Źródło

ServiceWorkerRegistration.getNotifications()

Obsługa przeglądarek

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

Źródło

clients.matchAll()

Obsługa przeglądarek

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

Źródło

Więcej informacji znajdziesz w tym poście wprowadzającym do mechanizmów Service Worker.

Co dalej

Codelabs