Comunicação bidirecional com service workers

Andrew Guan
Andrew Guan
Demián Renzulli
Demián Renzulli

Em alguns casos, um app da Web pode precisar estabelecer um canal de comunicação bidirecional entre a página e o service worker.

Por exemplo: em um PWA de podcast, é possível criar um recurso para permitir que o usuário faça o download de episódios para consumo off-line e que o service worker mantenha a página regularmente informada sobre o progresso, de modo que a linha de execução principal possa atualizar a interface.

Neste guia, vamos conhecer as diferentes maneiras de implementar uma comunicação bidirecional entre o contexto da janela e do service worker, explorando diferentes APIs, a biblioteca Workbox e alguns casos avançados.

Diagrama mostrando um service worker e a página trocando mensagens.

Como usar o Workbox

workbox-window é um conjunto de módulos da biblioteca Workbox (link em inglês) que são destinados a serem executados no contexto da janela. A classe Workbox fornece um método messageSW() para enviar uma mensagem ao service worker registrado da instância e aguardar uma resposta.

O código da página a seguir cria uma nova instância de Workbox e envia uma mensagem ao service worker para receber a versão:

const wb = new Workbox('/sw.js');
wb.register();

const swVersion = await wb.messageSW({type: 'GET_VERSION'});
console.log('Service Worker version:', swVersion);

O service worker implementa um listener de mensagens na outra extremidade e responde ao service worker registrado:

const SW_VERSION = '1.0.0';

self.addEventListener('message', (event) => {
  if (event.data.type === 'GET_VERSION') {
    event.ports[0].postMessage(SW_VERSION);
  }
});

Por trás, a biblioteca usa uma API do navegador que vamos analisar na próxima seção: Canal de mensagens, mas abstrai muitos detalhes de implementação, facilitando o uso, além de aproveitar o amplo suporte a navegadores que essa API tem.

Diagrama que mostra a comunicação bidirecional entre a página e o worker de serviço usando a janela do Workbox.

Como usar APIs do navegador

Se a biblioteca Workbox não for suficiente para suas necessidades, há várias APIs de nível inferior disponíveis para implementar a comunicação bidirecional entre páginas e service workers. Elas têm algumas semelhanças e diferenças:

Semelhanças:

  • Em todos os casos, a comunicação começa em uma extremidade pela interface postMessage() e é recebida na outra extremidade pela implementação de um gerenciador message.
  • Na prática, todas as APIs disponíveis permitem implementar os mesmos casos de uso, mas algumas delas podem simplificar o desenvolvimento em alguns cenários.

Diferenças:

  • Eles têm maneiras diferentes de identificar o outro lado da comunicação: alguns usam uma referência explícita ao outro contexto, enquanto outros podem se comunicar implicitamente por meio de um objeto proxy instanciado em cada lado.
  • O suporte a navegadores varia entre eles.
Diagrama mostrando a comunicação bidirecional entre a página e o service worker, além das APIs de navegador disponíveis.

API Broadcast Channel

Compatibilidade com navegadores

  • Chrome: 54.
  • Edge: 79.
  • Firefox: 38.
  • Safari: 15.4.

Origem

A API Broadcast Channel permite a comunicação básica entre contextos de navegação por meio de objetos BroadcastChannel.

Para implementar, primeiro cada contexto precisa instanciar um objeto BroadcastChannel com o mesmo ID e enviar e receber mensagens dele:

const broadcast = new BroadcastChannel('channel-123');

O objeto BroadcastChannel expõe uma interface postMessage() para enviar uma mensagem a qualquer contexto de detecção:

//send message
broadcast.postMessage({ type: 'MSG_ID', });

Qualquer contexto do navegador pode detectar mensagens pelo método onmessage do objeto BroadcastChannel:

//listen to messages
broadcast.onmessage = (event) => {
  if (event.data && event.data.type === 'MSG_ID') {
    //process message...
  }
};

Como visto, não há uma referência explícita a um contexto específico. Portanto, não é necessário obter uma referência primeiro para o service worker ou qualquer cliente específico.

Diagrama mostrando comunicação bidirecional entre a página e o service worker, usando um objeto de canal de transmissão.

A desvantagem é que, no momento em que este artigo foi escrito, a API era compatível com o Chrome, Firefox e Edge, mas outros navegadores, como o Safari, ainda não são compatíveis.

API do cliente

Compatibilidade com navegadores

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

Origem

A API Client permite que você receba uma referência a todos os objetos WindowClient que representam as guias ativas que o service worker está controlando.

Como a página é controlada por um único service worker, ela ouve e envia mensagens para o service worker ativo diretamente pela interface serviceWorker:

//send message
navigator.serviceWorker.controller.postMessage({
  type: 'MSG_ID',
});

//listen to messages
navigator.serviceWorker.onmessage = (event) => {
  if (event.data && event.data.type === 'MSG_ID') {
    //process response
  }
};

Da mesma forma, o service worker detecta mensagens implementando um listener onmessage:

//listen to messages
self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'MSG_ID') {
    //Process message
  }
});

Para se comunicar com qualquer um dos clientes, o service worker obtém uma matriz de objetos WindowClient executando métodos como Clients.matchAll() e Clients.get(). Em seguida, ele pode postMessage() qualquer um deles:

//Obtain an array of Window client objects
self.clients.matchAll(options).then(function (clients) {
  if (clients && clients.length) {
    //Respond to last focused tab
    clients[0].postMessage({type: 'MSG_ID'});
  }
});
Diagrama mostrando um service worker se comunicando com uma matriz de clientes.

Client API é uma boa opção para se comunicar facilmente com todas as guias ativas de um service worker de maneira relativamente simples. A API tem suporte de todos os principais navegadores, mas nem todos os métodos podem estar disponíveis. Verifique o suporte do navegador antes de implementá-la no site.

Canal de mensagens

Compatibilidade com navegadores

  • Chrome: 2.
  • Edge: 12.
  • Firefox: 41.
  • Safari: 5.

Origem

O canal de mensagens requer a definição e a transmissão de uma porta de um contexto a outro para estabelecer um canal de comunicação bidirecional.

Para inicializar o canal, a página instancia um objeto MessageChannel e o usa para enviar uma porta ao service worker registrado. A página também implementa um listener onmessage para receber mensagens do outro contexto:

const messageChannel = new MessageChannel();

//Init port
navigator.serviceWorker.controller.postMessage({type: 'PORT_INITIALIZATION'}, [
  messageChannel.port2,
]);

//Listen to messages
messageChannel.port1.onmessage = (event) => {
  // Process message
};
Diagrama mostrando uma página que transmite uma porta para um worker de serviço para estabelecer comunicação bidirecional.

O service worker recebe a porta, salva uma referência a ela e a usa para enviar uma mensagem para o outro lado:

let communicationPort;

//Save reference to port
self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'PORT_INITIALIZATION') {
    communicationPort = event.ports[0];
  }
});

//Send messages
communicationPort.postMessage({type: 'MSG_ID'});

No momento, o MessageChannel é aceito por todos os principais navegadores.

APIs avançadas: sincronização e busca em segundo plano

Neste guia, abordamos maneiras de implementar técnicas de comunicação bidirecional para casos relativamente simples, como transmitir uma mensagem de string que descreve a operação a ser realizada ou uma lista de URLs para armazenar em cache de um contexto para outro. Nesta seção, vamos analisar duas APIs para lidar com cenários específicos: falta de conectividade e downloads longos.

Sincronização em segundo plano

Compatibilidade com navegadores

  • Chrome: 49.
  • Borda: 79.
  • Firefox: não é compatível.
  • Safari: não é compatível.

Origem

Um app de chat pode querer garantir que as mensagens nunca sejam perdidas devido a uma conectividade ruim. A API Background Sync permite adiar ações para que sejam tentadas novamente quando o usuário tiver uma conectividade estável. Isso é útil para garantir que tudo o que o usuário quer enviar seja enviado.

Em vez da interface postMessage(), a página registra um sync:

navigator.serviceWorker.ready.then(function (swRegistration) {
  return swRegistration.sync.register('myFirstSync');
});

Em seguida, o service worker detecta o evento sync para processar a mensagem:

self.addEventListener('sync', function (event) {
  if (event.tag == 'myFirstSync') {
    event.waitUntil(doSomeStuff());
  }
});

A função doSomeStuff() precisa retornar uma promessa indicando o sucesso/falha do que está tentando fazer. Se ela for atendida, a sincronização será concluída. Se ela falhar, será programada outra sincronização para uma nova tentativa. As novas tentativas de sincronização também aguardam a conectividade e usam uma espera exponencial.

Depois que a operação for realizada, o service worker poderá se comunicar de volta com a página para atualizar a IU usando qualquer uma das APIs de comunicação exploradas anteriormente.

A Pesquisa Google usa a sincronização em segundo plano para manter consultas com falha devido a conectividade ruim e tentar novamente mais tarde, quando o usuário estiver on-line. Depois que a operação é realizada, o resultado é comunicado ao usuário por uma notificação push da Web:

Diagrama mostrando uma página passando uma porta para um service worker para estabelecer uma comunicação bidirecional.

Busca em segundo plano

Compatibilidade com navegadores

  • Chrome: 74.
  • Borda: 79.
  • Firefox: incompatível.
  • Safari: não é compatível.

Origem

Para trabalhos relativamente curtos, como enviar uma mensagem ou uma lista de URLs para o cache, as opções exibidas até agora são uma boa escolha. Se a tarefa demorar muito, o navegador vai encerrar o worker de serviço. Caso contrário, isso representa um risco para a privacidade e a bateria do usuário.

A API Background Fetch permite que você transfira uma tarefa demorada para um worker de serviço, como o download de filmes, podcasts ou níveis de um jogo.

Para se comunicar com o worker de serviço na página, use backgroundFetch.fetch em vez de postMessage():

navigator.serviceWorker.ready.then(async (swReg) => {
  const bgFetch = await swReg.backgroundFetch.fetch(
    'my-fetch',
    ['/ep-5.mp3', 'ep-5-artwork.jpg'],
    {
      title: 'Episode 5: Interesting things.',
      icons: [
        {
          sizes: '300x300',
          src: '/ep-5-icon.png',
          type: 'image/png',
        },
      ],
      downloadTotal: 60 * 1024 * 1024,
    },
  );
});

O objeto BackgroundFetchRegistration permite que a página detecte o evento progress para acompanhar o progresso do download:

bgFetch.addEventListener('progress', () => {
  // If we didn't provide a total, we can't provide a %.
  if (!bgFetch.downloadTotal) return;

  const percent = Math.round(
    (bgFetch.downloaded / bgFetch.downloadTotal) * 100,
  );
  console.log(`Download progress: ${percent}%`);
});
Diagrama mostrando uma página transmitindo uma porta para um worker de serviço para estabelecer comunicação bidirecional.
A interface é atualizada para indicar o progresso de um download (à esquerda). Graças aos service workers, a operação pode continuar em execução quando todas as guias são fechadas (à direita).

Próximas etapas

Neste guia, abordamos o caso mais geral de comunicação entre a página e os service workers (comunicação bidirecional).

Muitas vezes, um contexto pode precisar de apenas um para se comunicar com o outro, sem receber uma resposta. Confira os guias a seguir para saber como implementar técnicas unidirecionais nas suas páginas de e para o service worker, além de casos de uso e exemplos de produção:

  • Guia de armazenamento em cache imperativo: chamar um service worker da página para armazenar recursos em cache com antecedência (por exemplo, em cenários de pré-carregamento).
  • Transmitir atualizações: chamar a página do service worker para informar sobre atualizações importantes (por exemplo, uma nova versão do webapp está disponível).