Como criar um PWA no Google, parte 1

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

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

Esta é a primeira de uma série de postagens do blog sobre as lições que a equipe do Boletim do Google aprendeu ao criar um PWA externo. Nestas postagens, vamos compartilhar alguns dos desafios que enfrentamos, as abordagens que adotamos para superá-los e conselhos gerais para evitar armadilhas. Isso não significa uma visão geral completa dos PWAs. O objetivo é compartilhar os aprendizados da experiência da nossa equipe.

Nesta primeira postagem, abordaremos um pouco de informações básicas e, em seguida, vamos nos aprofundar 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 foi uma opção atrativa para este projeto:

  • Capacidade de iteração rápida. Isso é especialmente valioso, já que o Mural seria piloto em vários mercados.
  • Única base de código. Nossos usuários estavam divididos igualmente entre Android e iOS. Um PWA significava que poderíamos criar um único app da Web que funcionaria nas duas plataformas. Isso aumentou a velocidade e o impacto da equipe.
  • Atualizados 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 disponíveis. Conseguimos implementar alterações interruptivas no back-end com um tempo de migração muito curto para os clientes.
  • Facilmente integrado a apps próprios e de terceiros. Essas integrações eram um requisito do app. Com um PWA, isso significava simplesmente abrir um URL.
  • Remoção do atrito da instalação de um app.

Nossa estrutura

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

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 alguma complexidade, descobrimos que os benefícios deles superam a complexidade adicional.

Gere o relatório, se puder.

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

Dito isso, devido ao nosso conjunto interno de tecnologias, não era possível usar uma biblioteca para gerar e gerenciar nosso service worker. Nosso aprendizado abaixo às vezes refletirá isso. Acesse Armadilhas para service workers não gerados para ler 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.). Confira se as bibliotecas essenciais para o aplicativo são compatíveis com o service worker. Para este PWA específico, queríamos usar gapi.js para autenticação, mas não conseguimos fazer isso porque ele não era compatível com service workers. Os autores de bibliotecas também precisam reduzir ou remover as suposições desnecessárias sobre o contexto do JavaScript sempre que possível para oferecer suporte aos casos de uso de service worker, como evitar APIs incompatíveis com o service worker e evitar o estado global.

Evitar acessar o IndexedDB durante a inicialização

Não leia IndexedDB ao inicializar o script de service worker. Caso contrário, você pode se deparar com esta situação indesejada:

  1. O usuário tem um app da Web com a versão N do IndexedDB (IDB)
  2. O novo aplicativo da Web é enviado com a versão N+1 do IDB.
  3. O usuário acessa o PWA, que aciona o download do novo service worker
  4. O novo service worker lê 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 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 é instalado

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

Seja resiliente

Os scripts do service worker são executados em segundo plano, mas 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 pode ser retomado a qualquer momento.

No caso de um processo de sincronização que carregou arquivos grandes no servidor e os salvou no IDB, nossa solução para uploads parciais interrompidos era aproveitar o sistema retomável da biblioteca de upload interna, salvando o URL de upload retomável no IDB antes do upload e usando esse URL para retomar um upload caso 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 ponto do processo estávamos para cada registro.

Não depender do estado global

Como os service workers existem em um contexto diferente, muitos símbolos que podem existir não estão presentes. Grande parte do nosso código foi executado em um contexto window e em um contexto de service worker (como geração de registros, flags, sincronização etc.). O código precisa ter uma defesa em relação aos serviços usados, como armazenamento local ou cookies. Use globalThis para se referir ao objeto global de uma maneira que funcione em todos os contextos. Além disso, use com moderação os dados armazenados em variáveis globais, já que não há garantia de quando o script será encerrado e o estado removido.

Desenvolvimento local

Um componente importante dos service workers é o armazenamento em cache local de recursos. No entanto, durante o desenvolvimento, isso é exatamente o oposto do que você quer, principalmente quando as atualizações são feitas lentamente. Você ainda quer instalar o server worker 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 por meio do Chrome DevTools ativando a caixa de seleção Ignorar para rede (painel Aplicativo > painel Service workers), além de marcar a caixa de seleção Desativar cache no painel Rede 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 nosso service worker, que é ativado por padrão em builds de 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 fornece várias ferramentas de depuração úteis para PWAs. Ela 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 receber alertas se você quebrar um dos critérios para ser um PWA. Na verdade, isso aconteceu uma vez, em que o service worker não estava instalando e não percebemos isso antes de um push de produção. Ter o Lighthouse como parte da nossa CI evitaria isso.

Adote a entrega contínua

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

Conseguimos implementar alterações interruptivas no back-end com um tempo de migração muito curto para os clientes. Normalmente, daríamos um mês para os usuários atualizarem para clientes mais recentes antes de fazerem alterações interruptivas. Como o app funcionaria enquanto estava desatualizado, era possível que clientes mais antigos existissem livremente se o usuário não tivesse aberto o app por muito tempo. No iOS, os service workers são removidos após algumas semanas, portanto, esse caso não acontece. Para Android, esse problema pode ser atenuado se o conteúdo não for veiculado enquanto estiver desatualizado ou se o conteúdo expirar manualmente após algumas semanas. Na prática, nunca encontramos problemas de clientes desatualizados. O nível de 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 receber valores de cookie em um service worker

Às vezes, é necessário acessar os valores de cookie em um contexto de service worker. Nesse caso, precisávamos acessar os valores de cookies para gerar um token e autenticar as solicitações primárias de API. Em um service worker, as APIs síncronas, como document.cookies, não estão disponíveis. Sempre é possível enviar uma mensagem do service worker para clientes ativos (em janela) para solicitar os valores de cookie. 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 esse problema, criamos um endpoint em nosso servidor de front-end que simplesmente ecoou 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 receber os valores de cookie.

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

Armadilhas para service workers não gerados

Verifique se o script do service worker é alterado se qualquer arquivo estático em cache for alterado

Um padrão de PWA comum é que um service worker instale 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ão instalados apenas quando o navegador detecta que o script do service worker foi alterado de alguma forma. Por isso, tivemos que garantir que o próprio arquivo de script do service worker fosse alterado de alguma forma quando um arquivo armazenado em cache foi alterado. Fizemos isso manualmente incorporando um hash do conjunto de arquivos de recursos estáticos no nosso script de service worker. Dessa forma, cada versão produziu um arquivo JavaScript do service worker distinto. Bibliotecas de service workers, como a Workbox, automatizam esse processo para você.

Teste de unidade

As APIs do 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 gatilho de evento, o objeto de evento, aguardar o callback respondWith() e 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 é testado com mais facilidade.

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

Devido às dificuldades de testar a unidade de um script de service worker, mantivemos o script do service worker principal o mais básico possível, dividindo a maior parte da implementação em outros módulos. Como esses arquivos eram apenas módulos JS padrão, seria mais fácil fazer testes de unidade com bibliotecas de teste padrão.

Não perca as partes 2 e 3

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