Você já esteve em um site e notou uma notificação que sugere que há uma nova versão do site disponível? Recentemente, visitei a Caixa de entrada do Google e notei uma notificação um pouco como a imagem abaixo:
Já criei vários Progressive Web Apps que simplesmente atualizam o Service Worker silenciosamente para o usuário em segundo plano, porém, eu realmente gosto dessa abordagem - especialmente para um aplicativo planejado para trabalhar offline-first. Se você já tentou criar um aplicativo web completamente offline-first, sabe como pode ser complicado fazer alterações no cache dos usuários quando houver atualizações no site e o usuário tiver conectividade. É aqui que uma notificação pop-up, como a da Caixa de entrada do Google, fornece ao usuário um meio de sempre ter a versão mais recente dos recursos armazenados em cache. Isso me fez pensar como eu poderia construir algo semelhante e, acontece que é um pouco mais complicado do que parece, mas não é impossível!
Neste artigo, mostrarei como adicionar uma notificação ao seu site e exibi-la sempre que houver uma nova versão disponível do seu Service Worker. Você também aprenderá como atualizar a página, para que o usuário esteja atualizado e tenha a versão mais recente de qualquer arquivo em cache. Este artigo é um pouco longo, então aperte os cintos e fique confortável!
Projeto de Exemplo
Neste exemplo, vou usar uma página web bem básica que consiste em três ativos:
index.html
dog.jpg
-
service-worker.js
$ mkdir exemplo-service-worker
$ cd $_
$ touch index.html
$ touch service-worker.js
Para começar, o HTML da minha página web parece um pouco com o código a seguir:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>PWA - Novo Service Worker disponível</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
body {
margin: 0;
}
img {
display: block;
max-width: 100%;
}
#notification {
background: #444;
bottom: 8px;
color: #fff;
display: none;
padding: 16px;
position: absolute;
right: 8px;
width: 240px;
}
#notification.show {
display: block;
}
</style>
</head>
<body>
<img src="./dog.jpg" />
<!-- A notificação que iremos mostrar -->
<div id="notification">
Uma nova versão está disponível. Clique <a id="reload">aqui</a> para
atualizar.
</div>
</body>
<script>
if ("serviceWorker" in navigator) {
navigator.serviceWorker
.register("./service-worker.js")
.then(function(registration) {
// SUCESSO - ServiceWorker Registrado
console.log(
"ServiceWorker registrado com sucesso no escopo: ",
registration.scope
);
})
.catch(function(err) {
// ERRO - Falha ao registrar o ServiceWorker
console.log("Falha ao registrar o ServiceWorker: ", err);
});
}
</script>
</html>
Na página web acima, você pode perceber que adicionei um código HTML padrão e o registro de um Service Worker. Vamos adicionar um pouco de magia agora! No arquivo service-worker.js
adicione o seguinte código:
const cacheName = "firstVersion";
self.addEventListener("install", event => {
event.waitUntil(
caches.open(cacheName).then(cache => cache.addAll(["./dog.jpg"]))
);
});
self.addEventListener("fetch", function(event) {
event.respondWith(
caches.match(event.request).then(function(response) {
if (response) {
return response;
}
return fetch(event.request);
})
);
});
No código acima, adicionamos a funcionalidade básica de armazenamento em cache ao nosso Service Worker. Depois que ele for instalado, e cada vez que um usuário fizer a solicitação para a imagem dog.jpg, o Service Worker irá buscar do cache e exibir instantaneamente para o usuário. Se você não estiver familiarizado com o código acima, recomendo dar uma olhada neste artigo para obter mais informações. Ele o levará através dos princípios básicos e ajudará você a entender como o armazenamento em cache do Service Worker funciona.
Neste momento, se abrirmos a página da web, ela ficará um pouco parecida com a imagem abaixo:
Até aí tudo bem, mas nós temos uma página web que realmente não faz muita coisa! Para completar as peças do quebra-cabeça, precisamos atualizar nosso código para que ele notifique o usuário quando houver uma mudança no próprio Service Worker. Antes de mergulharmos mais fundo, vamos dar uma olhada no fluxo básico que precisa acontecer:
No diagrama acima, você pode perceber que várias etapas precisam ser realizadas antes de termos um produto atualizado. Em primeiro lugar, o navegador verifica se houve atualização no arquivo do service worker. Se houver uma atualização disponível, mostramos uma notificação na tela, caso contrário, não fazemos nada. Quando o usuário clica na notificação, enviamos uma mensagem para o service worker e informamos que ele pule a espera e se torne o service worker ativo. Quando a instalação for concluída, recarregamos a página e nosso novo service worker está no controle!
Ufa! Finalmente! 🎉😆
Embora possa parecer confuso, ao final deste artigo, o fluxo acima fará um pouco mais de sentido. Vamos pegar o que aprendemos no fluxo acima e aplicar as alterações no código em nossa página web. Vamos fazer as alterações abaixo em nosso arquivo index.html
:
...
<script>
let newWorker;
// O evento de clique na notificação
document.getElementById("reload").addEventListener("click", function() {
newWorker.postMessage({ action: "skipWaiting" });
});
if ("serviceWorker" in navigator) {
navigator.serviceWorker
.register("./service-worker.js") // [A]
.then(function(registration) {
registration.addEventListener("updatefound", () => { // [B]
// Uma atualização no Service Worker foi encontrada, instalando...
newWorker = registration.installing; // [C]
newWorker.addEventListener("statechange", () => {
// O estado do Service Worker mudou?
switch (newWorker.state) {
case "installed": {
// Existe um novo Service Worker disponível, mostra a notificação
if (navigator.serviceWorker.controller) {
let notification = document.getElementById("notification");
notification.className = "show";
break;
}
}
}
});
});
// SUCESSO - ServiceWorker Registrado
console.log(
"ServiceWorker registrado com sucesso no escopo: ",
registration.scope
);
})
.catch(function(err) {
// ERRO - Falha ao registrar o ServiceWorker
console.log("Falha ao registrar o ServiceWorker: ", err);
});
}
</script>
...
Wow! O código na página index.html
Cresceu bastante! Vamos dividi-lo passo a passo para entender melhor o fluxo.
Depois de registrar o service worker ([A]), adicionamos um eventListener ao evento .updateFound
([B]). Este evento é acionado sempre que a propriedade ServiceWorkerRegistration.installing ([C]) adquire um novo Service Worker. Isso determinará se houve alguma alteração no arquivo do service worker e ocorre quando o usuário recarrega ou retorna à página web. O navegador tem uma maneira prática de verificar o conteúdo do arquivo service-worker.js
e, mesmo que tenha sido alterado apenas por um byte, ele será tratado como uma nova versão.
Se uma nova versão for descoberta, o evento .updateFound
([B]) será acionado. Se esse evento for disparado, precisaremos verificar se um novo Service Worker foi adquirido e atribuí-lo a uma nova variável ([C]), pois usaremos isso em um estágio posterior.
Agora que determinamos que há um novo service worker esperando para ser instalado, podemos exibir uma notificação na parte inferior de nossa página notificando o usuário de que há uma nova versão disponível:
Se você se lembrar do diagrama no início deste artigo, você se lembrará de que ainda precisamos concluir as etapas 3 e 4 para que o novo Service Worker comece a controlar a página. Para a etapa 3, precisamos adicionar funcionalidades a notificação, para que, quando o usuário clicar em atualizar, nós enviamos um .postMessage()
para o nosso Service Worker pular a fase de espera. Lembre-se de que você não pode se comunicar diretamente com um Service Worker do cliente, precisamos usar o método .postMessage()
para enviar uma mensagem para ele (seja um Window
, Worker
ou SharedWorker
). A mensagem é recebida no evento "message"
no navigator.serviceWorker
.
O código dentro do arquivo service-worker.js deve ser atualizado para responder ao evento de mensagem:
self.addEventListener("message", function(event) {
if (event.data.action === "skipWaiting") {
self.skipWaiting();
}
});
Estamos quase lá! Em nossa quarta e última etapa, precisamos que nossa página web recarregue e ative o novo Service Worker. Para fazer isso, precisamos atualizar a página index.html
e recarregar a página assim que o evento controllerchange
for disparado:
...
<script>
...
let refreshing;
// Esse evento será chamado quando o Service Worker for atualizado
// Aqui estamos recarregando a página
navigator.serviceWorker.addEventListener("controllerchange", function() {
if (refreshing) {
return;
}
window.location.reload();
refreshing = true;
});
</script>
...
É isso aí! Agora você tem um exemplo totalmente funcional! 👏😎
O Resultado
Para testar isso em ação, inicie o projeto no seu localhost e navegue para a página index.html
. Usando o Google Chrome e DevTools, fica bem fácil testar nosso Service Worker. Abrindo o DevTools, podemos ir até a guia Application e com a opção de menu Service Workers selecionada, você deve observar que o nosso Service Worker está instalado na página atual.
Esse é o resultado que esperávamos, o Service Worker está instalado e controlando a página. Cada vez que atualizamos a página, recuperamos a imagem do cachorro do cache ao invés da rede.
Para simular uma atualização no nosso service worker, farei uma pequena alteração no arquivo service-worker.js
:
const cacheName = "secondVersion";
No código acima, simplesmente atualizei o nome do cache para secondVersion
. Essa pequena alteração permitirá que o navegador saiba que temos um novo Service Worker para o rock and roll. Ao atualizar a página, veremos a notificação de que há uma versão mais recente disponível. Usando o DevTools do Chrome, posso ver o novo Service Worker esperando para ser ativado; observe que a seção Status agora tem duas versões, cada uma com um status diferente:
Se você clicar no link de atualização na barra de notificações em nossa página web, o novo Service Worker será instalado e controlará a página. Você pode verificar isso no DevTools e indo para o guia Application. Você poderá notar que o novo Service Worker está instalado e controlando a página. Você pode ver isso na imagem abaixo, o número da versão na seção Status:
Conclusão
Usar uma técnica como essa (mostrando uma notificação) é uma ótima maneira de garantir que você mantenha seu Progressive Web App atualizado e com toda a mágica do armazenamento em cache, ao mesmo tempo em que mantém viva a versão mais recente do seu service worker!
Se você gostaria de ver o código completo para este exemplo, por favor, dirija-se ao repositório em github.com/deanhume/pwa-update-available.
Eu tenho que admitir, levei um tempo para descobrir tudo isso, e eu não poderia ter feito isso sem os artigos a seguir. Eu recomendo a leitura caso você queira aprender mais:
Créditos ⭐️
- How to display a "new version available" for a Progressive Web App, escrito originalmente por Dean Hume
Top comments (3)
Exatamente o que eu procurava.
Obrigado
Olá. Parabéns pelo artigo.
No inicio você fala sobre atualizar o pwa silenciosamente.
Pode explicar como é feito este procedimento?
fala Guilherme, uma atualização do Service Worker é acionada automaticamente pelo navegador assim que o arquivo de sw for alterado,
Podemos reunir alguns outros motivos também:
push
ousync
ou uma verificação de atualização do arquivo de sw (pelo navegador) nas últimas 24 horas.register()
, no caso da URL do sw ter sido alterada, você deve evitar mudar a url do seu sw em caso de trabalho offline (ex:index.html
registrasw-v1.js
como sw, o sw faz o cache doindex.html
para trabalhar offline, você atualiza oindex.html
para usarsw-v2.js
, o usuário nunca receberá osw-v2.js
)Quando uma atualização acontece, o comportamento padrão é:
install
, rodando em um novo contextoself.skipWaiting()
para remover essa espera e assumir o controle da página assim que o sw for instaladovou me concentrar no arquivo de sw nos exemplos a seguir,
vamos supor que instalamos o seguinte sw:
agora, vamos atualizar o sw para um nova versão, lembrando, que "atualizar um sw" é basicamente mudar qualquer byte dentro do arquivo registrado como sw,
Você pode perceber o uso do
self.skipWaiting()
na fase de instalação, isso irá silenciosamente dar controle da página para o novo sw, resultando na "atualização silenciosa" mencionada aqui.Olhando o artigo novamente, você pode ver que enviamos um evento para o sw do frontend em:
Desse modo, o cliente controla se ele deve atualizar no momento que a detecção de uma nova versão do sw PELO navegador acontece.
A atualização irá acontecer de qualquer modo, se houver um novo sw, o navegador irá atualizar o cliente com base nas regras descritas no começo desse comentário.
Se tiver mais dúvidas só mandar!