Padrões comuns de notificação

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

Isso envolverá o uso de algumas APIs diferentes que estão disponíveis no service worker.

Evento de fechamento de notificação

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

Há também um evento notificationclose que será chamado se o usuário dispensar um dos seus notificações (ou seja, em vez de clicar na notificação, o usuário clica na cruz ou desliza o notificação ausente).

Normalmente, esse evento é usado em análises para acompanhar o engajamento do usuário com 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ó útil se o usuário tiver clicado na notificação. Por exemplo, o URL que deve ser aberta quando uma notificação é clicada.

A forma mais fácil de extrair dados de um evento push e anexá-los a um é adicionar um parâmetro data ao objeto de opções transmitido em 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 a um URL específico. Podemos fazer isso com o clients.openWindow() API.

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, veremos como verificar se a página para a qual queremos direcionar o usuário está já está aberto ou não. Dessa forma, podemos focar na guia aberta em vez de abrir novas guias.

Focar uma janela que já existe

Quando possível, devemos focar em uma janela em vez de abrir uma nova janela toda vez que o usuário clica em uma notificação.

Antes de vermos como fazer isso, é importante destacar que só é possível para páginas na sua origem. Isso porque podemos ver apenas quais páginas do nosso site estão abertas. Isso evita os desenvolvedores não consigam ver todos os sites que os usuários estão visitando.

Considerando 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 de URL. Esse é um truque legal que eu peguei do Jeff Posnick (em inglês). Chamar new URL() com o objeto location vai retorna um URL absoluto se a string transmitida for relativa (ou seja, / se tornará https://example.com/).

Tornamos o URL absoluto para que possamos compará-lo ao URL da janela posteriormente.

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

Em seguida, recebemos uma lista dos objetos WindowClient, que é a lista dos guias e janelas abertas no momento. Lembre-se de que essas são guias apenas para sua origem.

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

As opções transmitidas para matchAll informam ao navegador que queremos apenas para pesquisar "janela" de tipos de cliente (ou seja, procure guias e janelas e excluir os funcionários da Web). includeUncontrolled nos permite pesquisar todas as guias da sua origem que não são controladas pelo serviço atual worker, ou seja, o service worker que executa esse código. De modo geral, sempre quer que includeUncontrolled seja verdadeiro ao chamar matchAll().

Capturamos a promessa retornada como promiseChain para que ela seja transmitida à event.waitUntil() mais tarde, mantendo nosso service worker ativo.

Quando a promessa matchAll() é resolvida, iteramos os clientes de janela retornados e comparar seus URLs com o URL que queremos abrir. Se encontrarmos uma correspondência, nosso foco que chamará a atenção dos usuários para essa janela. O foco é feito com o 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);
  }
});

Mesclagem de notificações

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

No entanto, você pode ficar mais sofisticado com o recolhimento de notificações usando o 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 as últimas mensagem.

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

Vejamos como podemos usar essa API para implementar o exemplo do 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 vamos fazer é encontrar quaisquer notificações abertas para um usuário com uma nome de usuário Vamos pegar registration.getNotifications(), fazer um loop sobre elas e conferir 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 rastrear o número de novas mensagens adicionando uma contagem ao nosso novo os dados da notificação e incrementá-los com 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 o número de mensagens e definimos o título e corpo da mensagem da notificação. Se houver não há notificações, criamos uma nova notificação com newMessageCount de 1.

O resultado é que a primeira mensagem ficaria 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 o as notificações apareçam uma sobre a outra, ele terá 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 quando recebe um envio, 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 tem seu site aberto e focado.

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

O código para buscar todas as janelas e procurar uma janela em foco é 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 receber todos os nossos clientes de janela e, em seguida, fazemos um loop sobre eles 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 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 houver uma notificação muito pesada?

Uma abordagem é enviar uma mensagem do service worker para a página, dessa forma, a página pode mostrar uma notificação ou atualização ao 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 push, verificamos se o foco do nosso aplicativo da Web então podemos "postar uma mensagem" a 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, ouvimos as mensagens adicionando um evento de mensagem ouvinte:

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 interface personalizada sua página ou ignorar completamente a mensagem.

Vale ressaltar que, se você não definir um listener de mensagens em sua página da Web, o as mensagens do service worker não farão 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 melhore a UX geral do seu app da Web armazenando em cache as páginas que você espera que os usuários acessem depois clicando na notificação.

Isso requer que seu service worker esteja configurado para processar eventos fetch, mas se você implementar um listener de eventos fetch, certifique-se de levar aproveitá-la no evento push armazenando a página e os recursos em cache de que precisará 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 introdução a service workers. post.

A seguir

Code labs