Como criar um PWA no Google, parte 1

O que a equipe do Bulletin aprendeu sobre os service workers ao desenvolver um PWA.

Douglas Parker
Douglas Parker
Joel Riley
Joel Riley
Dikla Cohen
Dikla Cohen

Esta é a primeira de uma série de postagens do blog sobre as lições que a equipe do Google Bulletin aprendeu durante a criação de um PWA externo. Nestas postagens, vamos compartilhar alguns dos desafios que enfrentamos, as abordagens que seguimos para superá-los e conselhos gerais para evitar armadilhas. Esta não é, de forma alguma, uma visão geral completa dos PWAs. O objetivo é compartilhar o que aprendemos com a experiência da nossa equipe.

Nesta primeira postagem, abordaremos algumas informações básicas e, em seguida, nos aprofundaremos em tudo o que aprendemos sobre service workers.

Contexto

O Mural estava em desenvolvimento ativo de meados de 2017 a meados de 2019.

Por que escolhemos criar um PWA

Antes de nos aprofundarmos no processo de desenvolvimento, vamos examinar por que a criação de um PWA era uma opção atraente para este projeto:

  • Capacidade de iterar rapidamente. Especialmente valioso, porque o Bulletin seria testado em vários mercados.
  • Única base de código. Os usuários estavam divididos de maneira quase uniforme entre Android e iOS. Com um PWA, podíamos criar um único app da Web que funcionasse nas duas plataformas. Isso aumentou a velocidade e o impacto da equipe.
  • Atualizado de maneira rápida e independente do comportamento do usuário. Os PWAs podem ser atualizados automaticamente, o que reduz a quantidade de clientes desatualizados. Conseguimos realizar alterações interruptivas de back-end com um tempo de migração muito curto para os clientes.
  • Facilmente integrada a apps próprios e de terceiros. Essas integrações eram um requisito para o app. Com um PWA, muitas vezes significava apenas abrir um URL.
  • Removemos o atrito da instalação de um app.

Nossa estrutura

Para o Mural, usamos o Polymer, mas qualquer framework moderno e com suporte funcionará.

O que aprendemos sobre service workers

Não é possível ter um PWA sem um service worker. Os service workers oferecem muito poder, como estratégias avançadas de armazenamento em cache, recursos off-line, sincronização em segundo plano etc. Embora os service workers adicionem um pouco de complexidade, descobrimos que seus benefícios superaram a complexidade adicional.

Gere-a se puder

Evite escrever um script de service worker manualmente. A gravação manual de service workers requer o gerenciamento manual de recursos armazenados em cache e a regravação da lógica comum à maioria das bibliotecas de service workers, como o Workbox.

Dito isso, devido ao nosso conjunto interno de tecnologias, não foi possível usar uma biblioteca para gerar e gerenciar o service worker. Os aprendizados abaixo podem, às vezes, refletir isso. Acesse Armadilhas dos service workers não gerados para saber mais.

Nem todas as bibliotecas são compatíveis com service workers

Algumas bibliotecas JS fazem suposições que não funcionam como esperado quando executadas por um service worker. Por exemplo, supondo que window ou document estejam disponíveis ou usando uma API não disponível para service workers (XMLHttpRequest, armazenamento local etc.). Certifique-se de que as bibliotecas críticas necessárias para seu aplicativo sejam compatíveis com o service worker. Para esse PWA específico, queríamos usar o gapi.js para autenticação, mas não foi possível porque ele não era compatível com service workers. Os autores de bibliotecas também precisam reduzir ou remover suposições desnecessárias sobre o contexto do JavaScript sempre que possível para oferecer suporte a casos de uso de service workers, como evitar APIs incompatíveis com service workers e evitar o estado global.

Evitar acessar IndexedDB durante a inicialização

Não leia IndexedDB ao inicializar o script do service worker. Caso contrário, você poderá entrar nesta situação indesejada:

  1. O usuário tem um app da Web com a versão N do IndexedDB (IDB)
  2. Novo aplicativo da web é enviado com a versão N+1 do IDB
  3. O usuário acessa o PWA, que aciona o download de um novo service worker
  4. O novo service worker faz leituras do IDB antes de registrar o manipulador de eventos install, acionando um ciclo de upgrade do IDB para ir de N para N+1.
  5. Como o usuário tem um cliente antigo com a versão N, o processo de upgrade do service worker trava, já que as conexões ativas ainda estão abertas para a versão antiga do banco de dados.
  6. O service worker trava e nunca instala

No nosso caso, o cache foi invalidado na instalação do service worker. Portanto, se o service worker nunca for instalado, os usuários nunca receberam o app atualizado.

Torne a página resiliente

Embora os scripts do service worker sejam executados em segundo plano, eles também podem ser encerrados a qualquer momento, mesmo no meio de operações de E/S (rede, IDB etc.). Qualquer processo de longa duração precisa ser retomado a qualquer momento.

No caso de um processo de sincronização que fazia o upload de arquivos grandes para o servidor e salvava no IDB, nossa solução para uploads parciais interrompidos era aproveitar o sistema retomável da nossa biblioteca de upload interna, salvar o URL de upload retomável no IDB antes do upload e usar esse URL para retomar um upload se ele não fosse concluído na primeira vez. Também antes de qualquer operação de E/S de longa duração, o estado era salvo no IDB para indicar em que ponto do processo estávamos para cada registro.

Não dependam do estado global

Como os service workers existem em um contexto diferente, muitos símbolos que você pode esperar que existam não estão presentes. Grande parte do nosso código foi executada em um contexto de window e em um contexto de service worker (como geração de registros, sinalizações, sincronização etc.). O código precisa ficar na defensiva dos serviços que usa, como armazenamento local ou cookies. Você pode usar globalThis para se referir ao objeto global de uma maneira que funcione em todos os contextos. Além disso, use os dados armazenados em variáveis globais com moderação, já que não há garantia de quando o script será encerrado e o estado será removido.

Desenvolvimento local

Um componente importante dos service workers é armazenar recursos em cache localmente. No entanto, durante o desenvolvimento, esse é exatamente o oposto do que você quer, principalmente quando as atualizações são feitas lentamente. Você ainda quer que o worker do servidor esteja instalado para depurar problemas com ele ou trabalhar com outras APIs, como sincronização em segundo plano ou notificações. No Chrome, você pode fazer isso pelo Chrome DevTools ativando a caixa de seleção Bypass for network (painel Application > Service workers), além de marcar a caixa de seleção Disable cache no painel Network para também desativar o cache de memória. Para abranger mais navegadores, optamos por uma solução diferente, incluindo uma sinalização para desativar o armazenamento em cache no service worker, que é ativado por padrão nas versões do desenvolvedor. Isso garante que os desenvolvedores sempre recebam as alterações mais recentes sem problemas de armazenamento em cache. É importante incluir também o cabeçalho Cache-Control: no-cache para impedir que o navegador armazene recursos em cache.

Farol

O Lighthouse oferece várias ferramentas de depuração úteis para PWAs. Ele verifica um site e gera relatórios sobre PWAs, desempenho, acessibilidade, SEO e outras práticas recomendadas. Recomendamos executar o Lighthouse na integração contínua para alertar se você violar um dos critérios para ser um PWA. Na verdade, isso aconteceu uma vez, em que o service worker não estava sendo instalado e não percebemos antes do envio de produção. Ter o Lighthouse como parte da nossa CI teria evitado isso.

Adote a entrega contínua

Como os service workers podem ser atualizados automaticamente, os usuários não conseguem limitar upgrades. Isso reduz significativamente a quantidade de clientes desatualizados. Quando o usuário abria o app, o service worker atendia ao cliente antigo enquanto fazia o download do novo cliente lentamente. Após o download do novo cliente, será solicitado que o usuário atualize a página para acessar os novos recursos. Mesmo que o usuário tenha ignorado essa solicitação, na próxima vez que atualizar a página, ele vai receber a nova versão do cliente. Como resultado, é muito difícil para um usuário recusar atualizações da mesma maneira que faz para apps iOS/Android.

Conseguimos realizar alterações interruptivas no back-end com um tempo de migração muito curto para os clientes. Normalmente, daremos um mês para os usuários atualizarem para clientes mais recentes antes de fazerem alterações interruptivas. Como o app seria veiculado enquanto estava desatualizado, na verdade, era possível que clientes mais antigos existissem se o usuário não tivesse aberto o app por muito tempo. No iOS, os service workers são excluídos após algumas semanas. Portanto, esse caso não acontece. No Android, esse problema pode ser atenuado por não veicular enquanto está desatualizado ou expirar manualmente o conteúdo após algumas semanas. Na prática, nunca encontramos problemas de clientes obsoletos. A rigidez de uma equipe depende do caso de uso específico, mas os PWAs oferecem muito mais flexibilidade do que os apps iOS/Android.

Como conseguir valores de cookies em um service worker

Às vezes, é necessário acessar valores de cookies em um contexto de service worker. Nesse caso, era necessário acessar os valores de cookies para gerar um token e autenticar as solicitações de API primárias. Em um service worker, APIs síncronas, como document.cookies, não estão disponíveis. Sempre é possível enviar uma mensagem aos clientes ativos (em janelas) do service worker para solicitar os valores de cookies. No entanto, o service worker pode ser executado em segundo plano sem nenhum cliente em janela disponível, como durante uma sincronização em segundo plano. Para contornar isso, criamos um endpoint no servidor de front-end que simplesmente transmitiu o valor do cookie de volta para o cliente. O service worker fez uma solicitação de rede para esse endpoint e leu a resposta para conseguir os valores de cookies.

Com o lançamento da API Cookie Store, essa solução alternativa não será mais necessária para navegadores compatíveis, já que ela fornece acesso assíncrono a cookies do navegador e pode ser usada diretamente pelo service worker.

Dificuldades para service workers não gerados

Verifique se o script do service worker muda caso algum arquivo estático armazenado em cache seja alterado

Um padrão de PWA comum é um service worker instalar todos os arquivos estáticos de aplicativos durante a fase install, o que permite que os clientes acessem o cache da API Cache Storage diretamente para todas as visitas subsequentes . Os service workers só são instalados quando o navegador detecta que o script do service worker foi alterado de alguma forma. Por isso, tínhamos que garantir que o próprio arquivo de script do service worker mudasse de alguma forma quando um arquivo armazenado em cache fosse alterado. Fizemos isso manualmente incorporando um hash do conjunto de arquivos de recursos estáticos no script do nosso service worker, para que cada versão produzisse um arquivo JavaScript distinto do service worker. Bibliotecas de service workers como o Workbox automatizam esse processo para você.

Teste de unidade

As APIs de service worker funcionam adicionando listeners de eventos ao objeto global. Exemplo:

self.addEventListener('fetch', (evt) => evt.respondWith(fetch('/foo')));

Esse teste pode ser complicado, porque você precisa simular o gatilho de evento, o objeto de evento, aguardar o callback respondWith() e, em seguida, aguardar a promessa antes de finalmente fazer a declaração no resultado. Uma maneira mais fácil de estruturar isso é delegar toda a implementação a outro arquivo, que é mais fácil de testar.

import fetchHandler from './fetch_handler.js';
self.addEventListener('fetch', (evt) => evt.respondWith(fetchHandler(evt)));

Devido às dificuldades do teste de unidade do script de um service worker, mantivemos o script do service worker principal o mais simples possível, dividindo a maior parte da implementação em outros módulos. Como esses arquivos eram apenas módulos JS padrão, eles poderiam ser testados de unidade mais facilmente com bibliotecas de teste padrão.

Fique de olho nas partes 2 e 3

Nas partes 2 e 3 desta série, falaremos sobre o gerenciamento de mídia e problemas específicos do iOS. Se você quiser mais informações sobre a criação de um PWA no Google, acesse nossos perfis de autor para descobrir como entrar em contato: