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.

Evento de fechamento de notificação

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

Há também um evento notificationclose que é chamado quando 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 na análise para rastrear 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 ter dados que só serão úteis se o usuário tiver clicado na notificação. Por exemplo, o URL que precisa ser aberto quando uma notificação é clicada.

A maneira mais fácil de extrair 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(), desta forma:

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. Podemos fazer isso com a API clients.openWindow().

No evento notificationclick, executaríamos 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 aprender a 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 em uma janela existente

Sempre que possível, é necessário focar em uma janela, em vez de abrir uma nova sempre que o usuário clica em uma notificação.

Antes de mostrar como fazer isso, vale destacar que isso só é possível para páginas na sua origem. Isso acontece porque só podemos ver quais páginas estão abertas e pertencem ao nosso site. Isso impede que os desenvolvedores vejam todos os sites acessados pelos usuários.

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. Este é um truque legal que escolhai 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 compará-lo ao URL 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. Lembre-se de que essas guias são apenas para sua origem.

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

As opções transmitidas para matchAll informam ao navegador que queremos pesquisar apenas clientes do tipo "janela" (ou seja, apenas procurar 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. Geralmente, você sempre quer que includeUncontrolled seja verdadeiro ao chamar matchAll().

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

Quando a promessa matchAll() é resolvida, iteramos os clientes da janela retornada e comparamos os URLs deles com o URL que queremos abrir. Se encontrarmos uma correspondência, focaremos esse cliente, o que chamará a atenção dos usuários para essa janela. O foco é feito com a chamada matchingClient.focus().

Se não for possível encontrar um cliente correspondente, abrimos 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);
  }
});

Mesclagem de notificações

Vimos 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 pode querer uma nova notificação para mostrar uma mensagem semelhante a "Você tem duas mensagens de Matt", em vez de apenas mostrar a mensagem mais recente.

É possível fazer isso ou manipular as notificações atuais de outras maneiras usando a API registration.getNotifications(), que fornece acesso a todas as notificações visíveis do app da Web.

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

Em nosso aplicativo de bate-papo, vamos supor que cada notificação tenha alguns dados, que incluem um nome de usuário.

A primeira coisa que queremos fazer é encontrar todas as notificações abertas para um usuário com um nome de usuário específico. Vamos usar registration.getNotifications(), fazer uma repetição e verificar se notification.data há 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.

Nesse app de mensagens falsas, vamos rastrear o número de novas mensagens adicionando uma contagem aos dados da nova notificação e o incrementaremos 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 uma notificação estiver sendo exibida, vamos aumentar a contagem de mensagens e definir o título da notificação e o corpo da mensagem de acordo. Se não houver notificações, vamos criar uma nova com um newMessageCount de 1.

O resultado é que a primeira mensagem seria assim:

Primeira notificação sem mesclagem.

Uma segunda notificação recolheria as notificações desta forma:

Segunda notificação com mesclagem.

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

A exceção à regra

Eu tenho declarado que você precisa mostrar uma notificação quando recebe um push, e isso acontece na maioria do tempo. Você não precisa mostrar uma notificação quando o usuário está com seu site aberto e focado.

Dentro do evento de push, você pode verificar se precisa exibir uma notificação examinando os clientes da janela e procurando uma janela em foco.

O código para extrair todas as janelas e procurar uma janela em foco fica assim:

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 reunir todos os clientes de janela e, em seguida, fazemos a repetição deles verificando o parâmetro focused.

Dentro do nosso evento push, usaríamos 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 mensagem para uma página de um evento push

Observamos que é possível pular a exibição de uma notificação se o usuário estiver atualmente em 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. Assim, a página da Web pode mostrar uma notificação ou atualização ao usuário, informando sobre o evento. Isso é útil nas situações em que uma notificação sutil na página é melhor e mais simples para o usuário.

Digamos que recebemos um push, verificamos se nosso aplicativo da Web está em foco no momento e podemos "postar uma mensagem" em cada página aberta, da seguinte forma:

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, ouvimos mensagens adicionando um listener de eventos de mensagem:

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

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

Vale ressaltar que, se você não definir um listener de mensagens na página da Web, as mensagens do service worker não terão efeito.

Armazenar uma página em cache e abrir uma janela

Um cenário que está fora do escopo deste guia, mas 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.

Isso exige que o service worker esteja configurado para processar eventos fetch. No entanto, se você implementar um listener de eventos fetch, aproveite esse recurso 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

  • 50
  • 17
  • 44
  • 16

Origem

Clients.openWindow()

Compatibilidade com navegadores

  • 40
  • 17
  • 44
  • 11.1

Origem

ServiceWorkerRegistration.getNotifications()

Compatibilidade com navegadores

  • 40
  • 17
  • 44
  • 16

Origem

clients.matchAll()

Compatibilidade com navegadores

  • 42
  • 17
  • 54
  • 11.1

Origem

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

A seguir

Laboratórios de códigos