Padrões comuns de notificação

Veremos alguns padrões comuns de implementação de push na Web.

Isso envolve o uso de algumas APIs diferentes disponíveis no service worker.

Na última seção, mostramos como detectar eventos notificationclick.

Há também um evento notificationclose que é chamado se o usuário dispensa uma das suas notificações, ou seja, em vez de clicar na notificação, o usuário clica na cruz ou desliza a notificação para fora.

Esse evento normalmente é usado para que a análise de marketing acompanhe o engajamento do usuário com as notificações.

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

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

Como adicionar dados a uma notificação

Quando uma mensagem push é recebida, é comum que os dados sejam úteis apenas se o usuário clicar na notificação. Por exemplo, o URL que será aberto quando uma notificação for clicada.

A maneira mais fácil de coletar dados de um evento push e anexá-los a uma notificação é adicionar um parâmetro data ao objeto de opções transmitido para showNotification(), assim:

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

Em um gerenciador de cliques, os dados podem ser acessados com 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 uma janela

Uma das respostas mais comuns a uma notificação é abrir uma janela / guia para um URL específico. Isso pode ser feito com a API clients.openWindow().

No evento notificationclick, executamos um código como este:

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

Na próxima seção, veremos como verificar se a página para onde queremos direcionar o usuário já está aberta ou não. Dessa forma, podemos focar a guia aberta em vez de abrir novas guias.

Focar uma janela

Sempre que possível, foque uma janela em vez de abrir uma nova janela sempre que o usuário clicar em uma notificação.

Antes de saber como fazer isso, é importante destacar que isso é possível apenas para páginas na sua origem. Isso ocorre porque só podemos ver quais páginas estão abertas pertencem ao nosso site. Isso impede que os desenvolvedores acessem todos os sites que os usuários estão visualizando.

Usando o exemplo anterior, vamos alterar o código para ver se /demos/notification-examples/example-page.html já está aberto.

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

Vamos analisar o código.

Primeiro, analisamos nossa página de exemplo usando a API URL. Esse é um truque interessante que aprendi de Jeff Posnick. Chamar new URL() com o objeto location vai retornar um URL absoluto se a string transmitida for relativa (ou seja, / vai se tornar https://example.com/).

Tornamos o URL absoluto para que possamos fazer a correspondência com os URLs da janela mais tarde.

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

Em seguida, recebemos uma lista dos objetos WindowClient, que é a lista das guias e janelas abertas no momento. Essas são as guias apenas para sua origem.

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

As opções transmitidas para matchAll informam ao navegador que só queremos procurar clientes do tipo "janela" (ou seja, procurar apenas guias e janelas e excluir workers da Web). includeUncontrolled permite pesquisar todas as guias da sua origem que não são controladas pelo service worker atual, ou seja, o service worker que executa esse código. Em geral, é sempre melhor que includeUncontrolled seja verdadeiro ao chamar matchAll().

Capturamos a promessa retornada como promiseChain para que possamos transmiti-la para event.waitUntil() mais tarde, mantendo nosso service worker ativo.

Quando a promessa matchAll() é resolvida, iteramos os clientes de janela retornados e comparamos os URLs deles com o URL que queremos abrir. Quando encontramos uma correspondência, focamos o cliente, e isso chama a atenção dos usuários. O foco é feito com a chamada matchingClient.focus().

Se não conseguirmos encontrar um cliente correspondente, abriremos uma nova janela, como na seção 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);
  }
});

Mesclar notificações

Percebemos que adicionar uma tag a uma notificação ativa um comportamento em que qualquer notificação existente com a mesma tag é substituída.

No entanto, você pode ficar mais sofisticado com o recolhimento de notificações usando a API Notifications. Considere um app de chat em que o desenvolvedor queira que uma nova notificação mostre uma mensagem semelhante a "Você tem duas mensagens de Matt" em vez de mostrar apenas a mensagem mais recente.

Você pode fazer isso ou manipular as notificações atuais de outras maneiras usando a API registration.getNotifications(), que dá acesso a todas as notificações visíveis do seu app da Web.

Vamos conferir como usar essa API para implementar o exemplo de chat.

No nosso app de chat, vamos supor que cada notificação tenha alguns dados, incluindo um nome de usuário.

A primeira coisa que queremos fazer é encontrar quaisquer notificações abertas para um usuário com um nome de usuário específico. Vamos receber registration.getNotifications(), fazer um loop sobre eles e verificar o notification.data para um nome de usuário 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;
});

A próxima etapa é substituir essa notificação por uma nova.

Neste app de mensagens falsas, vamos acompanhar o número de novas mensagens adicionando uma contagem aos dados da nova notificação e incrementá-la a cada nova notificação.

.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 houver uma notificação em exibição no momento, incrementamos a contagem de mensagens e definimos o título e o corpo da mensagem de acordo. Se não houver notificações, criaremos uma nova notificação com um newMessageCount de 1.

O resultado é que a primeira mensagem fica assim:

Primeira notificação sem mesclagem.

Uma segunda notificação reduziria as notificações da seguinte forma:

Segunda notificação com mesclagem.

O bom dessa abordagem é que, se o usuário testemunhar as notificações aparecendo uma sobre a outra, elas terão uma aparência mais coesa do que apenas substituir a notificação pela mensagem mais recente.

A exceção à regra

Eu tenho dito que você precisa mostrar uma notificação ao receber um push, e isso é verdade na maioria das vezes. O único cenário em que você não precisa mostrar uma notificação é quando o usuário está com seu site aberto e focado.

No seu evento push, é possível verificar se você precisa mostrar uma notificação ou não analisando os clientes de janela e procurando uma janela em foco.

O código para buscar todas as janelas e procurar uma janela em foco tem esta aparência:

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 receber todos os clientes de janela e, em seguida, os percorremos verificando o parâmetro focused.

No evento push, usamos essa função para decidir se precisamos mostrar uma notificação:

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 mensagens para uma página usando um evento push

Notamos que é possível pular a exibição de uma notificação se o usuário estiver no seu site. Mas e se você ainda quiser informar ao usuário que um evento ocorreu, mas uma notificação for muito pesada?

Uma abordagem é enviar uma mensagem do service worker para a página, dessa maneira, a página da Web pode mostrar uma notificação ou atualização para o usuário, informando sobre o evento. Isso é útil para situações em que uma notificação sutil na página é melhor e mais amigável para o usuário.

Digamos que recebemos um envio, verificamos se nosso app da Web está em foco no momento e, em seguida, podemos "postar uma mensagem" em cada página aberta, assim:

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

Em cada uma das páginas, detectamos mensagens adicionando um listener de evento de mensagem:

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

Nesse listener de mensagem, você pode fazer o que quiser, mostrar uma IU personalizada na página ou ignorar completamente a mensagem.

Também é importante observar que, se você não definir um listener de mensagens na sua página da Web, as mensagens do service worker não vão fazer nada.

Armazenar uma página em cache e abrir uma janela

Um cenário que está fora do escopo deste guia, mas que vale a pena discutir, é que você pode melhorar a UX geral do seu app da Web armazenando em cache as páginas da Web que você espera que os usuários acessem depois de clicar na notificação.

Para isso, é necessário configurar o worker do serviço para processar eventos fetch. No entanto, se você implementar um listener de evento fetch, aproveite essa configuração no evento push armazenando em cache a página e os recursos necessários antes de mostrar a notificação.

Compatibilidade com navegadores

O evento notificationclose

Compatibilidade com navegadores

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

Origem

Clients.openWindow()

Compatibilidade com navegadores

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

Origem

ServiceWorkerRegistration.getNotifications()

Compatibilidade com navegadores

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

Origem

clients.matchAll()

Compatibilidade com navegadores

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

Origem

Para mais informações, confira este post de introdução aos service workers.

A seguir

Laboratórios de códigos