Como criar um PWA no Google, parte 1

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

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

Esta é a primeira de uma série de postagens no blog sobre as lições que a equipe do Google Bulletin aprendeu ao criar um PWA voltado para fora. 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 é uma visão geral completa dos PWAs. O objetivo é compartilhar o que aprendemos com a experiência da nossa equipe.

Nesta primeira postagem, vamos abordar algumas informações básicas e, em seguida, mergulhar em tudo o que aprendemos sobre service workers.

Contexto

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

Por que escolhemos criar uma PWA

Antes de mergulharmos no processo de desenvolvimento, vamos examinar por que criar uma PWA foi uma opção atraente para este projeto:

  • Capacidade de iterar rapidamente. Especialmente valioso, já que 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. Um PWA significava que poderí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 em uso. Conseguimos fazer mudanças importantes no 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 simplesmente abrir um URL.
  • Removeu a dificuldade de instalar um app.

Nossa estrutura

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

O que aprendemos sobre os service workers

Não é possível ter uma PWA sem um service worker. Os service workers dão a você muito poder, como estratégias avançadas de armazenamento em cache, recursos off-line, sincronização em segundo plano etc. Embora eles aumentem um pouco a complexidade, descobrimos que os benefícios superam essa complicação.

Gere, se possível

Evite escrever um script de worker de serviço manualmente. Escrever service workers manualmente requer o gerenciamento manual de recursos em cache e a reescrita de lógica que é comum à maioria das bibliotecas de service workers, como a Workbox.

No entanto, devido à nossa pilha de tecnologia interna, não foi possível usar uma biblioteca para gerar e gerenciar o service worker. Os aprendizados abaixo podem, às vezes, refletir isso. Acesse Problemas em 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 worker de serviço. Por exemplo, supondo que window ou document estejam disponíveis ou usando uma API que não está disponível para workers de serviço (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 essa PWA específica, queríamos usar gapi.js para autenticação, mas não foi possível porque ela não oferecia suporte a workers de serviço. 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.

Evite acessar o IndexedDB durante a inicialização

Não leia o 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 visita o PWA, o que aciona o download do novo service worker
  4. O novo service worker lê o 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, porque 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 fosse instalado, os usuários nunca receberiam o app atualizado.

Torne-o resiliente

Embora os scripts de worker de serviço sejam executados em segundo plano, eles também podem ser encerrados a qualquer momento, mesmo quando estão no meio de operações de E/S (rede, IDB etc.). Qualquer processo de longa duração precisa ser resumível 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. Além disso, antes de qualquer operação de E/S de longa duração, o estado era salvo no IDB para indicar em que parte 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ê espera encontrar não estão presentes. Grande parte do nosso código foi executado em um contexto window e em um contexto de worker de serviço (como registro, sinalizações, sincronização etc.). O código precisa ser defensivo em relação aos serviços que ele usa, como armazenamento local ou cookies. É possível 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 dos principais componentes dos service workers é o armazenamento em cache de recursos localmente. No entanto, durante o desenvolvimento, isso é exatamente o contrário do que você quer, principalmente quando as atualizações são feitas de forma lenta. 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, é possível fazer isso usando o Chrome DevTools, ativando a caixa de seleção Bypass for network (painel Application > Service workers) e 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 flag para desativar o armazenamento em cache no nosso service worker, que é ativado por padrão nos builds de desenvolvedor. Isso garante que os desenvolvedores sempre recebam as mudanças mais recentes sem problemas de armazenamento em cache. É importante incluir o cabeçalho Cache-Control: no-cache para impedir que o navegador armazene em cache os recursos.

Farol

O Lighthouse oferece várias ferramentas de depuração úteis para PWA. 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. Isso aconteceu com a gente uma vez, em que o worker de serviço não estava sendo instalado e não percebemos isso antes de um push de produção. Ter o Lighthouse como parte da nossa CI teria evitado isso.

Adote a entrega contínua

Como os workers de serviço podem ser atualizados automaticamente, os usuários não podem limitar os upgrades. Isso reduz significativamente a quantidade de clientes desatualizados em uso. Quando o usuário abria nosso app, o service worker atendia o cliente antigo enquanto fazia o download preguiçoso do novo. Depois que o novo cliente fosse feito o download, o usuário seria solicitado a atualizar a página para acessar os novos recursos. Mesmo que o usuário ignore essa solicitação, na próxima vez que ele atualizar a página, vai receber a nova versão do cliente. Como resultado, é muito difícil para um usuário recusar atualizações da mesma forma que para apps iOS/Android.

Conseguimos fazer mudanças importantes no back-end com um tempo de migração muito curto para os clientes. Normalmente, damos um mês para que os usuários atualizem para clientes mais recentes antes de fazer mudanças importantes. Como o app era exibido enquanto estava desatualizado, era possível que clientes mais antigos existissem no mundo real se o usuário não tivesse aberto o app por um longo período. No iOS, os service workers são expulsos após algumas semanas, portanto, esse caso não acontece. No Android, esse problema pode ser mitigado ao não veicular o conteúdo enquanto ele estiver desatualizado ou ao expirar o conteúdo manualmente após algumas semanas. Na prática, nunca encontramos problemas de clientes desatualizados. O nível de rigidez que uma equipe quer ter aqui depende do caso de uso específico, mas os PWAs oferecem muito mais flexibilidade do que os apps iOS/Android.

Como receber valores de cookies em um worker de serviço

Às vezes, é necessário acessar os valores do cookie em um contexto de worker de serviço. No nosso caso, precisamos acessar os valores do cookie para gerar um token e autenticar as solicitações de API próprias. Em um service worker, APIs síncronas, como document.cookies, não estão disponíveis. Você sempre pode enviar uma mensagem para clientes ativos (em janela) do service worker para solicitar os valores do cookie, embora seja possível que o service worker seja executado em segundo plano sem nenhum cliente em janela disponível, como durante uma sincronização em segundo plano. Para contornar esse problema, criamos um endpoint no nosso servidor de front-end que simplesmente repetia o valor do cookie para o cliente. O worker de serviço fez uma solicitação de rede para esse endpoint e leu a resposta para receber os valores do cookie.

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

Armadilhas para service workers não gerados

Garanta que o script do service worker seja alterado se algum arquivo estático em cache mudar

Um padrão de PWA comum é um service worker instalar todos os arquivos de aplicativos estáticos 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 mudou de alguma forma. Por isso, precisamos garantir que o arquivo do script do service worker mudasse de alguma forma quando um arquivo em cache mudasse. Fizemos isso manualmente, incorporando um hash do arquivo de recursos estáticos no script do service worker. Assim, cada versão produzia um arquivo JavaScript de service worker distinto. Bibliotecas de service worker, como 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')));

Isso pode ser difícil de testar porque você precisa simular o acionador do evento, o objeto do evento, aguardar o callback respondWith() e aguardar a promessa antes de finalmente declarar o 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.

Não perca as partes 2 e 3

Nas partes 2 e 3 desta série, vamos falar sobre gerenciamento de mídia e problemas específicos do iOS. Se você quer saber mais sobre como criar uma PWA no Google, acesse nossos perfis de autor para saber como entrar em contato: