Patrones de notificaciones comunes

Veamos algunos patrones de implementación comunes para el envío web.

Para ello, deberás usar algunas APIs diferentes que están disponibles en el service worker.

Evento de cierre de notificación

En la última sección, vimos cómo se pueden escuchar los eventos notificationclick.

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

Por lo general, este evento se usa para realizar un seguimiento de la participación del usuario con las notificaciones con fines estadísticos.

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

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

Agrega 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 manera 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('');

Abrir una ventana

Una de las respuestas más comunes a una notificación es abrir una ventana o pestaña a 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.

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 páginas del origen. Esto se debe a que solo podemos ver qué páginas están abiertas y que pertenecen a nuestro sitio. Esto impide 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. Es un buen truco que adquirí 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 poder compararla con las URL 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 abiertas actualmente. (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 informan al navegador que solo queremos buscar clientes de tipo “window” (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. En general, siempre querrás que includeUncontrolled sea verdadero cuando llames a matchAll().

Capturamos la promesa que se muestra como promiseChain para poder pasarla más adelante a event.waitUntil() y mantener activo nuestro service worker.

Cuando se resuelve la promesa matchAll(), iteramos por los clientes de ventana que se muestran y comparamos sus URLs con la URL que queremos abrir. Si encontramos una coincidencia, enfocamos ese cliente, lo que atraerá la atención del usuario a esa ventana. La concentración se hace con la llamada matchingClient.focus().

Si no podemos encontrar un cliente que coincida, abriremos una ventana nueva, como 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);
  }
});

Combinación de notificaciones

Vimos 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 ser más sofisticado con la contracción de las notificaciones mediante la API de Notifications. 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 último mensaje.

Puedes hacerlo 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 debemos hacer es buscar las notificaciones abiertas de un usuario con un nombre de usuario específico. Obtendremos registration.getNotifications(), los aplicaremos indefinidamente y buscaremos un nombre de usuario específico en 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;
});

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

En esta app de mensajes falsos, agregaremos un recuento a los datos de nuestra nueva notificación y realizaremos un seguimiento de la cantidad de mensajes nuevos y lo aumentaremos 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 se muestra una notificación en ese momento, aumentamos el recuento de mensajes y configuramos el título de la notificación y el mensaje del cuerpo según corresponda. Si no hay notificaciones, crearemos una notificación nueva con un newMessageCount de 1.

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

Primera notificación sin combinar

Si lo haces, las notificaciones se contraerán de la siguiente manera:

Segunda notificación con combinación.

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

Excepción a la regla

Indicé que debes mostrar una notificación cuando recibes un mensaje push, y esto es verdadero 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 del evento push, puedes verificar si necesitas mostrar una notificación o no si examinas los clientes de ventana y buscas una ventana enfocada.

El código para obtener todas las ventanas y buscar una ventana enfocada tiene el siguiente aspecto:

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, realizamos un bucle sobre ellos para verificar el parámetro focused.

Dentro de nuestro evento push, usaríamos 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);

Enviar un mensaje a una página desde un evento push

Notamos que puedes omitir mostrar una notificación si el usuario se encuentra en el sitio. Pero ¿qué sucede si aún deseas informar al usuario que se produjo un evento, pero una notificación es demasiado pesada?

Un método consiste en enviar un mensaje desde el service worker a la página, de esta manera, la página web puede mostrar una notificación o una actualización al usuario para informarle del evento. Esto es útil en situaciones en las que una notificación sutil en la página es mejor y más fácil de usar para el usuario.

Supongamos que recibimos un mensaje, verificamos que la aplicación web está enfocada y 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 escuchar los mensajes:

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 tener en cuenta que, si no defines un objeto de escucha de mensajes en tu página web, los mensajes del service worker no harán nada.

Almacenar una página en caché 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 si almacenas en caché las páginas web que esperas que visiten los usuarios después de hacer clic en tu notificación.

Esto requiere que tu service worker esté configurado para controlar los 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 tu notificación.

Compatibilidad del navegador

El evento notificationclose

Navegadores compatibles

  • 50
  • 17
  • 44
  • 16

Origen

Clients.openWindow()

Navegadores compatibles

  • 40
  • 17
  • 44
  • 11.1

Origen

ServiceWorkerRegistration.getNotifications()

Navegadores compatibles

  • 40
  • 17
  • 44
  • 16

Origen

clients.matchAll()

Navegadores compatibles

  • 42
  • 17
  • 54
  • 11.1

Origen

Para obtener más información, consulta esta publicación de introducción a los service workers.

Próximos pasos

Code labs