PWA: Criar notificação de "uma nova versão está disponível"
Tags: pwa, javascript, webdev, ptbr
Published: 2019-04-01 11:49:35

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:

![](https://thepracticaldev.s3.amazonaws.com/i/9scdvn1xprs5k9ftjcns.png)

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`

```bash
$ mkdir exemplo-service-worker
$ cd $_
$ touch index.html
$ touch service-worker.js
```

[Download **dog.jpg**](https://images.dog.ceo/breeds/pembroke/n02113023_14262.jpg)

Para começar, o HTML da minha página web parece um pouco com o código a seguir:

```html
<!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:

```js
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](https://developers.google.com/web/ilt/pwa/caching-files-with-service-worker). 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:

<img src="https://thepracticaldev.s3.amazonaws.com/i/arpd4c0zqtoi1hcje037.png" width="320" />

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:

![](https://thepracticaldev.s3.amazonaws.com/i/fj2q5df03iye3jk2i4zw.jpg)

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`:

```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:

<img src="https://thepracticaldev.s3.amazonaws.com/i/ipo2yx4flg9rny5zosg8.png" width="320" />

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:

```js
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:

```html
...
<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`:

```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:

![](https://thepracticaldev.s3.amazonaws.com/i/5drb6ta7sdz2iffgx7as.png)

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**:

![](https://thepracticaldev.s3.amazonaws.com/i/w3vuulv99a2kppdbldwd.png)

# 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](https://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:

- [Ciclo de vida do Service Worker](https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle#skip_waiting)
- [Notificação de atualização do PWA](https://medium.com/progressive-web-apps/pwa-create-a-new-update-available-notification-using-service-workers-18be9168d717)
- [StackOverflow](https://stackoverflow.com/questions/40100922/activate-updated-service-worker-on-refresh)

# Créditos ⭐️
- [How to display a "new version available" for a Progressive Web App](https://deanhume.com/displaying-a-new-version-available-progressive-web-app/), escrito originalmente por [Dean Hume](https://deanhume.com/about/)