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 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 explorar as diferentes maneiras de implementar uma comunicação bidirecional entre o contexto da janela e do service worker, analisando diferentes APIs, a biblioteca Workbox e alguns casos avançados.
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
oferece 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 para o 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);
}
});
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 e aproveitando o amplo suporte a navegadores que essa API tem.
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 workers de serviço. Eles 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 gerenciadormessage
. - 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 um objeto proxy instanciado em cada lado.
- O suporte a navegadores varia entre eles.
API Broadcast Channel
A API Broadcast Channel permite a comunicação básica entre contextos de navegação usando objetos BroadcastChannel.
Para implementar isso, 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 escuta:
//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.
A desvantagem é que, no momento em que este artigo foi escrito, a API tinha suporte do Chrome, Firefox e Edge, mas outros navegadores, como o Safari, ainda não têm.
API do cliente
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'});
}
});
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á-lo no site.
Canal de mensagens
O canal de mensagens exige a definição e a transmissão de uma porta de um contexto para 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
};
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 conhecer duas APIs para lidar com cenários específicos: falta de conectividade e downloads longos.
Sincronização em segundo plano
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');
});
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 cumprida, a sincronização será concluída. Se ela falhar, outra sincronização será programada 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 worker de serviço poderá se comunicar com a página para atualizar a interface usando qualquer uma das APIs de comunicação analisadas anteriormente.
A Pesquisa Google usa a sincronização em segundo plano para manter consultas com falha devido a uma 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:
Busca em segundo plano
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}%`);
});
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 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).
- Atualizações de transmissão: chamar a página do service worker para informar sobre atualizações importantes (por exemplo, uma nova versão da WebApp está disponível).