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 dupla 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 permitir que o service worker mantenha a página informada regularmente sobre o progresso, para que a linha de execução principal possa atualizar a interface.

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

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

Usar o Workbox

workbox-window é um conjunto de módulos da biblioteca Workbox que se destinam a ser 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 Workbox e envia uma mensagem ao service worker para receber a versão dele:

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

Internamente, a biblioteca usa uma API de navegador que analisaremos na próxima seção: Canal de mensagens, mas abstrai muitos detalhes de implementação, facilitando o uso e aproveitando o suporte amplo a navegadores que essa API tem.

Diagrama mostrando a comunicação bidirecional entre a página e o service worker usando a janela da caixa de trabalho.

Como usar as APIs do navegador

Se a biblioteca do 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. Eles têm algumas semelhanças e diferenças:

Semelhanças:

  • Em todos os casos, a comunicação começa em uma extremidade na interface postMessage() e é recebida do outro lado, implementando um gerenciador message.
  • Na prática, todas as APIs disponíveis nos 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 um objeto de proxy instanciado em cada lado.
  • A compatibilidade com navegadores varia entre eles.
Diagrama mostrando a comunicação bidirecional entre a página e o service worker e as APIs de navegador disponíveis.

API Broadcast Channel

Compatibilidade com navegadores

  • 54
  • 79
  • 38
  • 15,4

Origem

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

Para implementá-lo, 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 de navegador pode detectar mensagens com o método onmessage do objeto BroadcastChannel:

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

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

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

A desvantagem é que, no momento, a API é compatível com o Chrome, o Firefox e o Edge, mas outros navegadores, como o Safari, ainda não oferecem suporte.

API do cliente

Compatibilidade com navegadores

  • 40
  • 17
  • 44
  • 11.1

Origem

A API Client permite acessar 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 detecta 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 de volta com qualquer um dos clientes, o service worker recebe 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 é compatível com todos os principais navegadores, mas nem todos os métodos estão disponíveis. Por isso, verifique o suporte do navegador antes de implementá-la no seu site.

Canal de mensagens

Compatibilidade com navegadores

  • 2
  • 12
  • 41
  • 5

Origem

O canal da mensagem 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 transmitindo uma porta para um service worker, 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 é compatível com todos os principais navegadores.

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

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

Sincronização em segundo plano

Compatibilidade com navegadores

  • 49
  • 79
  • x
  • x

Origem

Um app de chat pode querer garantir que as mensagens nunca sejam perdidas devido a uma conexão ruim. A API Background Sync permite adiar ações para que sejam repetidas quando o usuário tiver uma conectividade estável. Isso é útil para garantir que o conteúdo que o usuário quiser enviar seja realmente 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 ou a falha de qualquer coisa que esteja tentando fazer. Se ela for atendida, a sincronização estará concluída. Se falhar, outra sincronização será agendada para tentar de novo. As novas sincronizações também aguardam a conectividade e empregam uma espera exponencial.

Depois que a operação for executada, 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 à conectividade ruim e as tenta novamente mais tarde, quando o usuário estiver on-line. Quando a operação é realizada, eles comunicam o resultado ao usuário por meio de uma notificação push da Web:

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

Busca em segundo plano

Compatibilidade com navegadores

  • 74
  • 79
  • x
  • x

Origem

Para partes relativamente curtas, como enviar uma mensagem ou uma lista de URLs para armazenar em cache, as opções exploradas até agora são uma boa escolha. Se a tarefa demorar muito, o navegador vai eliminar o service worker. Caso contrário, há um risco à privacidade e à bateria do usuário.

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

Para se comunicar com o service worker a partir da 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 service worker, para estabelecer comunicação bidirecional.
A IU é 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 forem fechadas (à direita).

Próximas etapas

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

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

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