Apresentamos a Busca em segundo plano

Jake Archibald
Jake Archibald

Em 2015, lançamos a sincronização em segundo plano, que permite que o service worker adie o trabalho até que o usuário tenha conectividade. Isso significa que o usuário pode digitar uma mensagem, clicar em "Enviar" e sair do site sabendo que a mensagem será enviada agora ou quando ele tiver conectividade.

É um recurso útil, mas exige que o service worker esteja ativo durante a busca. Isso não é um problema para partes curtas de trabalho, como o envio de uma mensagem. No entanto, se a tarefa demorar muito, o navegador vai encerrar o service worker. Caso contrário, há um risco à privacidade e à bateria do usuário.

E se você precisar fazer o download de algo que pode levar muito tempo, como um filme, podcasts ou níveis de um jogo? É para isso que serve a Busca em segundo plano.

A Busca em segundo plano está disponível por padrão desde o Chrome 74.

Esta é uma demonstração rápida de dois minutos que mostra o estado tradicional das coisas em comparação com o uso da Busca em segundo plano:

Experimente a demonstração e navegue pelo código.

Como funciona

Uma busca em segundo plano funciona da seguinte maneira:

  1. Você instrui o navegador a executar um grupo de buscas em segundo plano.
  2. O navegador busca essas coisas, mostrando o progresso ao usuário.
  3. Depois que a busca é concluída ou falha, o navegador abre o service worker e dispara um evento para informar o que aconteceu. É aqui que você decide o que fazer com as respostas, se for o caso.

Se o usuário fechar as páginas do seu site após a etapa 1, não tem problema, o download continuará. Como a busca é altamente visível e facilmente cancelável, não há a preocupação de privacidade de uma tarefa de sincronização em segundo plano longa demais. Como o service worker não está em execução constantemente, não há a preocupação de que ele possa abusar do sistema, como a mineração de bitcoin em segundo plano.

Em algumas plataformas (como o Android), é possível que o navegador seja fechado após a etapa 1, já que o navegador pode transferir a busca para o sistema operacional.

Se o usuário iniciar o download enquanto estiver off-line ou ficar off-line durante o download, a busca em segundo plano será pausada e retomada mais tarde.

A API

Detecção de recurso

Como acontece com qualquer recurso novo, você quer detectar se o navegador é compatível. Para a Busca em segundo plano, é simples:

if ('BackgroundFetchManager' in self) {
  // This browser supports Background Fetch!
}

Como iniciar uma busca em segundo plano

A API principal trava o registro de um service worker. Portanto, primeiro registre um service worker. Depois:

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

backgroundFetch.fetch usa três argumentos:

Parâmetros
id string
identifica de forma exclusiva essa busca em segundo plano.

backgroundFetch.fetch será rejeitado se o ID corresponder a uma busca em segundo plano existente.

requests Array<Request|string>
O que será buscado As strings serão tratadas como URLs e transformadas em Requests por meio de new Request(theString).

É possível buscar itens de outras origens, desde que os recursos permitam isso usando o CORS.

Observação: atualmente, o Chrome não aceita solicitações que exijam uma simulação do CORS.

options Um objeto que pode incluir o seguinte:
options.title string
Um título para o navegador mostrar com o progresso.
options.icons Array<IconDefinition>
Uma matriz de objetos com "src", "size" e "type".
options.downloadTotal number
O tamanho total dos corpos de resposta (depois de descompactar com gzip).

Embora seja opcional, é altamente recomendável que você a forneça. Ele é usado para informar ao usuário o tamanho do download e fornecer informações de progresso. Se você não fornecer essa informação, o navegador vai informar ao usuário que o tamanho é desconhecido e, como resultado, o usuário poderá cancelar o download.

Se os downloads de busca em segundo plano excederem o número fornecido aqui, ela será cancelada. Não há problema se o download for menor que o downloadTotal. Portanto, se você não tiver certeza de qual será o total do download, é melhor errar por ter cautela.

backgroundFetch.fetch retorna uma promessa que é resolvida com um BackgroundFetchRegistration. Falarei sobre isso mais tarde. A promessa vai ser rejeitada se o usuário tiver desativado os downloads ou um dos parâmetros fornecidos for inválido.

Fornecer muitas solicitações para uma única busca em segundo plano permite combinar coisas que, logicamente, são únicas para o usuário. Por exemplo, um filme pode ser dividido em milhares de recursos (típico com MPEG-DASH) e vir com recursos adicionais, como imagens. Um nível de jogo pode ser distribuído por muitos recursos de JavaScript, imagem e áudio. Mas para o usuário, é apenas "o filme" ou "o nível".

Como conseguir uma busca em segundo plano existente

É possível conseguir uma busca em segundo plano existente como esta:

navigator.serviceWorker.ready.then(async (swReg) => {
  const bgFetch = await swReg.backgroundFetch.get('my-fetch');
});

... passando o id da busca em segundo plano que você quiser. get vai retornar undefined se não houver uma busca ativa em segundo plano com esse ID.

Uma busca em segundo plano é considerada "ativa" a partir do momento em que é registrada, até que seja concluída, falhar ou ser cancelada.

Veja uma lista de todas as buscas ativas em segundo plano usando getIds:

navigator.serviceWorker.ready.then(async (swReg) => {
  const ids = await swReg.backgroundFetch.getIds();
});

Registros de busca em segundo plano

Uma BackgroundFetchRegistration (bgFetch nos exemplos acima) tem o seguinte:

Propriedades
id string
O ID da busca em segundo plano.
uploadTotal number
O número de bytes a serem enviados ao servidor.
uploaded number
O número de bytes enviados.
downloadTotal number
O valor fornecido quando a busca em segundo plano foi registrada, ou zero.
downloaded number
O número de bytes recebidos.

Esse valor pode diminuir. Por exemplo, se a conexão cair e o download não puder ser retomado, o navegador vai reiniciar a busca desse recurso do zero.

result

Opções:

  • "": a busca em segundo plano está ativa. Por isso, ainda não há resultado.
  • "success": a busca em segundo plano foi concluída.
  • "failure": a busca em segundo plano falhou. Esse valor só aparece quando a busca em segundo plano falha totalmente, já que o navegador não pode tentar novamente/retomar.
failureReason

Opções:

  • "": a busca em segundo plano não falhou.
  • "aborted": a busca em segundo plano foi cancelada pelo usuário, ou abort() foi chamado.
  • "bad-status": uma das respostas apresentava um status "não OK". Por exemplo, 404.
  • "fetch-error": uma das buscas falhou por algum outro motivo, por exemplo, CORS, MIX, uma resposta parcial inválida ou uma falha geral de rede para uma busca que não pode ser repetida.
  • "quota-exceeded": a cota de armazenamento foi alcançada durante a busca em segundo plano.
  • "download-total-exceeded": o "downloadTotal" fornecido foi excedido.
recordsAvailable boolean
É possível acessar as solicitações/respostas subjacentes?

Se ele for falso, match e matchAll não poderão ser usados.

Métodos
abort() Retorna Promise<boolean>
Cancelar a busca em segundo plano.

A promessa retornada é resolvida com true se a busca foi cancelada com sucesso.

matchAll(request, opts) Retorna Promise<Array<BackgroundFetchRecord>>
Receba as solicitações e respostas.

Os argumentos são iguais aos da API de cache. Chamar sem argumentos retorna uma promessa para todos os registros.

Veja mais detalhes abaixo.

match(request, opts) Retorna Promise<BackgroundFetchRecord>
como acima, mas é resolvido com a primeira correspondência.
Eventos
progress Disparado quando qualquer uploaded, downloaded, result ou failureReason muda.

Como acompanhar o progresso

Isso pode ser feito pelo evento progress. Lembre-se de que downloadTotal é o valor que você forneceu, ou 0 caso você não tenha fornecido um valor.

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

Como receber solicitações e respostas

bgFetch.match('/ep-5.mp3').then(async (record) => {
  if (!record) {
    console.log('No record found');
    return;
  }

  console.log(`Here's the request`, record.request);
  const response = await record.responseReady;
  console.log(`And here's the response`, response);
});

A record é uma BackgroundFetchRecord e tem esta aparência:

Propriedades
request Request
A solicitação que foi fornecida.
responseReady Promise<Response>
A resposta buscada.

A resposta é prometida e pode não ter sido recebida ainda. A promessa vai ser rejeitada se a busca falhar.

Eventos de service worker

Eventos
backgroundfetchsuccess Tudo foi buscado.
backgroundfetchfailure Falha em uma ou mais buscas.
backgroundfetchabort Falha em uma ou mais buscas.

Isso só é realmente útil se você deseja realizar a limpeza de dados relacionados.

backgroundfetchclick O usuário clicou na interface de progresso do download.

Os objetos de evento têm o seguinte:

Propriedades
registration BackgroundFetchRegistration
Métodos
updateUI({ title, icons }) Permite alterar o título/ícones que você definiu inicialmente. Isso é opcional, mas permite que você forneça mais contexto, se necessário. Só é possível fazer isso *uma vez* durante os eventos backgroundfetchsuccess e backgroundfetchfailure.

Como reagir a sucessos/falhas

Já vimos o evento progress, mas ele só é útil enquanto o usuário tem uma página aberta para o site. O principal benefício da busca em segundo plano é que as coisas continuam funcionando depois que o usuário sai da página ou até mesmo fecha o navegador.

Se a busca em segundo plano for concluída, o service worker receberá o evento backgroundfetchsuccess, e event.registration será o registro da busca em segundo plano.

Após esse evento, as solicitações e respostas buscadas não poderão mais ser acessadas. Portanto, se você quiser mantê-las, mova-as para algum lugar como a API de cache.

Como na maioria dos eventos de service worker, use event.waitUntil para que o service worker saiba quando o evento está concluído.

Por exemplo, no service worker:

addEventListener('backgroundfetchsuccess', (event) => {
  const bgFetch = event.registration;

  event.waitUntil(async function() {
    // Create/open a cache.
    const cache = await caches.open('downloads');
    // Get all the records.
    const records = await bgFetch.matchAll();
    // Copy each request/response across.
    const promises = records.map(async (record) => {
      const response = await record.responseReady;
      await cache.put(record.request, response);
    });

    // Wait for the copying to complete.
    await Promise.all(promises);

    // Update the progress notification.
    event.updateUI({ title: 'Episode 5 ready to listen!' });
  }());
});

As falhas podem ter se voltado a um único erro 404, o que pode não ter sido importante para você. Por isso, ainda pode valer a pena copiar algumas respostas em um cache, conforme mostrado acima.

Reagindo ao clique

É possível clicar na interface que mostra o progresso e o resultado do download. O evento backgroundfetchclick no service worker permite que você reaja a isso. Assim como acima, event.registration será o registro da busca em segundo plano.

A coisa mais comum a fazer com esse evento é abrir uma janela:

addEventListener('backgroundfetchclick', (event) => {
  const bgFetch = event.registration;

  if (bgFetch.result === 'success') {
    clients.openWindow('/latest-podcasts');
  } else {
    clients.openWindow('/download-progress');
  }
});

Outros recursos

Correção: uma versão anterior deste artigo fazia referência incorreta à Busca em segundo plano como um "padrão da Web". No momento, a API não está seguindo os padrões. A especificação pode ser encontrada nas WICG como um rascunho do relatório do grupo comunitário.