Padrões comuns de notificação

Vamos analisar alguns padrões de implementação comuns para push na Web.

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

Evento de fechamento de notificação

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

Também há um evento notificationclose que é chamado se o usuário dispensar uma das notificações. Em vez de clicar na notificação, o usuário clica na cruz ou desliza a notificação.

Esse evento normalmente é usado para que a análise de marketing rastreie 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, vamos conferir como verificar se a página para a qual 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, vale ressaltar que isso é apenas possível para páginas na sua origem. Isso acontece porque só podemos ver as páginas abertas que 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 saber 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 legal que aprendi com 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 pelos clientes de janela retornados e comparamos os URLs deles com o URL que queremos abrir. Se encontrarmos uma correspondência, vamos focar esse cliente, o que vai chamar a atenção dos usuários para essa janela. O foco é feito com a chamada matchingClient.focus().

Se não encontrarmos um cliente correspondente, vamos abrir 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 com a mesma tag é substituída.

No entanto, você pode tornar o processo de redução de notificações mais sofisticado 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 vamos fazer é encontrar 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 exibida no momento, vamos incrementar a contagem de mensagens e definir o título e a mensagem do corpo da notificação 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 agruparia as notificações assim:

Segunda notificação com mesclagem.

A vantagem dessa abordagem é que, se o usuário testemunhar as notificações aparecendo uma sobre a outra, elas vão parecer e se sentir mais coesas 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 quando receber um push, e isso é verdade na maioria dos casos. O único cenário em que você não precisa mostrar uma notificação é quando o usuário tem 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 uma mensagem para uma página usando um evento push

Você pode 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 forma, a página da Web pode mostrar uma notificação ou uma atualização para o usuário, informando-o sobre o evento. Isso é útil em situações em que uma notificação sutil na página é melhor e mais amigável para o usuário.

Digamos que recebemos um push, verificamos se o app da Web está focado 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

Code labs