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 trabalhos curtos, como enviar uma mensagem, mas, se a tarefa demorar demais, o navegador vai encerrar o worker do serviço. Caso contrário, isso representa um risco para a privacidade e a 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.

Confira uma demonstração rápida de dois minutos mostrando o estado tradicional das coisas em comparação com o uso do fetch em segundo plano:

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

Como funciona

Uma busca em segundo plano funciona assim:

  1. Você informa ao navegador para executar um grupo de buscas em segundo plano.
  2. O navegador busca esses itens, mostrando o progresso para o 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 houver alguma.

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

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

Se o usuário iniciar o download 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 recursos

Como acontece com qualquer novo recurso, você precisa detectar se o navegador oferece suporte a ele. 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 depende de um registro de service worker. Portanto, registre um service worker primeiro. Em seguida:

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>
As coisas a serem buscadas. As strings serão tratadas como URLs e transformadas em Requests usando new Request(theString).

É possível buscar coisas de outras origens, desde que os recursos permitam isso pelo CORS.

Observação:no momento, o Chrome não oferece suporte a solicitações que exigem um pré-lançamento do CORS.

options Um objeto que pode incluir o seguinte:
options.title string
Um título para o navegador mostrar junto 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 descompactados).

Embora isso seja opcional, é altamente recomendável que você forneça. Ele é usado para informar ao usuário o tamanho do download e fornecer informações sobre o progresso. Se você não fornecer essa informação, o navegador vai informar ao usuário que o tamanho é desconhecido. Como resultado, o usuário pode ter mais probabilidade de interromper o download.

Se os downloads de busca em segundo plano excederem o número indicado aqui, eles serão abortados. Não há problema se o download for menor que o downloadTotal. Portanto, se você não tiver certeza do total do download, é melhor usar o valor máximo.

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

Fornecer muitas solicitações para uma única busca em segundo plano permite combinar coisas que são logicamente uma única coisa para o usuário. Por exemplo, um filme pode ser dividido em milhares de recursos (típico com MPEG-DASH) e ter recursos adicionais, como imagens. Um nível de um 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 receber uma busca em segundo plano

É possível fazer uma busca em segundo plano existente desta forma:

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

…transmitindo o ID da busca em segundo plano que você quer. get retorna undefined se não houver uma busca em segundo plano ativa com esse ID.

Uma busca em segundo plano é considerada "ativa" desde o momento em que é registrada até que seja concluída, falha ou seja interrompida.

É possível conferir uma lista de todas as transferências em segundo plano ativas usando getIds:

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

Registros de busca em segundo plano

Um 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 do recurso do zero.

result

Opções:

  • "": a busca em segundo plano está ativa, então ainda não há resultados.
  • "success": a busca em segundo plano foi concluída.
  • "failure": ocorreu uma falha na busca em segundo plano. Esse valor só aparece quando a busca em segundo plano falha totalmente, já que o navegador não pode tentar novamente/reassumir.
failureReason

Opções:

  • "": a busca em segundo plano não falhou.
  • "aborted": a busca em segundo plano foi abortada pelo usuário ou abort() foi chamado.
  • "bad-status": uma das respostas teve um status inválido, 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 em uma busca que não pode ser repetida.
  • "quota-exceeded": a cota de armazenamento foi atingida durante a busca em segundo plano.
  • "download-total-exceeded": o "downloadTotal" fornecido foi excedido.
recordsAvailable boolean
As solicitações/respostas podem ser acessadas?

Quando esse valor for falso, match e matchAll não poderão ser usados.

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

A promessa retornada é resolvida com "true" se a busca foi interrompida.

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

Os argumentos aqui são os mesmos 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 resolve com a primeira correspondência.
Eventos
progress É acionado quando qualquer um dos valores uploaded, downloaded, result ou failureReason muda.

Acompanhamento do progresso

Isso pode ser feito pelo evento progress. Lembre-se de que downloadTotal é qualquer valor que você forneceu ou 0 se você não tiver 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 as 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);
});

record é um BackgroundFetchRecord e tem este formato:

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

A resposta está atrás de uma promessa porque talvez ainda não tenha sido recebida. A promessa será rejeitada se a busca falhar.

Eventos de service worker

Eventos
backgroundfetchsuccess Tudo foi buscado com sucesso.
backgroundfetchfailure Uma ou mais transferências falharam.
backgroundfetchabort Um ou mais downloads falharam.

Isso é útil apenas se você quiser limpar 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 mudar o título/ícones definidos inicialmente. Isso é opcional, mas permite fornecer mais contexto, se necessário. Você só pode fazer isso *uma vez* durante os eventos backgroundfetchsuccess e backgroundfetchfailure.

Reagir a sucesso/falha

Já abordamos o evento progress, mas ele só é útil enquanto o usuário tem uma página aberta no seu site. O principal benefício do fetch 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 vai receber o evento backgroundfetchsuccess, e event.registration será o registro da busca em segundo plano.

Depois desse evento, as solicitações e respostas buscadas não são mais acessíveis. Se você quiser mantê-las, mova-as para algum lugar, como a API de cache.

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

Por exemplo, no seu worker de serviço:

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!' });
  }());
});

A falha pode ter sido causada por um único erro 404, que pode não ter sido importante para você. Portanto, pode ainda valer a pena copiar algumas respostas para um cache, como acima.

Reação a cliques

A interface que mostra o progresso e o resultado do download é clicável. O evento backgroundfetchclick no service worker permite reagir a isso. Como acima, event.registration será o registro de busca em segundo plano.

O que geralmente acontece com esse evento é a abertura de 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 se referia incorretamente ao fetch em segundo plano como um "padrão da Web". No momento, a API não está no padrão, e a especificação pode ser encontrada no WICG como um rascunho do relatório do grupo da comunidade.