Artigo original: https://www.freecodecamp.org/news/build-a-pwa-from-scratch-with-html-css-and-javascript/

Aplicativos Web Progressivos (do inglês, PWA, ou Progressive Web App) são uma forma de trazer a usabilidade de um aplicativo móvel para um aplicativo web tradicional. Com eles, podemos melhorar nosso website com funcionalidades de aplicativos móveis que aumentarão a usabilidade e oferecerão uma ótima experiência de usuário.

Nesse artigo, criaremos um PWA do zero, com HTML, CSS e JavaScript. Aqui estão os tópicos que nós cobriremos:

Então, vamos começar com uma pergunta importante: o que é, afinal, um Aplicativo Web Progressivo (PWA)?

O que é um Aplicativo Web Progressivo?

Um Aplicativo Web Progressivo é um aplicativo web que entrega uma experiência semelhante a de um aplicativo móvel para os usuários usando as capacidades da web moderna. No final, é só o seu site da web normal, rodando em um navegador, com alguns melhoramentos. Aplicativos Web Progressivos têm a capacidade de:

  • Instalar em uma página home do seu celular
  • Acessar o aplicativo quando você estiver desconectado da internet
  • Acessar a sua câmera
  • Receber notificações push
  • Manter uma sincronização em segundo plano

E muito mais.

Contudo, para podermos transformar nosso aplicativo web normal em um PWA, teremos que fazer alguns pequenos ajustes, adicionando um arquivo de manifesto de aplicativo web e um service worker.

Não se preocupe com esses termos novos. Falaremos sobre eles adiante.

Primeiro, temos que construir nosso aplicativo web tradicional. Então, vamos começar com o HTML.

HTML

O arquivo HTML é relativamente simples. Envolveremos tudo dentro de uma tag main.

  • N0 arquivo index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <link rel="stylesheet" href="css/style.css" />
    <title>Dev'Coffee PWA</title>
  </head>
  <body>
    <main>
      <nav>
        <h1>Dev'Coffee</h1>
        <ul>
          <li>Home</li>
          <li>About</li>
          <li>Blog</li>
        </ul>
      </nav>
      <div class="container"></div>
    </main>
    <script src="js/app.js"></script>
  </body>
</html>

Vamos criar um barra de navegação com a tag nav. Então, uma div com a classe .container, onde ficarão todos os cartões que vamos adicionar depois com JavaScript.

Agora que terminamos essa parte, vamos estilizar nosso aplicativo com CSS.

Estilização

Aqui, como de costume, começamos importando as fontes de que vamos precisar. Então, vamos redefinir alguns comportamentos (e valores) que são padrão em nosso navegador.

  • No arquivo css/style.css
@import url("https://fonts.googleapis.com/css?family=Nunito:400,700&display=swap");
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
body {
  background: #fdfdfd;
  font-family: "Nunito", sans-serif;
  font-size: 1rem;
}
main {
  max-width: 900px;
  margin: auto;
  padding: 0.5rem;
  text-align: center;
}
nav {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
ul {
  list-style: none;
  display: flex;
}

li {
  margin-right: 1rem;
}
h1 {
  color: #e74c3c;
  margin-bottom: 0.5rem;
}

Então, vamos limitar o tamanho do elemento main para largura máxima de 900px (900 pixels) para fazer com que ele apareça bem em telas mais largas.

Para a barra de navegação, queremos que o logotipo fique à esquerda e os links à direita. Então, para a tag nav, depois de torná-la um "contêiner flex", usamos justify-content: space-between; para alinhá-las.

  • No arquivo css/style.css
.container {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  grid-gap: 1rem;
  justify-content: center;
  align-items: center;
  margin: auto;
  padding: 1rem 0;
}
.card {
  display: flex;
  align-items: center;
  flex-direction: column;
  width: 15rem auto;
  height: 15rem;
  background: #fff;
  box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23);
  border-radius: 10px;
  margin: auto;
  overflow: hidden;
}
.card--avatar {
  width: 100%;
  height: 10rem;
  object-fit: cover;
}
.card--title {
  color: #222;
  font-weight: 700;
  text-transform: capitalize;
  font-size: 1.1rem;
  margin-top: 0.5rem;
}
.card--link {
  text-decoration: none;
  background: #db4938;
  color: #fff;
  padding: 0.3rem 1rem;
  border-radius: 20px;
}

Teremos alguns cartões. Por isso, o elemento com a classe .container será mostrado como grid. E, com a propriedade grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)), podemos, agora, fazer nossos cartões ficarem responsivos, então eles podem usar pelo menos 15rem de largura se houver espaço suficiente para isso (ou 1fr se não houver espaço para 15rem).

E para fazer nossos cartões ficarem ainda mais bonitos, nós duplicamos o efeito de sombra no elemento com a classe .card e usamos object-fit: cover no .card--avatar para evitar que a imagem estique demais.

Agora, está muito melhor. Porém, ainda não temos dados para mostrar.

Vamos consertar isso na próxima seção.

Exibição dos dados com JavaScript

Note que usei imagens grandes, que demoram algum tempo para carregar. Isso mostrará a você, da melhor forma, o poder dos service workers.

Como disse antes, a classe .container conterá nossos cartões. Portanto, precisamos selecioná-la com nosso código JavaScript.

  • No arquivo js/app.js
const container = document.querySelector(".container")
const coffees = [
  { name: "Perspiciatis", image: "images/coffee1.jpg" },
  { name: "Voluptatem", image: "images/coffee2.jpg" },
  { name: "Explicabo", image: "images/coffee3.jpg" },
  { name: "Rchitecto", image: "images/coffee4.jpg" },
  { name: " Beatae", image: "images/coffee5.jpg" },
  { name: " Vitae", image: "images/coffee6.jpg" },
  { name: "Inventore", image: "images/coffee7.jpg" },
  { name: "Veritatis", image: "images/coffee8.jpg" },
  { name: "Accusantium", image: "images/coffee9.jpg" },
]

Então, criamos um array de cartões com os nomes e imagens.

  • No arquivo js/app.js
const showCoffees = () => {
  let output = ""
  coffees.forEach(
    ({ name, image }) =>
      (output += `
              <div class="card">
                <img class="card--avatar" src=${image} />
                <h1 class="card--title">${name}</h1>
                <a class="card--link" href="#">Taste</a>
              </div>
              `)
  )
  container.innerHTML = output
}

document.addEventListener("DOMContentLoaded", showCoffees)

Com o código acima, podemos fazer um loop no array e renderizar os elementos no nosso arquivo HTML. E, para fazer tudo funcionar, esperamos até que o conteúdo do DOM (Modelo de Objeto de Documentos) termine de carregar para que possamos executar o método showCoffees.

Já fizemos muito, mas, por agora, temos somente um aplicativo web tradicional. Então, vamos mudar isso na próxima seção, introduzindo algumas funcionalidades de Aplicativo Web Progressivo.

source

Manifesto do Aplicativo Web

O manifesto do aplicativo web é um simples arquivo em formato JSON, que informa ao navegador sobre o seu aplicativo web. Esse manifesto informa como o aplicativo deve se comportar quando for instalado no dispositivo móvel do usuário ou no seu desktop. E, para mostrar a opção de adicionar à tela inicial, é obrigatório ter o manifesto.

Agora que já sabemos o que é um manifesto, vamos criar um novo arquivo chamado manifest.json (o nome precisa ser esse) na pasta raiz. Então, vamos adicionar o bloco de código abaixo.

  • No arquivo manifest.json
{
  "name": "Dev'Coffee",
  "short_name": "DevCoffee",
  "start_url": "index.html",
  "display": "standalone",
  "background_color": "#fdfdfd",
  "theme_color": "#db4938",
  "orientation": "portrait-primary",
  "icons": [
    {
      "src": "/images/icons/icon-72x72.png",
      "type": "image/png", "sizes": "72x72"
    },
    {
      "src": "/images/icons/icon-96x96.png",
      "type": "image/png", "sizes": "96x96"
    },
    {
      "src": "/images/icons/icon-128x128.png",
      "type": "image/png","sizes": "128x128"
    },
    {
      "src": "/images/icons/icon-144x144.png",
      "type": "image/png", "sizes": "144x144"
    },
    {
      "src": "/images/icons/icon-152x152.png",
      "type": "image/png", "sizes": "152x152"
    },
    {
      "src": "/images/icons/icon-192x192.png",
      "type": "image/png", "sizes": "192x192"
    },
    {
      "src": "/images/icons/icon-384x384.png",
      "type": "image/png", "sizes": "384x384"
    },
    {
      "src": "/images/icons/icon-512x512.png",
      "type": "image/png", "sizes": "512x512"
    }
  ]
}

No final, é só um arquivo JSON com algumas propriedades obrigatórias e outras opcionais.

name: Quando o navegador iniciar a tela de boas-vindas, esse será o nome mostrado na tela de boas-vindas

short_name: Será o nome que aparecerá abaixo do atalho para seu aplicativo na tela inicial

start_url: Será a página mostrada ao usuário quando o seu aplicativo for aberto.

display: Informará ao seu navegador como mostrar o aplicativo. Existem diversas formas de mostrar, como minimal-ui, fullscreen, browser, etc. Aqui, nós usaremos o modo standalone para esconder qualquer coisa relacionada com o navegador.

background_color: Quando o navegador iniciar a tela de boas-vindas, essa será a cor de fundo da tela.

theme_color: Essa será a cor da barra de status quando o app for aberto.

orientation: Informa ao navegador em qual orientação (horizontal ou vertical) mostrar o app.

icons: Quando o navegador carregar a página de boas-vindas, esse será o ícone mostrado na tela. Aqui, eu usei todos os tamanhos para que o app se ajuste a qualquer dispositivo, mas você pode usar um ou dois. Depende da sua escolha.

Agora que nós temos um manifesto de aplicativo web, vamos adicioná-lo ao nosso arquivo HTML.

  • No arquivo index.html (na tag head)
<link rel="manifest" href="manifest.json" />
<!-- ios support -->
<link rel="apple-touch-icon" href="images/icons/icon-72x72.png" />
<link rel="apple-touch-icon" href="images/icons/icon-96x96.png" />
<link rel="apple-touch-icon" href="images/icons/icon-128x128.png" />
<link rel="apple-touch-icon" href="images/icons/icon-144x144.png" />
<link rel="apple-touch-icon" href="images/icons/icon-152x152.png" />
<link rel="apple-touch-icon" href="images/icons/icon-192x192.png" />
<link rel="apple-touch-icon" href="images/icons/icon-384x384.png" />
<link rel="apple-touch-icon" href="images/icons/icon-512x512.png" />
<meta name="apple-mobile-web-app-status-bar" content="#db4938" />
<meta name="theme-color" content="#db4938" />

Como você pode ver, nosso arquivo manifest.json foi importado na tag head. Também importamos alguns outros links para podermos oferecer suporte para sistemas iOS na hora de mostrar ícones e colorir nossa barra de status com nosso tema de cores.

Com isso, agora podemos mergulhar mais fundo na parte final e introduzir o conceito de service worker.

O que é um Service Worker?

Note que Aplicativos Web Progressivos (PWAs) funcionarão apenas em https, porque o service worker pode acessar as requisições e lidar com elas.

Um service worker é um script que seu navegador executa em segundo plano em uma thread separada. Isso significa que ele é executado em um lugar completamente diferente da sua página web. Essa é a razão pela qual ele não pode manipular os elementos do DOM.

Contudo, ele é muito poderoso. O service worker pode interceptar e lidar com requisições de rede, gerenciar o cache para possibilitar o uso off-line ou enviar notificações push para seus usuários.

source--1-

Então, vamos criar nosso primeiro service worker na pasta raiz e dar a ele o nome de serviceWorker.js (você pode usar o nome que quiser). Porém, você deve colocá-lo na pasta raiz para não limitar o escopo a apenas uma pasta.

Armazenamento (cache) dos arquivos

  • No arquivo serviceWorker.js
const staticDevCoffee = "dev-coffee-site-v1"
const assets = [
  "/",
  "/index.html",
  "/css/style.css",
  "/js/app.js",
  "/images/coffee1.jpg",
  "/images/coffee2.jpg",
  "/images/coffee3.jpg",
  "/images/coffee4.jpg",
  "/images/coffee5.jpg",
  "/images/coffee6.jpg",
  "/images/coffee7.jpg",
  "/images/coffee8.jpg",
  "/images/coffee9.jpg",
]

self.addEventListener("install", installEvent => {
  installEvent.waitUntil(
    caches.open(staticDevCoffee).then(cache => {
      cache.addAll(assets)
    })
  )
})

Esse código parece intimidador no começo, mas é só JavaScript (então, não se preocupe).

Declaramos o nome do nosso cache staticDevCoffee e os arquivos para salvar no cache. Após fazermos isso, precisamos adicionar um listener ao self.

self é o service worker. Ele nos permite "ouvir" o ciclo de vida dos eventos e fazer alguma coisa quando esses eventos de fato acontecem.

O service worker tem alguns ciclos de vida e um deles é o evento install. Ele é executado quando um service worker é instalado. Ele executa tão logo um worker execute e somente é executado uma vez por service worker.

Quando o evento install é acionado, executamos uma função de retorno (callback), que nos dará acesso ao objeto event.

Armazenar alguma coisa no navegador pode levar algum tempo para terminar, porque é assíncrono.

Então, para lidar com isso, precisamos usar waitUntil() que, como você pode imaginar, vai esperar o carregamento para, então, finalizar.

Uma vez que a API do cache está pronta, podemos executar o método open() e criar nosso cache passando o nome do nosso cache como um argumento, assim: caches.open(staticDevCoffee)

Então, essa função nos retornará uma Promise, que nos ajuda a armazenar nossos arquivos no cache com cache.addAll(assets).

cached-images

Espero que você ainda esteja comigo.

source--2-

Agora, armazenamos nossos arquivos no navegador com sucesso. E, na próxima vez em que carregarmos a página, o service worker lidará com as requisições e acessará o cache se estivermos off-line.

Então, vamos acessar nosso cache.

Acesso aos arquivos

  • No arquivo serviceWorker.js
self.addEventListener("fetch", fetchEvent => {
  fetchEvent.respondWith(
    caches.match(fetchEvent.request).then(res => {
      return res || fetch(fetchEvent.request)
    })
  )
})

Aqui, usamos o evento fetch para obter nossos dados. A função de retorno (callback) nos dá acesso ao fetchEvent. Então, acoplamos respondWith() para modificar a resposta padrão do navegador. Em vez disso (da resposta padrão), a função retorna uma promise, já que a ação de acessar os arquivos pode demorar algum tempo para terminar.

Quando o cache estiver pronto, podemos aplicar caches.match(fetchEvent.request). Isso verificará se alguma coisa no cache corresponde a fetchEvent.request. Para que você saiba, fetchEvent.request é apenas um array de arquivos.

Então, a função retorna uma promise. Finalmente, podemos retornar os resultados, se eles existirem, ou fazer o fetch inicial caso não existam resultados a exibir.

Agora, nossos arquivos podem ser armazenados e acessados pelo service worker, o que melhora o tempo de carregamento das nossas imagens um pouco.

E o mais importante: isso faz com que o nosso app fique disponível off-line.

O service worker, porém, não consegue fazer todo o trabalho sozinho. Precisamos registrá-lo em nosso projeto.

source--3-

Registro do Service Worker

  • No arquivo js/app.js
if ("serviceWorker" in navigator) {
  window.addEventListener("load", function() {
    navigator.serviceWorker
      .register("/serviceWorker.js")
      .then(res => console.log("service worker registered"))
      .catch(err => console.log("service worker not registered", err))
  })
}

Aqui, verificamos se o service worker é suportado pelo nosso navegador (ele não é suportado por todos os navegadores).

Então, "ouvimos" o evento de carregamento da página para registrar nosso service worker, passando para ele o nome do arquivo serviceWorker.js para navigator.serviceWorker.register() como parâmetro para registrá-lo.

Com esse ajuste, agora transformamos nosso aplicativo web normal em um PWA.

source--4-

Conclusões

Através deste artigo, vimos que os PWAs podem ser maravilhosos. Adicionando um manifesto de aplicativo web e um service worker, realmente melhoramos a experiência de usuário do nosso aplicativo web tradicional. Isso ocorre porque os PWAs são rápidos, seguros, confiáveis e, o mais importante de tudo, eles permitem o uso off-line.

Muitos frameworks atuais já vêm com um service worker configurado para nós. Saber como implementá-los com JavaScript puro, no entanto, pode ajudá-lo a entender os PWAs.

Você pode ir mais além com os service workers, como, por exemplo: armazenar arquivos dinamicamente, limitar o tamanho do seu armazenamento e assim por diante.

Obrigado por ler esse artigo.

Você pode ver como o aplicativo ficou aqui e o código fonte aqui.

Leia mais artigos do autor no blog do autor.

Próximos passos (textos em inglês)

Web Manifest Documentation (documentação do manifesto da web)

Service Worker Documentation (documentação do service worker)

Web Manifest Generator (gerador do manifesto da web)

Browser Support (suporte aos navegadores)