Progressive Web Apps são legais e tal, mas e agora? Vamos migrar um site existente, feito apenas para desktop, para suportar as incíveis funcionalidades dos PWAs

O que você vai aprender

O que você vai precisar

Quando possível, esse codelab irá simplificar algumas explicações e caminhos para facilitar o entendimento. Essas explicações podem ser vistas com mais profundidades em outros codelabs dedicados à mesma. Seguindo as etapas passo-a-passo no código do website fornecido nesse codelab, você vai aprender como aplicar nos seus próprios sites.

O início de tudo

Você irá modificar um site chamado Dragotchi, onde você brinca com uma espécie de bichinho virtual (em específico, um dragão amigável). Este site possui três páginas (Home, Dragotchi e FAQ). Duas páginas são estáticas, mas a página Dragotchi faz algumas requisições Ajax simples à um servidor.

Esse site foi pensado apenas para navegadores desktop. Ele não possui desing responsivo, e também não tem uma cara muito boa. Mas tudo bem! Nós estamos apenas àlguns passos para melhorar isso.

Vamos começar baixando o código-fonte do Dragotchi e configurar nosso ambiente de desenvolvimento.

O que você precisa

Clone o repositório do GitHub usando a seguinte linha de comando:

$ git clone https://github.com/googlecodelabs/migrate-to-progressive-web-apps.git

Uma vez que todo código estiver salvo localmente, você deve usar uma aplicação que crie um Servidor HTTP (ou equivalente de linha de commando, usando por exemplo Python) para levantar um servidor na pasta work, usando a porta 8887.

Vocâ agora pode abrir a url e brincar com o 🐲. Não se preocupe, o Codelab vai estar aqui quando você voltar.

Visitando seu site num dispositivo móvel

Se você tem um dispositivo Android conectnado, você pode (no seu Chrome desktop) visitar o endereço: chrome://inspect (você vai precisar digitar ou copiar e colar esse endereço). Então, configure um redirecionamento de porta (usando a mesma porta do seu servidor configurado anteriormente). Aí é só salvar.

Agora você está apto para acessar a versão básica do Dragotchi em http://localhost:8887/ no seu dispositivo Android. Como já tinhamos discutido anteriormente, a versão base não é otimizada para dispositivos móveis— nós iremos ver mais sobre isso no próximo passo.

A primeira coisa que iremos fazer nesse site "retrô" é fazer com que ele fique mais amigável para dispositivos móveis, além de incluir um arquivo que é o chamado "Web App Manifest". Esse arquivo descreve metadados sobre o site, como por exemplo, o nome e o ícone com que ele vai aparecer na home screen do usuário.

O Dragotchi possui três páginas HTML. Como ele não utiliza nenhum framework nem templating system, você vai precisar editar o conteúdo do <head> tanto no index.html, omo no faq.html e dragon.html

<head>
  <meta name="viewport" content="width=device-width, user-scalable=no" />
  <link rel="manifest" href="manifest.json" />
</head>

Se você estiver sem tempo, você pode adiciona-las apenas no index.html para este codelab. Porém tenha ciência que em sites "reais" você precisa adicionar esse conteúdo em todas as páginas.

O viewport

A primeira linha é a tag meta, que especifica o viewport. Você pode saber mais sobre viewport acessando esse artigo no MDN. Mas por agora, se você atualizar o seu site (seja no seu Android, seja no Developer Tools) você irá perceber que a página irá renderizar devidamente, sendo responsiva ao tamanho da tela do dispositivo.

O Web App Manifest

A segunda linha adiciona o Web App Manifest, que é necessário para controlar como seu site será adicionado à tela inicial do usuário. No código, você já fez a referência ao arquivo, agora vamos cria-lo!

Abra seu editor de texto favorito. Nós iremos escrever um pouco de JSON. Vamos fazer algo mais ou menos assim-

{
  "name": "O melhor site de Dragões da Internet",
  "short_name": "🐉🐉🐉",
  "display": "minimal-ui",
  "start_url": "/",
  "theme_color": "#673ab6",
  "background_color": "#111111",
  "icons": [
    {
      "src": "icon-192.png",
      "sizes": "192x192",
      "type": "image/png"
    }
  ]
}

O campo short_name especifica extamente o que aparece na tela inicial do usuário. Tente manter esse campo pequeno, preferencialmente uma palavra única (se a palavra tiver mais de 15 caracteres, ela pode ser abreviada). No nosso caso, nós utilizamos os emojis de dragões pois, igual a qualquer outro caracter, eles são totalmente válidos!

Algumas coisas que valem a pena ser relembradas-

Salve esse arquivo como manifest.json. Recarregue a página no seu Android, vá no menu do Chrome e selecione a opção "Adicionar à Tela Inicial" Agora você deverá ver um amigável dragão na sua tela inicial!

Se você não tiver um dispostivo Android por perto, selecione a aba "Aplicação" dentro do painel do Chrome Developer Tools. Então, elecione "Manifesto" para ver os detalhes do mesmo. Se você ver uma mensagem como "Nenhum manifesto detectado", verifique se o arquivo manifest.json que você criou foi salvo corretamente na pasta 'work'.

Eita! Funcionou tudo lindo!

Sim, top demais.Agora vamos adicionar um Service Worker!

Um Service Worker é um script que roda em segundo plano no navegador. Ele é tão poderoso que ele consegue ser executado até quando sua página não está em primeiro plano. Com ele, podemos prover aos usuários funcionalidades como suporte Offline e notificações Push.

Até um Service Worker vazio pode ajudar seu site. Como é que eu implemento essa belezura? Continue lendo.

Adicionando o Service Worker

Primeiramente, nós iremos escrever um pouco de JavaScript. Vamos copiar o mínimo de código em um novo arquivo-

/** Um service worker que não faz nada! */
self.addEventListener('fetch', function(event) {
  /** Um listener que não faz nada! */
});

Salve isso como sw.js! É isso, está feito! Você criou o seu primeiro service worker! 🎆

Registrando o seu Service Worker

Bem, tem mais uma etapa. O código acima e o arquivo (sw.js) não estão sendo referenciados em lugar nenhum (os navegadores atuais são ótimos, mas ainda não adivinham as coisas). Você vai precisar declara-lo no seu código para o navegador reconhecer o seu service worker.

O Dragotchi já possui no seu código um script que executa em todas as páginas. Abra o arquivo site.js e adicione o código a seguir-

navigator.serviceWorker && navigator.serviceWorker.register('./sw.js').then(function(registration) {
  console.log('SW funcionando, registrado com o escopo: ', registration.scope);
});

É isso! Esse código vai executar a cada carregamento de página. Se o service worker já estiver registrado seu navegador vai ignorar a requisição de registro e depois de algum tempo, irá verificar se alguma coisa mudou.

Recarregue sua página e verifique em chrome://serviceworker-internals/ se o Service Worker está de fato carregado para o site. Vai parecer algo mais ou menos assim-

Fantástico, o que eu posso fazer com isso?

Primeiramente (e provavelmente, o mais top) é que seu site agora irá, sozinho, perguntar aos usuários se eles desejam instalar seu PWA na tela inicial. Para saber mais como isso funciona, leia esse artigo do Google Developers (em inglês).

Por fim, você pode expandir as capacidades do seu service worker para que ele possibilite o acesso offline na sua aplicação, bem como permite com que seus usuários recebam notificações. Continue lendo para saber como isso funciona!

Vamos enviar notificações! Nós não iremos desenvolver um servidor para enviar as mensagens, mas iremos deixar tudo pronto na parte do front-end do site, assim iremos conseguir testar as notificações sem problemas.

Você vai precisar de duas chaves, uma chave pública e a outra privada. Acesse o site Push Companionque ele vai gerar as chaves pra você, e vamos usa-las em breve. Mas atenção: você precisa manter o site aberto para que as chaves permaneçam ativas.

Inscrevendo-se para receber pushs

Dentro do código do seu site, você irá criar uma seção para que o usuário se inscreva para receber notificações. É bem simples. Vamos adicionar o seguinte código no arquivo site.js-

navigator.serviceWorker && navigator.serviceWorker.ready.then(function(serviceWorkerRegistration) {  
  serviceWorkerRegistration.pushManager.getSubscription()  
    .then(function(subscription) {  
      // subscription será null ou um objeto PushSubscription
    });
});

Se subscription retornar null, então nós precisamos adicionar um bloco de código para que o usuário tenha a possibilidade de fazer a inscrição-

      if (subscription) {
        console.info('O usuário já está inscrito', subscription);
        window.subscription = subscription;
        return
      }

      const applicationServerKey = urlB64ToUint8Array(publicKey);
      serviceWorkerRegistration.pushManager.subscribe({
          userVisibleOnly: true,
          applicationServerKey,
      })
        .then(function(subscription) { 
          console.info('Usuário acabou de se inscrever!', subscription);
          window.subscription = subscription;
        });

Primeiramente, note que a publicKey não está definida em lugar nenhum. Crie ela no início do arquivo, usando a chave que você obteve no site Push Companion mais vedo-

const publicKey = '<sua public key retirada do Push Companion>';

Você agora está apto para salvar seu arquivo e recarregar sua página - tanto no desktop como no Android, não importa qual. Se você der uma olhada no seu Console dentro do Developer Tools, você irá ver uma mensagem com o objeto da inscrição. Nele está continudo um campo "endpoint". Nós iremos usar isso em breve, então deixe a janela aberta.

Se voc&e ver um erro, certifique-se que sua publicKey está salva devidamente, ela parece uma string codificada em base64.

Exibindo a notificação

É legal se inscrever para receber notificaçÕes, mas mais legal ainda é recebe-las. Mas, o que acontece? Como funciona? O usuário nem sempre vai estar com o Dragotchi aberto... e é ai que o Service Worker entra. Se você estiver bem lembrado, um service worker continua funcionando, mesmo que o site não esteja aberto.

Vamos abrir o arquivo sw.js ae adicionar essa seção-

self.addEventListener('push', function(event) {
  event.waitUntil(
    self.registration.showNotification('Got Push?', {
      body: 'Push Message received'
   }));
});

Essa é uma das mais simples notificações possíveis que podem ser exibidas, apenas com título e mensagem. Para mais opções, dê uma olhada na documentação. Por agora, recarregue a página para que isso seja carregado no seu site.

Misturando tudo

Voltando ao Chrome Developer Tools no seu desktop. Nós iremos chamar uma função que irar gerar um comando para você enviar uma notificação. Você irá precisar ter o "curl" instalado na sua máquina. Em sistemas Mac ou Linux, o "curl" já está instalado por padrão.

Copie o código a seguir e cole ele no console dentro do Chrome Developer Tools-

var privateKey = window.prompt('Insira sua privateKey');
privateKey && prepareAuthorization(publicKey, privateKey, subscription.endpoint, 'mailto:example@example.com').then(auth => {
  const curl = `curl "${subscription.endpoint}" ` +
      `-X POST ` +
      `-H "Authorization: ${auth}" ` + 
      `-H "Crypto-Key: p256ecdsa=${publicKey}" ` +
      `-H "TTL: 100" ` +
      `-H "Content-Length: 0"`;
  console.warn('Copie e cole o conteúdo a seguir na linha de comando-');
  console.dir(curl);
});

Ao executar o código acima, ele irá imediatamente perguntar sobre a sua privateKey que você obteve usando o Push Companion. Cole a chave no campo e perte OK. Depois disso, o console irá exibir uma linha de código enorme. Dê um duplo clique nela, copie-a, cole na linha de comando e execute.

E pronto!

Você deve ver algo mais ou menos assim no seu desktop. Bom trabalho!

Esse é o exemplo mais básico possível. O endpoint gerado pode (e deve) ser enviado para um servidor, num serviço que pode executar as notificações a qualquer momento, até quando seu site não estiver disponivel.

Vamos criar um novo Service Worker para permitir que o Dragotchi seja acessado offline. Primeiramente, vamos abrir o arquivo sw.js de novo. Nosso objetivo aqui é que controlar o cache do navegador, então no nosso service worker fazemos o seguinte-

self.addEventListener('install', function(e) {
  e.waitUntil(
    caches.open('the-magic-cache').then(function(cache) {
    });
  );
});

Agora, uma vez que nós estamos controlando isso, vamos atualizar o código para adicionar nosso site inteiro ao cache-

self.addEventListener('install', function(e) {
  e.waitUntil(
    caches.open('the-magic-cache').then(function(cache) {
      return cache.addAll([
        '/',
        '/index.html',
        '/dragon.html',
        '/faq.html',
        '/manifest.json',
        '/background.jpeg',
        '/construction.gif',
        '/dragon.png',
        '/logo.png',
        '/site.js',
        '/dragon.js',
        '/styles.css',
      ]);
    })
  );
});

Essa lista é bem grande, mas é importante que seja. Se você usar algum processo de empacotamento/gerador para o seu site, então é interessante que você adicione automaticamente todas as URLs possíveis que seu usuário possa acessar.

Fazendo um "match" nas requisições

Até agora tá tudo muito bom, tá tudo muito bem, mas realmente o que a gente quer é que quando o usuário acesse o nosso site offline, ele continue funcionando. Até agora nós apenas listamos quais arquivos devem estar no cache, mas só agora nós iremos escrever a parte do código em que nós entregamos esses arquivos para o usuário. Nós iremos subistituir o listener 'fetch' que está dentro do sw.js-

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request).then(function(response) {
      return response || fetch(event.request);
    })
  );
});

O objeto que representa o cache irá executar um "match" com sua requisição, retornando uma resposta válida ou null. Se o valor for nulo, então nós entregamos [class]=" requisição para o método fetch fazer a requisição normalmente.

Você pode escrever "matchs" mais complexos. Esses dois blocos de código representam um cache simples, você armazena os objetos pela URL e acessa eles pela URL. Entretanto, não tem nada que impeça você de executar um JavaScript mais robusto no seu service worker. Nós iremos ver um pouco sobre isso mais à frente.

Testando

Vá até o seu Android e desistale o Dragotchi da sua tela inicial. Então, abra ele no Chrome, recarregue a página e depois adicione-o à sua tela inicial mais uma vez.

Se você desconectar seu Android do USB e apertar no ícone do Dragotchi, repare que ele continua carregando! Você pode até recarregar a página, tudo continua a funcionar.

Alguns detalhes importantes

Esse tipo de cache cache vai armazenar seu site pra sempre!*

*ou até o armazenamento do usuário ficar cheio

Isso não é muito legal. Isso significa que a cada mudança que você fizer na sua página, você necessariamente precisa limpar seu Service Worker para ver a alteração.

De agora em diante, no nosso projeto, acesse http://localhost:8887/clear.html e aperte o botão "Clear". Essa página serve como um auxiliar para ajudar você a limpar o cache do service worker que é executado em todo evento de "install".

Isso pode ser bem frustrante, pois precisa ser feito a cada mudança na página. Esse é o motivo que estamos fazendo isso por último nesse codelab.

Se quiser dar uma olhada de uma maneira mais profunda, acesse o projeto Workbox, que é uma coleção de bibliotecas que auxiliam você a desenvolver um Service Worker mais robusto.

A regra básica é: se o seu Service Worker mudar, quando uma página do seu site recarregar, ele será reinstalado. Durante o desenvolvimento, você pode adicionar um simples comentário que contém a versão do seu Service Worker. Toda vez que ela mudar, o evento de instalação irá ocorrer de novo, e ele irá fazer o cache novamente de todos os arquivos que você possa ter mudado. Adicione esse comentário no topo do arquivo sw.js file-

// version: Estou quase acabando o codelab!

Você pode imaginar que ao invés de fazer isso manualmente, o versionamento deve ser feito usando algum processo automatizado na construção do seu site.

Tá quase

Se você carregou o site mesmo com o USB desconectado e tentou cuidar do seu dragão, isso só funcionou por quê seu dispositivo ainda está online. Nós cacheamos apenas as páginas que correspondem o nosso próprio site.

Se você tentar fazer isso com o modo avião, por exemplo, apesar das páginas locais funcionarem, você vai receber um alert indicando a falha na conexão com o servidor externo. Vamos resolver isso.

Como mencionamos anteriormente, tem muito mais coisas que você pode fazer usando o Cache disponível no Service Worker. Vamos gerenciar agora um URL externo a nosso projeto. O status do seu dragão vem de um site externo, então nós podemos interceptar a conexão.

Dentro do blogo fetch no sw.js, vamos buscar por requisições que venham especificamente de uma URL-

self.addEventListener('fetch', function(event) {
  if (event.request.url == 'https://dragon-server.appspot.com/') {
    return;
  }
  /** código antigo */
});

Isso mostra que um Service Worker pode interceptar requisições em domínios externos ao seu próprio site. Agora, vamos fazer alguma coisa com essa URL-

  if (event.request.url == 'https://dragon-server.appspot.com/') {
    console.info('respondendo a requisição ao dragon-server via Service Worker! 🤓');
    event.respondWith(fetch(event.request).catch(function(e) {
      let out = {Gold: 1, Size: -1, Actions: []};
      return new Response(JSON.stringify(out));
    }));
    return;
  }

Isso irá retornar uma respsota válida, porém limitada, quando existe uma chamada a esse servidor externo quando o site estiver offline (veja o bloco catch nessa Promise). Esse codelab tem a intenção de dar uma introdução rápida, existem muitas mais alternativas para esse tipo de fallback. Você pode dar uma olhada em como armazenar dados offline usando coisas como o IndexedDB.

A chave aqui é mostrar que é possivel você ter total controle de qualquer requisição que você fizer, em qualquer contexto.

Testando

Como comentamos anteriormente, a primeira coisa a se fazer é limpar o cache. Visite http://localhost:8887/clear.html para limpar. Então, remova o app da tela inicial e reinstale-o, usando o mesmo processo de sempre.

Ative o modo avião e então abra o site pelo ícone, e navegue para a página do Dragotchi. Repare que o estado padrão está com os valores que você definiu via o Service Worker!

Parabéns

Você aprendeu um monte sobre como deixar seu site um pouco mais progressivos, até aqueles sites que parecem estar presos nos anos 90!

Agora, que tal acessar outros codelabs?

🤘