Pattern di notifica comuni

Matt Gaunt

Esamineremo alcuni modelli di implementazione comuni per il web push.

Ciò comporterà l'utilizzo di alcune API diverse disponibili nel service worker.

Evento di chiusura delle notifiche

Nell'ultima sezione abbiamo visto come possiamo ascoltare gli eventi notificationclick.

Esiste anche un evento notificationclose che viene chiamato se l'utente ignora una delle notifiche (ovvero, l'utente fa clic sulla croce o fa scorrere la notifica per eliminarla, anziché fare clic sulla notifica).

Questo evento viene in genere utilizzato per le analisi al fine di monitorare il coinvolgimento degli utenti con le notifiche.

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

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

Aggiunta di dati a una notifica

Alla ricezione di un messaggio push è normale che i dati siano utili solo se l'utente ha fatto clic sulla notifica. Ad esempio, l'URL da aprire quando si fa clic su una notifica.

Il modo più semplice per recuperare i dati da un evento push e collegarli a una notifica è aggiungere un parametro data all'oggetto opzioni passato in showNotification(), in questo modo:

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

All'interno di un gestore di clic, è possibile accedere ai dati con 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('');

Aprire una finestra

Una delle risposte più comuni a una notifica è l'apertura di una finestra o di una scheda che rimanda a un URL specifico. Possiamo farlo con l'API clients.openWindow().

Nel nostro evento notificationclick eseguiremmo un codice simile al seguente:

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

Nella prossima sezione vedremo come verificare se la pagina a cui vogliamo indirizzare l'utente è già aperta o meno. In questo modo possiamo impostare lo stato attivo sulla scheda aperta anziché aprire nuove schede.

Imposta lo stato attivo su una finestra esistente

Quando è possibile, dobbiamo impostare lo stato attivo su una finestra anziché aprirne una nuova ogni volta che l'utente fa clic su una notifica.

Prima di vedere come fare, è bene sottolineare che ciò è possibile solo per le pagine nella tua origine. Questo perché possiamo vedere solo quali pagine sono aperte che appartengono al nostro sito. In questo modo, gli sviluppatori non sono in grado di vedere tutti i siti visitati dagli utenti.

Prendendo l'esempio precedente, modificheremo il codice per vedere se /demos/notification-examples/example-page.html è già aperto.

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

Esaminiamo il codice.

Innanzitutto analizziamo la nostra pagina di esempio utilizzando l'API URL. Questo è un bel trucco che ho preso da Jeff Posnick. La chiamata a new URL() con l'oggetto location restituirà un URL assoluto se la stringa trasmessa è relativa (ad esempio / diventerà https://example.com/).

Rendiamo l'URL assoluto in modo da poterlo abbinare all'URL della finestra in un secondo momento.

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

Poi otteniamo un elenco di WindowClient oggetti, ovvero l'elenco delle schede e delle finestre attualmente aperte. Ricorda che queste schede riguardano soltanto la tua origine.

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

Le opzioni trasmesse a matchAll comunicano al browser che vogliamo cercare solo client di tipo "finestra" (ovvero, devono solo cercare schede e finestre ed escludere i web worker). includeUncontrolled ci consente di cercare tutte le schede della tua origine che non sono controllate dal service worker attuale, ovvero il service worker che esegue questo codice. In genere, è consigliabile sempre che il criterio includeUncontrolled sia true quando chiami matchAll().

Catturiamo la promessa restituita come promiseChain per poterla trasmettere in event.waitUntil() in seguito, mantenendo attivo il nostro service worker.

Quando la promessa matchAll() viene risolta, eseguiamo l'iterazione dei client delle finestre restituiti e confrontiamo i relativi URL con l'URL che vogliamo aprire. Se troviamo una corrispondenza, lo concentriamo sul client, in modo da portare la finestra all'attenzione degli utenti. La messa a fuoco viene eseguita con la chiamata matchingClient.focus().

Se non riusciamo a trovare un cliente corrispondente, apriamo una nuova finestra, come nella sezione precedente.

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

Unione delle notifiche

Abbiamo notato che l'aggiunta di un tag a una notifica attiva un comportamento in cui qualsiasi notifica esistente con lo stesso tag viene sostituita.

Tuttavia, puoi migliorare il livello di dettaglio con la compressione delle notifiche utilizzando l'API Notifications. Prendiamo in considerazione un'app di chat in cui lo sviluppatore potrebbe volere che una nuova notifica mostri un messaggio simile a "Hai due messaggi di Matt" anziché mostrare solo il messaggio più recente.

Puoi farlo o modificare le notifiche correnti in altri modi utilizzando l'API registration.getNotifications() che ti consente di accedere a tutte le notifiche attualmente visibili per la tua app web.

Vediamo come possiamo utilizzare questa API per implementare l'esempio di chat.

Nella nostra app di chat, supponiamo che ogni notifica contenga dei dati, tra cui un nome utente.

La prima cosa da fare è trovare eventuali notifiche aperte per un utente con un nome utente specifico. Esamineremo registration.getNotifications() e controlleremo in loop un nome utente specifico nel campo 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;
});

Il passaggio successivo consiste nel sostituire questa notifica con una nuova.

In questa app di messaggi falsi, terremo traccia del numero di nuovi messaggi aggiungendo un conteggio ai dati della nuova notifica e lo incrementeremo a ogni nuova notifica.

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

Se al momento è visualizzata una notifica, aumentiamo il numero dei messaggi e impostiamo il titolo e il corpo del messaggio della notifica di conseguenza. Se non ci sono notifiche, ne creiamo una nuova con un valore di newMessageCount pari a 1.

Il risultato è che il primo messaggio sarà simile a questo:

Prima notifica senza unire.

Una seconda notifica comprimerà le notifiche in questo modo:

Seconda notifica con unione.

L'aspetto positivo di questo approccio è che, se l'utente vede le notifiche una sull'altra, l'aspetto sarà più coeso della semplice sostituzione della notifica con il messaggio più recente.

Eccezione alla regola

Come spiegato in precedenza, devi mostrare una notifica quando ricevi un push, cosa che accade nella maggior parte delle volte. L'unico caso in cui non è necessario mostrare una notifica è quando l'utente ha il sito aperto e attivo.

All'interno dell'evento push, puoi verificare se devi mostrare o meno una notifica esaminando i client delle finestre e cercando una finestra attiva.

Il codice per accedere a tutte le finestre e cercare una finestra attiva ha il seguente aspetto:

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

Utilizziamo clients.matchAll() per recuperare tutti i nostri client finestra, quindi li controlliamo in loop controllando il parametro focused.

All'interno dell'evento push, utilizzeremmo questa funzione per decidere se dobbiamo mostrare una notifica:

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

Invia un messaggio a una pagina da un evento push

Abbiamo notato che puoi saltare la visualizzazione di una notifica se l'utente si trova attualmente sul tuo sito. E se volessi far sapere all'utente che si è verificato un evento, ma che la notifica è troppo pesante?

Un approccio consiste nell'inviare un messaggio dal service worker alla pagina, in questo modo la pagina web può mostrare una notifica o un aggiornamento all'utente per informarlo dell'evento. Questo è utile nei casi in cui una notifica sottile nella pagina è migliore e più amichevole per l'utente.

Supponiamo di aver ricevuto un push e di aver verificato che la nostra app web sia attiva, quindi possiamo "pubblicare un messaggio" in ogni pagina aperta, in questo modo:

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

In ogni pagina, ascoltiamo i messaggi aggiungendo un apposito elenco per gli eventi:

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

In questo listener di messaggi, puoi fare tutto ciò che vuoi, mostrare un'interfaccia utente personalizzata sulla pagina o ignorare completamente il messaggio.

È inoltre opportuno notare che se non definisci un listener di messaggi nella pagina web, i messaggi del service worker non avranno effetto.

Memorizzare una pagina nella cache e aprire una finestra

Uno scenario, che non rientra nell'ambito di questa guida, ma che vale la pena discutere, è che puoi migliorare l'esperienza utente complessiva della tua app web memorizzando nella cache le pagine web che ti aspetti vengano visitate dagli utenti dopo aver fatto clic sulla notifica.

Ciò richiede la configurazione del service worker per gestire gli eventi fetch, ma se implementi un listener di eventi fetch, assicurati di sfruttarlo nel tuo evento push memorizzando nella cache la pagina e gli asset necessari prima di mostrare la notifica.

Compatibilità del browser

L'evento notificationclose

Supporto dei browser

  • 50
  • 17
  • 44
  • 16

Fonte

Clients.openWindow()

Supporto dei browser

  • 40
  • 17
  • 44
  • 11.1

Fonte

ServiceWorkerRegistration.getNotifications()

Supporto dei browser

  • 40
  • 17
  • 44
  • 16

Fonte

clients.matchAll()

Supporto dei browser

  • 42
  • 17
  • 54
  • 11.1

Fonte

Per ulteriori informazioni, consulta questo post introduttivo ai service worker.

Passaggi successivi

Codelab