Patrones de notificaciones comunes

Analizaremos algunos patrones de implementación comunes para los servicios web push.

Esto implicará usar algunas APIs diferentes que están disponibles en el trabajador de servicio.

Evento de cierre de notificación

En la última sección, vimos cómo podemos detectar eventos notificationclick.

También hay un evento notificationclose al que se llama si el usuario descarta una de tus notificaciones (es decir, en lugar de hacer clic en la notificación, el usuario hace clic en la cruz o desliza la notificación).

Por lo general, este evento se usa para que las estadísticas realicen un seguimiento de la participación de los usuarios con las notificaciones.

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

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

Cómo agregar datos a una notificación

Cuando se recibe un mensaje push, es común tener datos que solo son útiles si el usuario hizo clic en la notificación. Por ejemplo, la URL que se debe abrir cuando se hace clic en una notificación.

La forma más fácil de tomar datos de un evento push y adjuntarlos a una notificación es agregar un parámetro data al objeto de opciones que se pasa a showNotification(), de la siguiente manera:

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

Dentro de un controlador de clics, se puede acceder a los datos 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('');

Cómo abrir una ventana

Una de las respuestas más comunes a una notificación es abrir una ventana o pestaña en una URL específica. Podemos hacerlo con la API de clients.openWindow().

En nuestro evento notificationclick, ejecutaríamos un código como este:

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

En la siguiente sección, veremos cómo verificar si la página a la que queremos dirigir al usuario ya está abierta o no. De esta manera, podemos enfocar la pestaña abierta en lugar de abrir pestañas nuevas.

Cómo enfocar una ventana existente

Cuando sea posible, debemos enfocar una ventana en lugar de abrir una nueva cada vez que el usuario hace clic en una notificación.

Antes de ver cómo lograrlo, vale la pena destacar que esto solo es posible para las páginas de tu origen. Esto se debe a que solo podemos ver las páginas abiertas que pertenecen a nuestro sitio. Esto evita que los desarrolladores puedan ver todos los sitios que ven sus usuarios.

Tomando el ejemplo anterior, modificaremos el código para ver si /demos/notification-examples/example-page.html ya está abierto.

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

Analicemos el código.

Primero, analizamos nuestra página de ejemplo con la API de URL. Este es un truco ingenioso que aprendí de Jeff Posnick. Si llamas a new URL() con el objeto location, se mostrará una URL absoluta si la cadena que se pasa es relativa (es decir, / se convertirá en https://example.com/).

Hacemos que la URL sea absoluta para que podamos hacer coincidirla con las URLs de la ventana más adelante.

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

Luego, obtenemos una lista de los objetos WindowClient, que es la lista de las pestañas y ventanas que están abiertas en ese momento. (Recuerda que estas son pestañas solo para tu origen).

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

Las opciones que se pasan a matchAll le informan al navegador que solo queremos buscar clientes de tipo "ventana" (es decir, solo buscar pestañas y ventanas, y excluir trabajadores web). includeUncontrolled nos permite buscar todas las pestañas de tu origen que no están controladas por el service worker actual, es decir, el service worker que ejecuta este código. Por lo general, siempre querrás que includeUncontrolled sea verdadero cuando llames a matchAll().

Capturamos la promesa que se devuelve como promiseChain para poder pasarla a event.waitUntil() más adelante, lo que mantiene activo nuestro service worker.

Cuando se resuelve la promesa matchAll(), iteramos por los clientes de ventana que se devuelven y comparamos sus URLs con la URL que queremos abrir. Si encontramos una coincidencia, enfocamos ese cliente, lo que llevará esa ventana a la atención de los usuarios. El enfoque se realiza con la llamada matchingClient.focus().

Si no encontramos un cliente coincidente, abrimos una ventana nueva, al igual que en la sección anterior.

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

Cómo combinar notificaciones

Observamos que agregar una etiqueta a una notificación habilita un comportamiento en el que se reemplaza cualquier notificación existente con la misma etiqueta.

Sin embargo, puedes hacer que el colapso de notificaciones sea más sofisticado con la API de notificaciones. Considera una app de chat en la que el desarrollador podría querer que una notificación nueva muestre un mensaje similar a "Tienes dos mensajes de Matt" en lugar de solo mostrar el mensaje más reciente.

Puedes hacer esto o manipular las notificaciones actuales de otras maneras con la API de registration.getNotifications(), que te brinda acceso a todas las notificaciones visibles actualmente de tu app web.

Veamos cómo podríamos usar esta API para implementar el ejemplo de chat.

En nuestra app de chat, supongamos que cada notificación tiene algunos datos que incluyen un nombre de usuario.

Lo primero que haremos será encontrar las notificaciones abiertas de un usuario con un nombre de usuario específico. Obtendremos registration.getNotifications(), haremos un bucle sobre ellos y comprobaremos el notification.data en busca de un nombre de usuario específico:

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

El siguiente paso es reemplazar esta notificación por una nueva.

En esta app de mensajes falsos, haremos un seguimiento de la cantidad de mensajes nuevos agregando un recuento a los datos de nuestra notificación nueva y aumentándolo con cada notificación nueva.

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

Si hay una notificación en exhibición, incrementamos el recuento de mensajes y configuramos el título y el cuerpo de la notificación según corresponda. Si no hay notificaciones, creamos una nueva con un newMessageCount de 1.

El resultado es que el primer mensaje se verá de la siguiente manera:

Primera notificación sin combinación.

Una segunda notificación colapsaría las notificaciones en lo siguiente:

Segunda notificación con combinación.

Lo bueno de este enfoque es que, si el usuario ve que las notificaciones aparecen una sobre la otra, se verá y se sentirá más cohesivo que simplemente reemplazar la notificación por el mensaje más reciente.

La excepción a la regla

He estado diciendo que debes mostrar una notificación cuando recibes un mensaje push, y esto es cierto la mayoría de las veces. La única situación en la que no tienes que mostrar una notificación es cuando el usuario tiene tu sitio abierto y enfocado.

Dentro de tu evento push, puedes verificar si necesitas mostrar una notificación o no. Para ello, examina los clientes de la ventana y busca una ventana enfocada.

El código para obtener todas las ventanas y buscar una ventana enfocada se ve así:

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

Usamos clients.matchAll() para obtener todos nuestros clientes de ventana y, luego, los iteramos para verificar el parámetro focused.

Dentro de nuestro evento push, usaremos esta función para decidir si necesitamos mostrar una notificación:

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

Cómo enviar un mensaje a una página desde un evento push

Vimos que puedes omitir mostrar una notificación si el usuario está en tu sitio. Pero ¿qué sucede si aún quieres informarle al usuario que ocurrió un evento, pero una notificación es demasiado invasiva?

Un enfoque es enviar un mensaje del trabajador de servicio a la página. De esta manera, la página web puede mostrarle al usuario una notificación o una actualización para informarle sobre el evento. Esto es útil en situaciones en las que una notificación sutil en la página es mejor y más amigable para el usuario.

Supongamos que recibimos un mensaje push, verificamos que nuestra app web esté enfocada en este momento y, luego, podemos "publicar un mensaje" en cada página abierta, de la siguiente manera:

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

En cada una de las páginas, agregamos un objeto de escucha de eventos de mensaje para detectarlos:

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

En este objeto de escucha de mensajes, puedes hacer lo que quieras, mostrar una IU personalizada en tu página o ignorar el mensaje por completo.

También vale la pena señalar que, si no defines un objeto de escucha de mensajes en tu página web, los mensajes del servicio trabajador no harán nada.

Cómo almacenar en caché una página y abrir una ventana

Una situación que está fuera del alcance de esta guía, pero que vale la pena analizar, es que puedes mejorar la UX general de tu app web almacenando en caché las páginas web que esperas que los usuarios visiten después de hacer clic en tu notificación.

Esto requiere que tu trabajador del servicio esté configurado para controlar eventos fetch, pero si implementas un objeto de escucha de eventos fetch, asegúrate de aprovecharlo en tu evento push almacenando en caché la página y los recursos que necesitarás antes de mostrar la notificación.

Compatibilidad del navegador

El evento notificationclose

Navegadores compatibles

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

Origen

Clients.openWindow()

Navegadores compatibles

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

Origen

ServiceWorkerRegistration.getNotifications()

Navegadores compatibles

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

Origen

clients.matchAll()

Navegadores compatibles

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

Origen

Para obtener más información, consulta esta entrada de introducción a los trabajadores de servicio.

Próximos pasos

Code labs