Pattern di notifica comuni

Matt Gaunt

Esamineremo alcuni pattern di implementazione comuni per le notifiche push web.

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

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

Viene richiamato anche un evento notificationclose se l'utente ignora una delle tue notifiche (ovvero, anziché fare clic sulla notifica, l'utente fa clic sulla croce o fa scorrere la notifica per nasconderla).

Questo evento viene normalmente utilizzato per l'analisi per 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

Quando viene ricevuto un messaggio push, è normale avere dati utili solo se l'utente ha fatto clic sulla notifica. Ad esempio, l'URL che deve essere aperto quando viene fatto clic su una notifica.

Il modo più semplice per estrarre i dati da un evento push e associarli a una notifica è aggiungere un parametro data all'oggetto options passato a showNotification(), come segue:

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 dei 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 è aprire una finestra/scheda con un URL specifico. Possiamo farlo con l'API clients.openWindow().

Nell'evento notificationclick, eseguiamo un codice simile al seguente:

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

Nella sezione successiva 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é aprirne di nuove.

Mettere in primo piano una finestra esistente

Se possibile, dobbiamo mettere in primo piano una finestra anziché aprirne una nuova ogni volta che l'utente fa clic su una notifica.

Prima di esaminare come ottenere questo risultato, è opportuno sottolineare che questa operazione è possibile solo per le pagine della tua origine. Questo perché possiamo vedere solo le pagine aperte che appartengono al nostro sito. In questo modo, gli sviluppatori non possono vedere tutti i siti visualizzati dagli utenti.

Nell'esempio precedente, modificheremo il codice per verificare 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.

Per prima cosa analizziamo la pagina di esempio utilizzando l'API URL. Questo è un bel trucco che ho imparato da Jeff Posnick. La chiamata di new URL() con l'oggetto location restituisce un URL assoluto se la stringa passata è relativa (ad esempio, / diventerà https://example.com/).

Rendiamo l'URL assoluto in modo da poterlo associare in un secondo momento agli URL delle finestre.

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

Poi otteniamo un elenco di oggetti WindowClient, ovvero l'elenco delle schede e delle finestre attualmente aperte. Ricorda che si tratta di schede solo per la tua origine.

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

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

Catturiamo la promessa restituita come promiseChain in modo da poterla passare in seguito a event.waitUntil(), mantenendo attivo il nostro service worker.

Una volta risolta la promessa matchAll(), eseguiamo l'iterazione tramite i client finestra restituiti e confrontiamo i loro URL con l'URL che vogliamo aprire. Se viene trovata una corrispondenza, viene visualizzato il client corrispondente, che attira l'attenzione degli utenti sulla finestra. L'attenzione 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);
  }
});

Unisci 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 ottenere risultati più sofisticati con il collasso delle notifiche utilizzando l'API Notifications. Prendiamo ad esempio un'app di chat, in cui lo sviluppatore potrebbe volere che una nuova notifica mostri un messaggio simile a "Hai due messaggi da Matt" anziché solo l'ultimo messaggio.

Puoi farlo o manipolare le notifiche attuali 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 alcuni dati che includono un nome utente.

La prima cosa da fare è trovare eventuali notifiche aperte per un utente con un nome utente specifico. Recupereremo registration.getNotifications(), li eseguiremo in loop e controlleremo notification.data per un nome utente specifico:

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 falsa, 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 di conseguenza. Se non ci sono notifiche, ne creiamo una nuova con newMessageCount pari a 1.

Il risultato è che il primo messaggio avrà il seguente aspetto:

Prima notifica senza unione.

Una seconda notifica comprimeva le notifiche in questa:

Seconda notifica con unione.

Il vantaggio di questo approccio è che, se l'utente vede le notifiche visualizzate una sopra l'altra, l'esperienza sarà più coerente rispetto alla semplice sostituzione della notifica con l'ultimo messaggio.

L'eccezione alla regola

Ho affermato che devi mostrare una notifica quando ricevi una notifica push e questo è vero la maggior parte delle volte. L'unico caso in cui non devi mostrare una notifica è quando l'utente ha il tuo sito aperto e attivo.

All'interno dell'evento push, puoi verificare se è necessario mostrare una notifica esaminando i client della finestra e cercando una finestra attiva.

Il codice per recuperare tutte le finestre e cercarne una con lo stato attivo è simile al seguente:

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 ottenere tutti i nostri client di finestra e poi li esaminiamo controllando il parametro focused.

All'interno dell'evento push, utilizzeremo questa funzione per decidere se 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);

Inviare un messaggio a una pagina da un evento push

Abbiamo notato che puoi saltare la visualizzazione di una notifica se l'utente è attualmente sul tuo sito. Ma cosa succede se vuoi comunque informare l'utente che si è verificato un evento, ma una notifica è troppo invasiva?

Un approccio è inviare un messaggio dal service worker alla pagina, in modo che la pagina web possa mostrare una notifica o un aggiornamento all'utente, per informarlo dell'evento. Questa opzione è utile per le situazioni in cui una notifica discreta nella pagina è migliore e più intuitiva per l'utente.

Supponiamo di aver ricevuto una notifica push e di aver verificato che la nostra app web sia attualmente attiva. Possiamo quindi "pubblicare un messaggio" in ogni pagina aperta, come segue:

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 listener di eventi di messaggio:

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

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

Vale anche la pena notare che se non definisci un ascoltatore di messaggi nella tua pagina web, i messaggi del servizio worker non faranno nulla.

Memorizzare nella cache una pagina e aprire una finestra

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

Per farlo, devi configurare il tuo worker di servizio per gestire gli eventi fetch, ma se implementi un ascoltatore di eventi fetch, assicurati di sfruttarlo nell'evento fetch memorizzando nella cache la pagina e gli asset di cui avrai bisogno prima di mostrare la notifica.

Compatibilità del browser

L'evento notificationclose

Supporto dei browser

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

Origine

Clients.openWindow()

Supporto dei browser

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

Origine

ServiceWorkerRegistration.getNotifications()

Supporto dei browser

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

Origine

clients.matchAll()

Supporto dei browser

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

Origine

Per saperne di più, consulta questo post introduttivo ai service worker.

Passaggi successivi

Codelab