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?

ūü§ė