A vida de um service worker

É difícil saber o que os workers de serviço estão fazendo sem entender o ciclo de vida deles. O funcionamento interno deles parece opaco, até mesmo arbitrário. É importante lembrar que, como qualquer outra API do navegador, os comportamentos do service worker são bem definidos, especificados e possibilitam aplicativos off-line, facilitando as atualizações sem interromper a experiência do usuário.

Antes de se aprofundar no Workbox, é importante entender o ciclo de vida do service worker para que o que o Workbox faz faça sentido.

Definição de termos

Antes de entrar no ciclo de vida do service worker, vale a pena definir alguns termos sobre como esse ciclo funciona.

Controle e escopo

A ideia de controle é crucial para entender como os service workers funcionam. Uma página descrita como controlada por um service worker é uma página que permite que um service worker intercepte solicitações de rede em nome dele. O service worker está presente e pode trabalhar para a página em um determinado escopo.

Escopo

O escopo de um worker de serviço é determinado pelo local dele em um servidor da Web. Se um service worker for executado em uma página localizada em /subdir/index.html e estiver localizado em /subdir/sw.js, o escopo dele será /subdir/. Para conferir o conceito de escopo em ação, confira este exemplo:

  1. Acesse https://service-worker-scope-viewer.glitch.me/subdir/index.html. Uma mensagem vai aparecer informando que nenhum service worker está controlando a página. No entanto, essa página registra um service worker de https://service-worker-scope-viewer.glitch.me/subdir/sw.js.
  2. Recarregue a página. Como o service worker foi registrado e está ativo, ele controla a página. Um formulário contendo o escopo do worker do serviço, o estado atual e o URL dele será mostrado. Observação: ter que recarregar a página não tem nada a ver com o escopo, mas sim com o ciclo de vida do worker de serviço, que será explicado mais adiante.
  3. Agora, acesse https://service-worker-scope-viewer.glitch.me/index.html. Mesmo que um service worker tenha sido registrado nessa origem, ainda há uma mensagem informando que não há um service worker atual. Isso ocorre porque esta página não está no escopo do service worker registrado.

O escopo limita quais páginas o service worker controla. Nesse exemplo, isso significa que o worker de serviço carregado de /subdir/sw.js só pode controlar páginas localizadas em /subdir/ ou no subárvore.

O exemplo acima mostra como o escopo funciona por padrão, mas o escopo máximo permitido pode ser substituído definindo o cabeçalho de resposta Service-Worker-Allowed, bem como transmitindo uma opção scope para o método register.

A menos que haja um motivo muito bom para limitar o escopo do service worker a um subconjunto de uma origem, carregue um service worker do diretório raiz do servidor da Web para que o escopo seja o mais amplo possível. Não se preocupe com o cabeçalho Service-Worker-Allowed. Assim fica muito mais simples para todos.

Cliente

Quando se diz que um service worker está controlando uma página, ele está controlando um cliente. Um cliente é qualquer página aberta cujo URL esteja dentro do escopo desse worker de serviço. Especificamente, são instâncias de um WindowClient.

Ciclo de vida de um novo worker de serviço

Para que um worker de serviço controle uma página, ele precisa ser criado. Vamos começar com o que acontece quando um novo worker é implantado para um site sem um worker ativo.

Registro

O registro é a etapa inicial do ciclo de vida do service worker:

<!-- In index.html, for example: -->
<script>
  // Don't register the service worker
  // until the page has fully loaded
  window.addEventListener('load', () => {
    // Is service worker available?
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register('/sw.js').then(() => {
        console.log('Service worker registered!');
      }).catch((error) => {
        console.warn('Error registering service worker:');
        console.warn(error);
      });
    }
  });
</script>

Esse código é executado na linha de execução principal e faz o seguinte:

  1. Como a primeira visita do usuário a um site ocorre sem um service worker registrado, aguarde até que a página seja totalmente carregada antes de registrar um. Isso evita a disputa de largura de banda se o worker de serviço pré-cachear algo.
  2. Embora o service worker tenha suporte, uma verificação rápida ajuda a evitar erros em navegadores que não oferecem suporte a ele.
  3. Quando a página estiver totalmente carregada e se o service worker tiver suporte, registre /sw.js.

Algumas coisas importantes para entender são:

  • Os service workers estão disponíveis apenas por HTTPS ou localhost.
  • Se o conteúdo de um worker de serviço tiver erros de sintaxe, o registro vai falhar e o worker de serviço será descartado.
  • Lembrete: os service workers operam dentro de um escopo. Aqui, o escopo é a origem inteira, já que foi carregada do diretório raiz.
  • Quando o registro começa, o estado do worker de serviço é definido como 'installing'.

Quando o registro for concluído, a instalação vai começar.

Instalação

Um worker de serviço dispara o evento install após o registro. install é chamado apenas uma vez por worker de serviço e não é acionado novamente até ser atualizado. Um callback para o evento install pode ser registrado no escopo do worker com addEventListener:

// /sw.js
self.addEventListener('install', (event) => {
  const cacheKey = 'MyFancyCacheName_v1';

  event.waitUntil(caches.open(cacheKey).then((cache) => {
    // Add all the assets in the array to the 'MyFancyCacheName_v1'
    // `Cache` instance for later use.
    return cache.addAll([
      '/css/global.bc7b80b7.css',
      '/css/home.fe5d0b23.css',
      '/js/home.d3cc4ba4.js',
      '/js/jquery.43ca4933.js'
    ]);
  }));
});

Isso cria uma nova instância de Cache e pré-armazena os recursos. Vamos ter muitas oportunidades para falar sobre a pré-cacheação mais tarde. Então vamos nos concentrar no papel do event.waitUntil. event.waitUntil aceita uma promessa e aguarda até que ela seja resolvida. Neste exemplo, a promessa faz duas coisas assíncronas:

  1. Cria uma nova instância de Cache com o nome 'MyFancyCache_v1'.
  2. Depois que o cache é criado, uma matriz de URLs de recursos é pré-criada usando o método addAll assíncrono.

A instalação falha se as promessas transmitidas para event.waitUntil forem rejeitadas. Se isso acontecer, o worker de serviço será descartado.

Se as promessas forem resolvidas, a instalação será bem-sucedida, e o estado do worker do serviço será alterado para 'installed' e ativado.

Ativação

Se o registro e a instalação forem bem-sucedidos, o worker será ativado e o estado dele será 'activating'. O trabalho pode ser feito durante a ativação no evento activate do worker. Uma tarefa típica nesse evento é podar caches antigos, mas, para um service worker totalmente novo, isso não é relevante no momento e será expandido quando falarmos sobre atualizações de service worker.

Para novos service workers, activate é acionado imediatamente após install ser bem-sucedido. Quando a ativação for concluída, o estado do service worker vai se tornar 'activated'. Por padrão, o novo service worker não começa a controlar a página até a próxima navegação ou atualização da página.

Como processar atualizações de service workers

Depois que o primeiro worker de serviço for implantado, ele provavelmente precisará ser atualizado mais tarde. Por exemplo, uma atualização pode ser necessária se ocorrerem mudanças no processamento de solicitações ou na lógica de pré-cache.

Quando as atualizações acontecem

Os navegadores vão verificar se há atualizações em um worker de serviço quando:

  • O usuário navega até uma página no escopo do service worker.
  • navigator.serviceWorker.register() é chamado com um URL diferente do service worker instalado atualmente. Não mude o URL de um service worker.
  • navigator.serviceWorker.register() é chamado com o mesmo URL do service worker instalado, mas com um escopo diferente. Novamente, evite isso mantendo o escopo na raiz de uma origem, se possível.
  • Quando eventos como 'push' ou 'sync' foram acionados nas últimas 24 horas, mas não se preocupe com esses eventos ainda.

Como as atualizações acontecem

Saber quando o navegador atualiza um worker de serviço é importante, mas também é importante saber "como". Supondo que o URL ou escopo de um service worker não seja alterado, um service worker instalado atualmente só será atualizado para uma nova versão se o conteúdo dele tiver mudado.

Os navegadores detectam mudanças de duas maneiras:

  • Quaisquer mudanças byte por byte nos scripts solicitadas por importScripts, se aplicável.
  • Qualquer mudança no código de nível superior do service worker, que afeta a impressão digital gerada pelo navegador.

O navegador faz muito trabalho pesado aqui. Para garantir que o navegador tenha tudo o que precisa para detectar mudanças no conteúdo de um worker do serviço, não diga ao cache HTTP para mantê-lo e não mude o nome do arquivo. O navegador realiza verificações de atualização automaticamente quando há uma navegação para uma nova página no escopo de um service worker.

Como acionar manualmente as verificações de atualização

Em relação às atualizações, a lógica de registro geralmente não muda. No entanto, uma exceção pode ser se as sessões em um site tiverem longa duração. Isso pode acontecer em aplicativos de página única em que solicitações de navegação são raras, já que o aplicativo normalmente encontra uma solicitação de navegação no início do ciclo de vida. Nessas situações, uma atualização manual pode ser acionada na linha de execução principal:

navigator.serviceWorker.ready.then((registration) => {
  registration.update();
});

Para sites tradicionais, ou em qualquer caso em que as sessões do usuário não sejam de longa duração, provavelmente não será necessário acionar atualizações manuais.

Instalação

Ao usar um bundler para gerar recursos estáticos, esses recursos vão conter hashes no nome, como framework.3defa9d2.js. Suponha que alguns desses recursos sejam pré-armazenados em cache para acesso off-line mais tarde. Isso exigiria uma atualização do worker do serviço para pré-cachear os recursos atualizados:

self.addEventListener('install', (event) => {
  const cacheKey = 'MyFancyCacheName_v2';

  event.waitUntil(caches.open(cacheKey).then((cache) => {
    // Add all the assets in the array to the 'MyFancyCacheName_v2'
    // `Cache` instance for later use.
    return cache.addAll([
      '/css/global.ced4aef2.css',
      '/css/home.cbe409ad.css',
      '/js/home.109defa4.js',
      '/js/jquery.38caf32d.js'
    ]);
  }));
});

Duas coisas são diferentes do primeiro exemplo de evento install:

  1. Uma nova instância Cache com uma chave de 'MyFancyCacheName_v2' é criada.
  2. Os nomes dos recursos pré-armazenados em cache foram alterados.

Um ponto importante é que um worker de serviço atualizado é instalado junto com o anterior. Isso significa que o serviço antigo ainda controla todas as páginas abertas e, após a instalação, o novo entra em um estado de espera até ser ativado.

Por padrão, um novo worker de serviço é ativado quando nenhum cliente está sendo controlado pelo antigo. Isso ocorre quando todas as guias abertas do site relevante são fechadas.

Ativação

Quando um service worker atualizado é instalado e a fase de espera termina, ele é ativado e o service worker antigo é descartado. Uma tarefa comum a ser realizada no evento activate de um service worker atualizado é podar caches antigos. Remova os caches antigos recebendo as chaves de todas as instâncias de Cache abertas com caches.keys e excluindo os caches que não estão em uma lista de permissões definida com caches.delete:

self.addEventListener('activate', (event) => {
  // Specify allowed cache keys
  const cacheAllowList = ['MyFancyCacheName_v2'];

  // Get all the currently active `Cache` instances.
  event.waitUntil(caches.keys().then((keys) => {
    // Delete all caches that aren't in the allow list:
    return Promise.all(keys.map((key) => {
      if (!cacheAllowList.includes(key)) {
        return caches.delete(key);
      }
    }));
  }));
});

Os caches antigos não são organizados automaticamente. Precisamos fazer isso por conta própria ou corremos o risco de exceder cotas de armazenamento. Como 'MyFancyCacheName_v1' do primeiro worker de serviço está desatualizado, a lista de permissões de cache é atualizada para especificar 'MyFancyCacheName_v2', que exclui caches com um nome diferente.

O evento activate será concluído depois que o cache antigo for removido. Nesse ponto, o novo service worker vai assumir o controle da página, substituindo o antigo.

O ciclo de vida continua

Seja para usar o Workbox para lidar com a implantação e as atualizações do service worker ou a API Service Worker diretamente, é importante entender o ciclo de vida do service worker. Com esse entendimento, os comportamentos do worker do serviço devem parecer mais lógicos do que misteriosos.

Para quem tem interesse em se aprofundar no assunto, confira este artigo de Jake Archibald. Há muitas nuances em como todo o ciclo de vida do serviço funciona, mas é possível conhecê-lo, e esse conhecimento vai longe ao usar o Workbox.