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 deles parecerá opaco, mesmo arbitrário. É importante lembrar que, como qualquer outra API de navegador, os comportamentos dos service workers são bem definidos, especificados e possibilitam aplicativos off-line, além de facilitar as atualizações sem interromper a experiência do usuário.

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

Definição de termos

Antes de entrar no ciclo de vida do service worker, é importante definir alguns termos sobre como esse ciclo funciona.

Controle e escopo

A ideia de controle é crucial para entender como os service workers operam. 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 na página em 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 dele será /subdir/. Para ver o conceito de escopo em ação, confira este exemplo:

  1. Acesse https://service-worker-scope-viewer.glitch.me/subdir/index.html. Vai aparecer 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, ele está controlando a página. Será mostrado um formulário contendo o escopo, o estado atual e o URL do service worker. Observação: atualizar a página não tem a ver com o escopo, mas com o ciclo de vida do service worker, que será explicado mais tarde.
  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 a página não está dentro do 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.

Veja acima 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 bom motivo para limitar o escopo do service worker a um subconjunto de uma origem, carregue um service worker no diretório raiz do servidor da Web para que o escopo seja o mais amplo possível e não se preocupe com o cabeçalho Service-Worker-Allowed. É muito mais simples para todos.

Cliente

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

O ciclo de vida de um novo service worker

Para que um service worker controle uma página, primeiro ela precisa existir, por assim dizer. Vamos começar com o que acontece quando um novo service worker é implantado em um site sem um service worker ativo.

Inscrição

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 um. Isso evita a contenção de largura de banda caso o service worker armazene alguma coisa em cache.
  2. Embora o service worker tenha um bom suporte, uma verificação rápida ajuda a evitar erros nos 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.

É importante entender algumas coisas:

  • Os service workers estão disponíveis 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 operam em um escopo. Aqui, o escopo é toda a origem, já que foi carregado do diretório raiz.
  • Quando o registro começar, o estado do service worker será definido como 'installing'.

A instalação começa assim que o registro é concluído.

Instalação

Um service worker dispara o evento install após o registro. install é chamado apenas uma vez por service worker e não será 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 em cache os recursos. Teremos muitas oportunidades para falar sobre o pré-armazenamento em cache mais tarde, então vamos nos concentrar na função de event.waitUntil. event.waitUntil aceita uma promessa e espera até que ela seja resolvida. Neste exemplo, essa promessa faz duas coisas assíncronas:

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

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

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

Na prática

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

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

Como gerenciar atualizações do service worker

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

Quando as atualizações são feitas

Os navegadores verificarão 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". Supondo que o URL ou o escopo de um service worker não seja alterado, um service worker atualmente instalado só será atualizado para uma nova versão se o conteúdo tiver sido alterado.

Os navegadores detectam alterações de algumas maneiras:

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

O navegador faz uma grande parte do trabalho aqui. Para garantir que o navegador tenha tudo o que precisa para detectar de maneira confiável alterações no conteúdo de um service worker, não diga ao cache HTTP para segurá-lo e não altere o nome do arquivo. O navegador executa automaticamente verificações de atualização quando há uma navegação para uma nova página dentro do escopo de um service worker.

Acionamento manual de verificações de atualização

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

Instalação

Ao usar um bundler para gerar recursos estáticos, esses recursos contêm 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 anterior:

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

Uma coisa a observar é que um service worker atualizado é instalado junto com o anterior. Isso significa que o service worker antigo ainda está no controle de todas as páginas abertas e, após a instalação, o novo vai entrar 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.

Na prática

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 executada no evento activate de um service worker atualizado é remover caches antigos. Remova os caches antigos acessando as chaves de todas as instâncias Cache abertas com caches.keys e excluindo os que não estiverem 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);
      }
    }));
  }));
});

Caches antigos não organizam as informações. Precisamos fazer isso por conta própria ou correremos o risco de exceder as cotas de armazenamento. Como o 'MyFancyCacheName_v1' do primeiro service worker está desatualizado, a lista de permissões do cache será atualizada para especificar 'MyFancyCacheName_v2', o que exclui os caches com um nome diferente.

O evento activate será concluído após a remoção do cache antigo. Neste ponto, o novo service worker assumirá o controle da página e, por fim, substituirá a antiga.

O ciclo de vida continua

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

Para aqueles interessados em se aprofundar nesse assunto, vale a pena conferir este artigo de Jake Archibald (link em inglês). Há várias nuances na forma como ocorre todo o ciclo de vida do serviço, mas isso é conhecível, e esse conhecimento pode ser muito útil quando se usa o Workbox.