A vida de um service worker

É difícil saber o que os service workers estão fazendo sem entender o ciclo de vida deles. O funcionamento interno delas vai parecer opaco, até mesmo arbitrário. Isso ajuda a lembrar que, como em qualquer outra API de navegador, os comportamentos dos service workers são bem definidos, especificados, e viabilizam aplicativos off-line, além de facilitar as atualizações sem prejudicar a experiência do usuário.

Antes de mergulhar 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 de vida 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 é aquela que permite que um service worker intercepte solicitações de rede em nome dele. O service worker está presente e pode fazer o trabalho para a página dentro de um determinado escopo.

Escopo

O escopo de um service worker é determinado pela localização dele em um servidor da Web. Se um service worker for executado em uma página localizada em /subdir/index.html e estiver em /subdir/sw.js, o escopo do service worker seja /subdir/. Para ver o conceito de escopo em ação, veja este exemplo:

  1. Acessar https://service-worker-scope-viewer.glitch.me/subdir/index.html. Será exibida uma mensagem 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 agora está ativo, ela está controlando a página. Um formulário contendo o escopo do service worker estado atual, e seu URL ficará visível. Observação: atualizar a página não tem nada a ver com o escopo, mas sim o ciclo de vida do service worker, que será explicado mais adiante.
  3. Agora acesse https://service-worker-scope-viewer.glitch.me/index.html. Mesmo que um service worker esteja registrado nessa origem, ainda haverá uma mensagem informando que não há service worker atual. Isso ocorre porque essa página não está no escopo do service worker registrado.

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

Confira acima como o escopo funciona por padrão, mas o escopo máximo permitido pode ser substituído pela configuração Cabeçalho de resposta do Service-Worker-Allowed, além de transmitir scope ao método register.

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

Cliente

Quando dizem que um service worker está controlando uma página, na verdade, na verdade está controlando um cliente. Um cliente é qualquer página aberta cujo URL esteja no escopo desse service worker. Especificamente, essas são instâncias de uma WindowClient.

O ciclo de vida de um novo service worker

Para que um service worker controle uma página, ela precisa existir primeiro, por assim dizer. Vamos começar com o que acontece quando um novo service worker é implantado em um site sem service 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 esteja totalmente carregada antes de registrar uma. Isso evita contenção de largura de banda se o service worker armazena algo em cache previamente.
  2. O service worker tem bom suporte, uma verificação rápida ajuda a evitar erros em navegadores que não são compatíveis.
  3. Quando a página estiver totalmente carregada e se o service worker for compatível, registre /sw.js.

Alguns pontos importantes para entender são:

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

Quando o registro terminar, a instalação será iniciada.

Instalação

Um service worker dispara seu install após a inscrição. install é chamado apenas uma vez por service worker e não é acionado novamente até que seja 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 em cache. Teremos muitas oportunidades de falar sobre pré-armazenamento em cache mais tarde, então vamos nos concentrar no papel event.waitUntil event.waitUntil aceita uma promessa. e espera até que a promessa seja resolvida. Nesse exemplo, essa promessa faz duas coisas assíncronas:

  1. Cria uma nova instância de Cache chamada 'MyFancyCache_v1'.
  2. Após a criação do cache, um conjunto de URLs de recursos é pré-armazenado em cache usando seu método addAll.

A instalação falhará se as promessas transmitidas para event.waitUntil forem rejeitada. Se isso acontecer, o service worker será descartado.

Se as promessas forem resolvidas, será bem-sucedida, e o estado do service worker mudará para 'installed' e, em seguida, será ativado.

Ativação

Se o registro e a instalação forem concluídos, o service worker é ativado e o estado dele se torna 'activating'. O trabalho pode ser feito durante a ativação no activate evento. Uma tarefa típica nesse evento é limpar caches antigos, mas, para um novo service worker, se isso não for relevante para o momento, e será ampliada quando abordarmos as atualizações dos service workers.

Para novos service workers, activate é disparado imediatamente depois que install for bem-sucedido. Quando a ativação for concluída, o estado do service worker se torna 'activated'. Observe que, por padrão, o novo service worker não começará a controlar a página até a próxima navegação ou atualização de página.

Como gerenciar atualizações do service worker

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

Quando as atualizações acontecem

Os navegadores verificam se há atualizações para um service worker quando:

Como as atualizações acontecem

Saber quando o navegador atualiza um service worker é importante, mas o “como” também. Supondo que o URL ou o escopo de um service worker não mude, um service worker atualmente instalado só atualiza para uma nova versão se seu conteúdo for alterado.

Os navegadores detectam alterações de duas maneiras:

  • Qualquer alteração byte por byte a scripts solicitados pelo importScripts, se aplicável.
  • Todas as alterações no código de nível superior do service worker que afeta a impressão digital gerada pelo navegador.

O navegador faz um grande trabalho aqui. Para garantir que o navegador tenha tudo o que precisa para detectar alterações no conteúdo de um service worker com segurança, não instrui o cache HTTP a mantê-lo, nem altera o nome do arquivo. O navegador executa 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 verificações de atualização

Com relação às atualizações, a lógica de registro geralmente não deve mudar. No entanto, uma exceção seria se as sessões em um site fossem de longa duração. Isso pode acontecer em aplicativos de página única, em que solicitações de navegação são raras, porque 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 de usuário não são de longa duração, o acionamento de atualizações manuais provavelmente não é necessário.

Instalação

Ao usar um bundler para gerar recursos estáticos, esses recursos terão 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 service worker para pré-armazenar em cache 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 mostrado anteriormente:

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

Uma coisa a ser observada é que um service worker atualizado é instalado junto com o anterior. Isso significa que o antigo service worker ainda controla 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 service worker será ativado quando nenhum cliente estiver 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, ela é ativada e o service worker antigo é descartado. Uma tarefa comum a ser executada no evento activate de um service worker atualizado é limpar caches antigos. Remova os caches antigos conseguindo as chaves de todas as instâncias Cache abertas com caches.keys e excluir 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 se organizam. Precisamos fazer isso por conta própria ou corremos o risco de exceder cotas de armazenamento. Como o 'MyFancyCacheName_v1' do primeiro service worker está desatualizado, a lista de permissões de cache será 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. Neste ponto, o novo service worker vai assumir o controle da página, finalmente a substituição do antigo!

O ciclo de vida continua

Se o Workbox é usado para lidar com a implantação e as atualizações do service worker ou se a API Service Worker for usada diretamente, é preciso entender o ciclo de vida do service worker. Com esse entendimento, os comportamentos dos service workers vão parecer mais lógicos do que misteriosos.

Se você quiser se aprofundar nesse assunto, vale a pena conferir este artigo de Jake Archibald. Há várias nuances no ciclo de vida do serviço, mas é conhecido, e esse conhecimento será muito útil ao usar o Workbox.