APIs e Microsserviços

13 jul, 2016

Aplicativos web progressivos com service workers

Publicidade

Neste artigo, vamos discutir sobre aplicativos web progressivos e service workers. Como eles podem ajudar os usuários modernos da web móvel de hoje, e como estamos experimentando isso no Booking.com? Vamos compartilhar alguns desafios que temos encontrado, bem como alguns dos nossos aprendizados.

O que é um aplicativo web progressivo?

Um aplicativo web progressivo (PWA, em inglês) é um termo cunhado pelo Google para descrever a sua perspectiva de experiências web app-like, em que as páginas web são capazes de oferecer muitos recursos de controle de conectividade que antes somente os aplicativos tinham, push notifications, ícones da tela inicial, e por aí vai.

Antes dessa iniciativa, algumas das características em discussão já estavam disponíveis para os usuários da web móvel (com extensibilidade limitada):

  • Adicionar na home screen (requer ações manuais)
  • Modo de tela cheia
  • Acesso offline por meio de cache de aplicativo
  • API de notificação

Páginas da web, no entanto, ainda não são a primeira escolha quando se trata de entregar a melhor experiência possível em um dispositivo móvel (apesar de ser mais fácil de encontrar nos motores de busca e potencialmente salvar o incômodo de baixar e instalar megabytes, especialmente importante para visitantes em conexões 2G/3G). Com demasiada frequência, vemos sites adicionando banners ou popups de tela cheia, implorando aos usuários para baixar seus aplicativos, indo ao ponto de deixar cair sua versão móvel completamente (apenas para ser ressuscitada 5 meses depois). Os argumentos de justificativa se repetem: aplicativos nativos rodam mais suavemente e têm melhores meios para envolver os clientes novamente, e o ambiente web simplesmente não tem fallbacks graciosos em condições ruins de rede.

Um aplicativo web progressivo aborda todas essas questões, exceto a parte de renderização do desempenho. Construir um aplicativo web progressivo não te força a mudar drasticamente sua arquitetura atual de front-end ou a maneira como você trabalha; ele só lhe fornece um conjunto de ferramentas para melhorar a experiência web de forma progressiva. No fim do dia, você vai ser capaz de ter:

  • Um ícone de tela inicial que abre o site em tela cheia
  • Diálogos nativos para permitir aos usuários adicionar seu aplicativo em suas telas iniciais com um clique
  • Um site rápido e sempre usável, mesmo em conexões ruins de rede
  • Push notifications, assim como nos aplicativos nativos

A maioria desses recursos é possível por meio de service workers.

O que é um service worker?

Service workers agem essencialmente como servidores proxy que ficam entre as aplicações web, o navegador e a rede (quando disponível). Eles têm a intenção (entre outras coisas) de permitir a criação de experiências offline eficazes, interceptando pedidos de rede e tomando medidas apropriadas com base no fato de a rede estar disponível e os ativos atualizados residirem no servidor. Eles também permitem o acesso a push notifications e APIs de sincronização de background. – MDN

Em suma, um service worker é uma thread em background que toma controle de todas as requisições da rede em uma página.

Fatos rápidos

Service workers são executados em um contexto diferente, portanto, não têm acesso a elementos DOM ou variáveis JavaScript na thread principal

Por razões de segurança, a página do cliente (a thread principal) deve estar em https e o script do service worker devem estar na mesma origem, mas todos os pedidos originados a partir dessa página podem ser interceptados por service workers, mesmo se eles não estão em https ou servidos a partir de um domínio diferente

Um CacheStorage é fornecido no worker para que você possa armazenar as respostas do servidor (incluindo os cabeçalhos e corpo de resposta) localmente, e servir a pedidos futuros.

Respostas do servidor podem ser forjadas no lado do cliente, se necessário.

Tudo é assíncrono, e a maioria das APIs retorna um Promise

Suporte ao navegador

Por enquanto, apenas Chrome, Firefox e Opera têm suporte adequado para service workers. Para dispositivos móveis, isso significa que apenas Android é suportado. Já que recursos como ícones de homescreen e push notifications estão integrados no SO, toda a iniciativa Progressive Web App realmente depende de quão entusiasmados os fornecedores do SO estarão sobre isso.

Em relação a service workers, a atitude da Apple é: As pessoas pensam que eles querem, alguns deles realmente querem. Nós provavelmente devemos fazer.

(Ao que parece, então, não vamos esperar por muito tempo antes de que os service workers estejam disponíveis em iPhones.)

Para uma tabela de compatibilidade detalhada de todas as características de service workers, confira este documento: Is ServiceWorker ready?

O que os service workers podem fazer?

A API ServiceWorker fornece métodos muito granulares para os desenvolvedores interceptarem pedidos, para armazenarem em cache e forjarem respostas, abrindo as portas para todos os tipos de atividades interessantes, tais como:

  • Acesso offline a determinadas páginas (uma confirmação de pedido, um e-ticket etc.)
  • Precaching com base em previsões das próximas ações do usuário (previsões não dependem de service workers em si, mas o cache gerenciável pode ser mais programável com service workers. Você pode até mesmo introduzir um tempo de expiração ou o algoritmo LRU, se você quiser)
  • Servir uma versão em cache quando se leva muito tempo para carregar alguns recursos
  • Reescrever URLs a serem sempre solicitadas com uma url canônica

Verifique o Offline Cookbook para mais detalhes sobre as estratégias de cache.

Além disso, os service workers também são usados para organizar a comunicação em background com os servidores (pense nisso como um “serviço”). Características como push notifications, sincronização de background, agendador de tarefas, tudo depende de service workers em certa extensão.

Service workers em ação

Agora, vamos sujar as mãos e nos familiarizar com service workers em ação.

Registro

Como service workers são executados em um contexto diferente, você precisa colocar o código para o worker em um arquivo separado e, em seguida, registrar na página do cliente:

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('service-worker.js', { scope: './' }).then(function() {
    if (navigator.serviceWorker.controller) {
        console.log('The service worker is currently handling network operations.');
    } else {
        console.log('Failed to register.');
    }
  });
}

Esse trecho registra um service worker com o arquivo service-worker.js. Uma vez registrado, o código nesse arquivo será capaz de controlar todas as solicitações originadas a partir de qualquer página dentro do parâmetro scope.

Por padrão, scope é o local base do script do service worker. Por exemplo, se você registrou “/static/js/serviceworker.js”, então o escopo padrão seria “/static/js/”. O script em si deve estar dentro da mesma origem que a página do cliente, por isso não é possível servir scripts de service worker com CDNs em diferentes domínios. Mas é possível substituir o escopo para estar fora do local de base do script:

navigator.serviceWorker.register('/scripts/service-worker.js', { scope: '/' })

Esse código permite que o service worker controle todas as páginas no caminho raiz da origem ({ scope: ‘/’ }). Mas você vai precisar adicionar um cabeçalho de resposta adicional Service-Worker-Allowed para fazê-lo funcionar.

Por exemplo, numa configuração nginx, pode ser feito da seguinte forma:

Server {

    listen www.example.com:443 ssl;

    ...

    location /scripts/service-worker.js {
        add_header 'Service-Worker-Allowed' '/';
    }
}

(Note que esse cabeçalho é adicionado para o script do service worker script, não a página em que foi registrado.)

Dentro do worker

Uma vez cadastrado, um service worker irá residir no background e interceptar todas as solicitações provenientes das páginas do cliente e ficar ativo até ser descadastrado.

O script é executado em um contexto chamado ServiceWorkerGlobalScope. Diversas variáveis e métodos globais estão disponíveis nesse contexto:

  • clients – Informações sobre páginas do cliente, usado para reivindicar controle sobre eles
  • registration – Representa o estado do registro
  • cache – O objeto CacheStorage em que você pode armazenar as respostas do servidor
  • skipWaiting() – Permite que o registro possa processar desde espera até um estado ativo
  • fetch(..) – Parte da API GlobalFetch, também disponível na thread principal
  • importScripts(..) – Scripts de importação JS de forma síncrona, ideal para carregar uma biblioteca de service worker

A equipe do Google Chrome forneceu uma bibioteca de alto nível para ajudar a lidar com tarefas de service worker. Ela vem com um roteador para aplicar padrões de cache expressivamente comuns a diferentes recursos, bem como um kit de ferramentas para precaching e gerenciamento de cache namespaced. É altamente recomendável usar essa biblioteca se você quiser construir algo pronto para produção; você economiza muito trabalho e também é um bom começo para você se familiarizar com os conceitos básicos em uma ServiceWorker. Confira as receitas para exemplos utilizáveis.

Se você está realmente atrás de detalhes, consulte o documento MDN Service Worker API e preste uma atenção extra a CacheStorage e FetchEvent.

Service workers no Booking.com

No Booking.com, estamos sempre abertos a testar novas tecnologias, e incentivamos qualquer inovação que melhore a satisfação do cliente. Estamos atualmente trabalhando bem próximos da equipe defensora de PWA do Google na aplicação de alguns dos principais recursos do aplicativos web progressivos para o nosso site móvel para ver onde ele ajuda os nossos clientes.

Booking-homepage

Instalar service workers para os usuários é relativamente simples – só é preciso eles estarem usando um navegador com suporte (atualmente isso significa usar o Chrome no Android). O verdadeiro desafio, no entanto, reside na forma como introduzir os recursos significativos enquanto se mede cuidadosamente o impacto. No Booking.com, fazemos todo o projeto voltado para o cliente em experimentos A/B teste, e tentamos alcançar as coisas em “pequenos passos viáveis”. O objetivo é publicar as coisas certas o mais rápido que pudermos. Mesmo para algo tão holístico como aplicativo web progressivo, trabalhamos em pequenos passos, a fim de resolver os problemas um a um, e aprender as coisas rapidamente.

Reunimos alguns aprendizados importantes sobre esse tema. O que se segue são alguns dos nossos aprendizados que podem ser interessantes para o público em geral.

Exemplos de estratégia de cache

service-worker-chrome-dev-panel

O Offline Cookbook resumiu algumas estratégias de cache para diferentes casos de uso.

  • cacheFirst – Servir cache, se ele existir, os pedidos ainda irão disparar, e novas respostas irão atualizar o cache
  • cacheOnly – Responder com o cache somente, nunca disparar a requisição real
  • networkFirst – Sempre tente buscar primeiro na rede e guarde a última resposta bem sucedida em cache, que será servida quando a rede falhar
  • networkOnly – Nunca use cache local

Vamos ver alguns exemplos de como aplicar cada um deles na vida real.

Para arquivos estáticos que nunca mudam, podemos seguramente servir com “cacheFirst”:

toolbox.router.get(/static\/(css|js|images|img)\//,
    toolbox.cacheFirst, {
       cache: { name: 'static-files' }
    }
);

Eles raramente mudam e, mesmo se eles o fizerem, nós atualizamos as URLs. Pode-se perguntar: qual é o uso dessa técnica se já definimos a data de validade nos cabeçalhos? Um service worker dá controle mais granular sobre o quanto de cache que você deseja armazenar e quando expirá-los. Por exemplo, sw-toolbox fornece configurações muito fáceis para maxEntries e maxAgeSeconds.

Para documentos HTML comuns, podemos usar “networkFirst”:

toolbox.router.get(/\/(confirmation|mybooking|myreservations)/i, 
    toolbox.networkFirst, {
        networkTimeoutSeconds: 10,
        cache: { name: 'booking-confirm' }
    }
);

Nós configuramos o parâmetro networkTimeoutSeconds aqui. Se for aceitável exibir essa página para visitantes no modo offline, então deve ser também aceitável oferecer a versão em cache para usuários com conexões de rede muito lentas e economizar algum tempo de espera para eles. Mas, claro, os segundos de tempo de espera dependem do seu tipo de negócio e da qualidade da conectividade comum de seus usuários.

Para solicitações de coleta de dados de comportamento do usuário, você pode querer usar “networkOnly”:

toolbox.router.any(/www.google-analytics.com/, toolbox.networkOnly);

Não há nenhum ponto para voltar o cache para um pedido de rastreamento, certo? Se a solicitação falhar, ela falha. Se você quiser, pode até mesmo monitorar o status de uma solicitação de rastreamento e reenviar quando ela falhar. Isso não será possível se (de alguma forma) o cache no service worker for acionado.

Atalhos locais

Não seria bom se os usuários pudessem salvar um link permanente no bookmarks que sempre redirecionaria para a última confirmação de reserva que eles viram?

Vamos adicionar um manipulador personalizado para a página de confirmação:

toolbox.router.get("/confirmations/(.*)", function(request, values, options) {
    var url = request.url;
    var promise = toolbox.networkFirst(request, values, options);
    var confirmationId = values[0]; 
    if (confirmationId) {
        // when the request finishes
        promise.then(function(response) {
            if (!response || response.status !== 200) return;
            self.caches.open('last-confirmation').then(function(cache) {
                // save a 302 Redirect response to "/confirmation"
                var redirectResponse = new Response('Redirecting', {
                    status: 302,
                    statusText: 'Found',
                    headers: {
                        Location: url
                    }
                });
                cache.put('/confirmation', redirectResponse);
            });
        });
    }
    return promise;
}, {
    networkTimeoutSeconds: 10,
    cache: {
        name: 'confirmations',
    }
});

toolbox.router.get('/confirmation', toolbox.cacheOnly, {
    cache: {
        name: 'last-confirmation'
    }
});

Cada vez que os usuários visitam uma página de confirmação, vamos voltar a resposta como normal, com a estratégia de “networkFirst”. Mas, além disso, vamos forçar uma resposta de redirecionamento 302 localmente, apontando para a url atual, em seguida, salvamos a resposta falsa em um armazenamento de cache chamado de last-confirmation com chave de URL /confirmation.

Nós também adicionamos uma regra no roteador para esse caminho e esse armazenamento de cache, de modo que nas próximas vezes em que os usuários visitarem a URL “/confirmation”, eles sempre serão redirecionados para a última página de confirmação que visitaram.

A resposta forjada foi colocada em um espaço de nomes de armazenamento de cache separado, e é servida com a estratégia cacheOnly. Porque, aparentemente, a URL é válida apenas localmente. Nós certamente não queremos misturar com pedidos normais.

O problema do domínio seguro

Para proteger os dados dos usuários, todas as partes do nosso processo de reserva e páginas de gestão de conta de usuários são servidas via HTTPS, sob um domínio separado “secure.booking.com”, em vez de “www.booking.com” – que é utilizado para o conteúdo público, tal como os resultados de pesquisa e detalhes das páginas do hotel.

No entanto, você não pode registrar um service worker entre dois domínios diferentes, mesmo que eles sejam subdomínios do mesmo domínio raiz. E (pelo menos por agora) não há nenhuma maneira de deixar dois service workers se comunicarem uns com os outros.

E se você quiser fazer um pré-cache de ativos para secure.booking.com enquanto os usuários ainda estiverem em www.booking.com, ou o contrário? Temos um monte de pessoas que salta entre os dois domínios, especialmente quando eles estão fazendo uma reserva. Além disso, com todas as funcionalidades importantes espalhadas em diferentes domínios, um service worker para um único domínio simplesmente não pode oferecer uma experiência offline ininterrupta.

Por causa disso, estamos unificando todas as funcionalidades básicas sob o mesmo domínio, e isso vai dar aos usuários total acesso HTTPS para toda sua jornada no Booking.com. Enquanto isso, os especialistas do grupo Service Worker Specs estão trabalhando em uma nova API chamada “foreign fetch”, que dará às autoridades dos service workers o poder de interceptar todos os pedidos de recursos dentro de seus escopos (como definido quando foram registrados). Esses pedidos podem ser originados a partir de qualquer página, mesmo que a página esteja em outro domínio.

Pensamentos finais

A API ServiceWorker mira em um problema de longa data para a web conectividade móvel. Ela tem o potencial para tornar a experiência do usuário suportável, mesmo quando a conectividade é ruim. Ela habilita modernas aplicações web com a capacidade de envolver os usuários de maneiras mais íntimas, e definitivamente aumenta a competitividade das aplicações web sobre as nativas.

A visão de aplicativo web progressivo é legal, mas, para um site de larga escala com uma velocidade muito alta, você não pode implementar tudo e enviar de uma só vez. Experimentar constantemente, aprendendo e melhorando as coisas com pequenos passos, é a chave para o sucesso.

Recursos

  1. Progressive Web Apps
  2. Service Worker Spec
  3. ServiceWorker API doc on MDN
  4. Service Worker Debugging
  5. Recipes 1
  6. Recipes 2
  7. Demos by W3C web mobile group

***

Jesse Yang faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: http://blog.booking.com/progressive-web-apps-with-service-workers.html