O ciclo de vida do service worker é a parte mais complicada. Se você não souber o que ele está tentando fazer e quais são os benefícios, pode parecer que ele está lutando contra você. Mas, depois de entender como ele funciona, você pode oferecer atualizações simples e discretas aos usuários, combinando o melhor dos padrões nativos e da Web.
Esta é uma investigação, mas os tópicos no início de cada seção cobrem a maior parte do que você precisa saber.
A intent
O objetivo do ciclo de vida é:
- Possibilite o modo off-line.
- Permitir que um novo service worker se prepare sem interromper o atual.
- Garanta que uma página no escopo seja controlada pelo mesmo service worker (ou nenhum service worker) o tempo todo.
- Verifique se há apenas uma versão do site em execução por vez.
Essa última é muito importante. Sem service workers, os usuários podem carregar uma guia do seu site e, depois, abrir outra. Isso pode resultar em duas versões do seu site em execução ao mesmo tempo. Às vezes, isso não é um problema, mas, se você estiver lidando com armazenamento, pode acabar com duas guias que têm opiniões muito diferentes sobre como o armazenamento compartilhado deve ser gerenciado. Isso pode resultar em erros ou, pior, perda de dados.
O primeiro service worker
Para resumir:
- O evento
installé o primeiro que um service worker recebe e só acontece uma vez. - Uma promessa transmitida para
installEvent.waitUntil()indica a duração e o sucesso ou a falha da instalação. - Um service worker não vai receber eventos como
fetchepushaté que a instalação seja concluída e ele se torne "ativo". - Por padrão, as buscas de uma página não passam por um service worker, a menos que a própria solicitação da página tenha passado por um service worker. Portanto, você precisará atualizar a página para ver os efeitos do service worker.
clients.claim()pode substituir esse padrão e assumir o controle de páginas não controladas.
Veja este HTML:
<!DOCTYPE html>
An image will appear here in 3 seconds:
<script>
navigator.serviceWorker.register('/sw.js')
.then(reg => console.log('SW registered!', reg))
.catch(err => console.log('Boo!', err));
setTimeout(() => {
const img = new Image();
img.src = '/dog.svg';
document.body.appendChild(img);
}, 3000);
</script>
Ele registra um service worker e adiciona a imagem de um cachorro após 3 segundos.
Este é o service worker, sw.js:
self.addEventListener('install', event => {
console.log('V1 installing…');
// cache a cat SVG
event.waitUntil(
caches.open('static-v1').then(cache => cache.add('/cat.svg'))
);
});
self.addEventListener('activate', event => {
console.log('V1 now ready to handle fetches!');
});
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
// serve the cat SVG from the cache if the request is
// same-origin and the path is '/dog.svg'
if (url.origin == location.origin && url.pathname == '/dog.svg') {
event.respondWith(caches.match('/cat.svg'));
}
});
Ele armazena em cache uma imagem de um gato e a exibe sempre que há uma solicitação para /dog.svg. No entanto, se você executar o exemplo acima, vai ver um cachorro na primeira vez que carregar a página. Atualize a página para ver o gato.
Escopo e controle
O escopo padrão de um registro de service worker é ./ em relação ao URL do script. Isso significa que, se você registrar um service worker em //example.com/foo/bar.js, ele terá um escopo padrão de //example.com/foo/.
Chamamos páginas, workers e workers compartilhados de clients. O service worker só pode controlar clientes que estão no escopo. Depois que um cliente é "controlado", as buscas dele passam pelo service worker no escopo. É possível detectar se um cliente é controlado por navigator.serviceWorker.controller, que será nulo ou uma instância de service worker.
Fazer o download, analisar e executar
Seu primeiro service worker é baixado quando você chama .register(). Se o script não for transferido por download, analisado ou gerar um erro na execução inicial, a promessa de registro será rejeitada e o service worker será descartado.
O DevTools do Chrome mostra o erro no console e na seção de service worker da guia do aplicativo:
Instalar
O primeiro evento que um service worker recebe é install. Ele é acionado assim que o worker é executado e é chamado apenas uma vez por service worker. Se você alterar o script do service worker, o navegador vai considerar que é um service worker diferente e ele vai receber o próprio evento install. Vou falar sobre as atualizações em detalhes mais adiante.
O evento install é sua chance de armazenar em cache tudo o que você precisa antes de controlar os clientes. A promessa que você transmite para event.waitUntil() informa ao navegador quando a instalação é concluída e se ela foi bem-sucedida.
Se a promessa for rejeitada, isso indica que a instalação falhou, e o navegador descarta o service worker. Ele nunca vai controlar os clientes. Isso significa que podemos confiar que cat.svg estará presente no cache nos eventos fetch. É uma dependência.
Ativar
Quando o service worker estiver pronto para controlar clientes e processar eventos funcionais, como push e sync, você vai receber um evento activate. Mas isso não significa que a página que chamou .register() será controlada.
Na primeira vez que você carregar a demonstração, mesmo que dog.svg seja solicitado muito depois da ativação do service worker, ele não vai processar a solicitação, e você ainda vai ver a imagem do cachorro. O padrão é a consistência. Se a página for carregada sem um service worker, os sub-recursos também não serão carregados. Se você carregar a demonstração uma segunda vez (ou seja, atualizar a página), ela será controlada. A página e a imagem vão passar por eventos fetch, e você vai ver um gato.
clients.claim
Para assumir o controle de clientes não controlados, chame clients.claim() no service worker depois que ele for ativado.
Confira uma variação da demonstração acima que chama clients.claim() no evento activate. Você deve ver um gato na primeira vez. Eu digo "deve" porque isso depende do tempo. Você só vai ver um gato se o service worker for ativado e clients.claim() entrar em vigor antes que a imagem tente carregar.
Se você usar o service worker para carregar páginas de forma diferente do que seria feito pela rede, o clients.claim() pode ser problemático, já que o service worker acaba controlando alguns clientes que foram carregados sem ele.
Como atualizar o service worker
Para resumir:
- Uma atualização é acionada se uma das seguintes situações ocorrer:
- Uma navegação para uma página no escopo.
- Um evento funcional, como
pushesync, a menos que tenha ocorrido uma verificação de atualização nas últimas 24 horas. - Chamar
.register()somente se o URL do service worker tiver mudado. No entanto, evite mudar o URL do worker.
- A maioria dos navegadores, incluindo o Chrome 68 e versões mais recentes, ignora os cabeçalhos de armazenamento em cache por padrão ao verificar se há atualizações do script do service worker registrado. Eles ainda respeitam os cabeçalhos de armazenamento em cache ao buscar recursos carregados em um service worker usando
importScripts(). É possível modificar esse comportamento padrão definindo a opçãoupdateViaCacheao registrar o service worker. - O service worker é considerado atualizado se for diferente em bytes do que o navegador já tem. (Estamos estendendo isso para incluir também scripts/módulos importados.)
- O service worker atualizado é iniciado junto com o atual e recebe o próprio evento
install. - Se o novo worker tiver um código de status que não seja "ok" (por exemplo, 404), não for analisado, gerar um erro durante a execução ou for rejeitado durante a instalação, ele será descartado, mas o atual vai permanecer ativo.
- Depois de instalado, o worker atualizado vai
waitaté que o worker atual esteja controlando zero clientes. Observe que os clientes se sobrepõem durante uma atualização. self.skipWaiting()impede a espera, o que significa que o service worker é ativado assim que a instalação é concluída.
Digamos que mudamos o script do service worker para responder com a imagem de um cavalo em vez de um gato:
const expectedCaches = ['static-v2'];
self.addEventListener('install', event => {
console.log('V2 installing…');
// cache a horse SVG into a new cache, static-v2
event.waitUntil(
caches.open('static-v2').then(cache => cache.add('/horse.svg'))
);
});
self.addEventListener('activate', event => {
// delete any caches that aren't in expectedCaches
// which will get rid of static-v1
event.waitUntil(
caches.keys().then(keys => Promise.all(
keys.map(key => {
if (!expectedCaches.includes(key)) {
return caches.delete(key);
}
})
)).then(() => {
console.log('V2 now ready to handle fetches!');
})
);
});
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
// serve the horse SVG from the cache if the request is
// same-origin and the path is '/dog.svg'
if (url.origin == location.origin && url.pathname == '/dog.svg') {
event.respondWith(caches.match('/horse.svg'));
}
});
Confira uma demonstração do que foi descrito acima. Você ainda vai ver a imagem de um gato. Veja por quê…
Instalar
Observe que mudei o nome do cache de static-v1 para static-v2. Isso significa que posso configurar o novo cache sem substituir as coisas no atual, que o service worker antigo ainda está usando.
Esse padrão cria caches específicos de versão, semelhantes aos recursos que um app nativo agruparia com o executável. Também é possível ter caches que não são específicos da versão, como avatars.
Aguardando
Depois de instalado, o service worker atualizado atrasa a ativação até que o service worker atual não esteja mais controlando os clientes. Esse estado é chamado de "em espera" e é como o navegador garante que apenas uma versão do service worker seja executada por vez.
Se você executou a demonstração atualizada, ainda vai ver a foto de um gato, porque o worker V2 ainda não foi ativado. Você pode ver o novo service worker aguardando na guia "Application" do DevTools:
Mesmo que você tenha apenas uma guia aberta para a demonstração, atualizar a página não é suficiente para que a nova versão assuma o controle. Isso ocorre devido ao funcionamento das navegações do navegador. Quando você navega, a página atual não desaparece até que os cabeçalhos de resposta sejam recebidos. Mesmo assim, ela pode permanecer se a resposta tiver um cabeçalho Content-Disposition. Devido a essa sobreposição, o service worker atual sempre controla um cliente durante uma atualização.
Para receber a atualização, feche ou saia de todas as guias que usam o service worker atual. Em seguida, quando você navegar para a demonstração novamente, o cavalo vai aparecer.
Esse padrão é semelhante ao modo como o Chrome é atualizado. As atualizações do Chrome são baixadas em segundo plano, mas só são aplicadas quando o navegador é reiniciado. Enquanto isso, você pode continuar usando a versão atual sem interrupções. No entanto, isso é um problema durante o desenvolvimento, mas o DevTools tem maneiras de facilitar, que vou abordar mais adiante neste artigo.
Ativar
Isso é acionado quando o service worker antigo é removido e o novo pode controlar os clientes. Esse é o momento ideal para fazer o que não era possível enquanto o worker antigo ainda estava em uso, como migrar bancos de dados e limpar caches.
Na demonstração acima, mantenho uma lista de caches que espero que estejam lá e, no evento activate, me livro de todos os outros, o que remove o cache static-v1 antigo.
Se você transmitir uma promessa para event.waitUntil(), ela vai armazenar em buffer os eventos funcionais (fetch, push, sync etc.) até que a promessa seja resolvida. Portanto, quando o evento fetch é disparado, a ativação está totalmente concluída.
Pular a fase de espera
A fase de espera significa que você está executando apenas uma versão do site por vez. No entanto, se você não precisar desse recurso, poderá fazer com que o novo service worker seja ativado mais cedo chamando self.skipWaiting().
Isso faz com que o service worker remova o worker ativo atual e se ative assim que entrar na fase de espera (ou imediatamente, se já estiver nela). Isso não faz com que o worker pule a instalação, apenas aguarde.
Não importa quando você chama skipWaiting(), desde que seja durante ou antes da espera. É comum chamá-lo no evento install:
self.addEventListener('install', event => {
self.skipWaiting();
event.waitUntil(
// caching etc
);
});
No entanto, talvez você queira chamá-lo como resultado de um postMessage() para o service worker. Por exemplo, você quer skipWaiting() após uma interação do usuário.
Confira uma demonstração que usa skipWaiting(). Você vai ver a imagem de uma vaca sem precisar sair da página. Como o clients.claim(), é uma corrida. Portanto, você só vai ver a vaca se o novo service worker buscar, instalar e ativar antes que a página tente carregar a imagem.
Atualizações manuais
Como mencionei antes, o navegador verifica se há atualizações automaticamente após navegações e eventos funcionais, mas você também pode acioná-las manualmente:
navigator.serviceWorker.register('/sw.js').then(reg => {
// sometime later…
reg.update();
});
Se você espera que o usuário use seu site por um longo período sem recarregar, chame update() em um intervalo (por exemplo, a cada hora).
Evite mudar o URL do script do service worker
Se você leu minha postagem sobre as práticas recomendadas de armazenamento em cache, considere dar a cada versão do service worker um URL exclusivo. Não faça isso! Isso geralmente é uma prática ruim para service workers. Basta atualizar o script no local atual.
Isso pode causar um problema como este:
index.htmlregistrasw-v1.jscomo um service worker.- O
sw-v1.jsarmazena em cache e disponibiliza oindex.htmlpara que ele funcione primeiro off-line. - Você atualiza
index.htmlpara que ele registre seu novo e brilhantesw-v2.js.
Se você fizer isso, o usuário nunca vai receber sw-v2.js, porque o sw-v1.js está veiculando a versão antiga de index.html do cache. Você se colocou em uma posição em que precisa atualizar o service worker para atualizar o service worker. Eca.
No entanto, para a demonstração acima, mudei o URL do service worker. Isso é para que, para fins de demonstração, você possa alternar entre as versões. Não é algo que eu faria em produção.
Como facilitar o desenvolvimento
O ciclo de vida do service worker foi criado pensando no usuário, mas durante o desenvolvimento ele é um pouco incômodo. Felizmente, existem algumas ferramentas para ajudar:
Atualizar ao recarregar
Este é o meu favorito.
Isso muda o ciclo de vida para ser mais fácil para os desenvolvedores. Cada navegação vai:
- Busque novamente o service worker.
- Instale-o como uma nova versão, mesmo que seja idêntico em bytes, o que significa que o evento
installé executado e os caches são atualizados. - Pule a fase de espera para que o novo service worker seja ativado.
- Navegue pela página.
Isso significa que você vai receber as atualizações em cada navegação (incluindo a atualização) sem precisar recarregar duas vezes ou fechar a guia.
Pular espera
Se você tiver um worker em espera, clique em "pular espera" no DevTools para promovê-lo imediatamente a "ativo".
Shift-recarregar
Se você forçar o recarregamento da página (shift-reload), o service worker será totalmente ignorado. Será descontrolado. Esse recurso está na especificação, então funciona em outros navegadores que oferecem suporte a service workers.
Como lidar com atualizações
O service worker foi projetado como parte da Web extensível. A ideia é que nós, como desenvolvedores de navegadores, reconhecemos que não somos melhores no desenvolvimento web do que os desenvolvedores web. Por isso, não devemos fornecer APIs de alto nível restritas que resolvam um problema específico usando padrões que nós gostamos. Em vez disso, devemos dar acesso às entranhas do navegador e permitir que você faça o que quiser, da maneira que funciona melhor para seus usuários.
Para permitir o maior número possível de padrões, todo o ciclo de atualização é observável:
navigator.serviceWorker.register('/sw.js').then(reg => {
reg.installing; // the installing worker, or undefined
reg.waiting; // the waiting worker, or undefined
reg.active; // the active worker, or undefined
reg.addEventListener('updatefound', () => {
// A wild service worker has appeared in reg.installing!
const newWorker = reg.installing;
newWorker.state;
// "installing" - the install event has fired, but not yet complete
// "installed" - install complete
// "activating" - the activate event has fired, but not yet complete
// "activated" - fully active
// "redundant" - discarded. Either failed install, or it's been
// replaced by a newer version
newWorker.addEventListener('statechange', () => {
// newWorker.state has changed
});
});
});
navigator.serviceWorker.addEventListener('controllerchange', () => {
// This fires when the service worker controlling this page
// changes, eg a new worker has skipped waiting and become
// the new active worker.
});
O ciclo de vida continua
Como você pode ver, vale a pena entender o ciclo de vida do service worker. Com esse conhecimento, os comportamentos do service worker vão parecer mais lógicos e menos misteriosos. Esse conhecimento vai dar mais confiança para você implantar e atualizar service workers.