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 usługach workera.

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

Po otrzymaniu wiadomości push zwykle mamy do czynienia z danymi, które są przydatne tylko wtedy, gdy użytkownik kliknie 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);

Wewnątrz modułu obsługi kliknięć dane są dostępne 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 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 pokażemy, jak sprawdzić, czy strona, na którą chcemy przekierować użytkownika, jest już otwarta. Dzięki temu możemy skupić się na otwartej karcie, zamiast otwierać nowe karty.

Ustawienie fokusa na istniejącym oknie

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

Przyjrzyjmy się kodowi.

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 jest względny (np. / 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). includeUncontrolled pozwala nam wyszukiwać wszystkie karty z Twojego źródła, które nie są kontrolowane przez bieżący skrypt service worker, czyli skrypt service worker, który wykonuje 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 utrzymać naszego pracownika usługi.

Gdy obietnica matchAll() zostanie spełniona, przejdziemy przez zwrócone okna klientów i porównamy ich adresy URL z adresem URL, który chcemy otworzyć. Jeśli znajdziemy dopasowanie, skupimy się na tym kliencie, co zwróci uwagę użytkowników na to okno. Skupienie uwagi odbywa się podczas połączenia 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);
  }
});

Łączenie powiadomień

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

Możesz jednak bardziej szczegółowo zamykać powiadomienia 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 za pomocą 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. Uzyskamy registration.getNotifications() i przejdziemy przez nie, sprawdzając notification.data dla konkretnej nazwy 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 żadnych powiadomień, tworzymy nowe powiadomienie z wartością newMessageCount 1.

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

Pierwsze powiadomienie bez łączenia.

Drugie powiadomienie zwinie powiadomienia do tego:

Drugie powiadomienie z połączonymi kontami.

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

Uważam, że musisz wyświetlać powiadomienie, gdy otrzymujesz powiadomienie push. Jest to w większości przypadków prawdą. Jedynym scenariuszem, w którym nie musisz wyświetlać powiadomienia, jest sytuacja, gdy użytkownik ma otwartą i aktywnie używaną Twoją witrynę.

W zdarzeniu push możesz sprawdzić, czy musisz wyświetlić powiadomienie, czy nie. Aby to zrobić, sprawdź klientów okna i poszukaj okna aktywnego.

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 zrobić, jeśli chcesz poinformować użytkownika o wystąpieniu zdarzenia, ale powiadomienie jest zbyt drastyczne?

Jednym z podejść jest wysłanie wiadomości z workera usługi do strony. Dzięki temu strona internetowa może wyświetlić użytkownikowi powiadomienie lub wiadomość z informacją 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 odbiornik 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, 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ć.

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

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 usługi dla robota, aby obsługiwała zdarzenia fetch. Jeśli zaimplementujesz 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 usług działających w tle.

Co dalej

Code labs