Mentalidade de service worker

Como pensar sobre os service workers.

Os service workers são poderosos e valem a pena aprender. Eles permitem que você ofereça um nível totalmente novo de experiência aos usuários. Seu site pode carregar instantaneamente. Ele pode funcionar off-line. Ele pode ser instalado como um app específico da plataforma e ter a mesma aparência, mas com o alcance e a liberdade da Web.

Mas os workers de serviço são diferentes de tudo que a maioria dos desenvolvedores da Web estão acostumados. Eles têm uma curva de aprendizado íngreme e alguns problemas que você precisa conhecer.

Recentemente, os desenvolvedores do Google e eu colaboramos em um projeto, Service Workies, um jogo sem custo financeiro para entender os service workers. Durante o desenvolvimento e o trabalho com os aspectos complexos dos service workers, encontrei alguns problemas. O que mais me ajudou foi criar algumas metáforas descritivas. Neste post, vamos analisar esses modelos mentais e entender as características paradoxais que tornam os service workers ao mesmo tempo complicados e incríveis.

O mesmo, mas diferente

Ao codificar seu worker de serviço, muitas coisas vão parecer familiares. Você pode usar seus novos recursos de linguagem JavaScript favoritos. Você ouve eventos de ciclo de vida da mesma forma que os eventos de interface. Você gerencia o fluxo de controle com promessas, como já está acostumado.

Mas outros comportamentos do service worker fazem você se confundir. Principalmente quando você atualiza a página e as mudanças no código não são aplicadas.

Uma nova camada

Normalmente, ao criar um site, você tem apenas duas camadas para pensar: o cliente e o servidor. O service worker é uma camada nova que fica no meio.

Um worker de serviço atua como uma camada intermediária entre o cliente e o servidor

Pense no service worker como uma espécie de extensão do navegador que o site pode instalar no navegador do usuário. Depois de instalado, o service worker amplia o navegador do seu site com uma camada intermediária poderosa. Essa camada de service worker pode interceptar e processar todas as solicitações feitas pelo site.

A camada do service worker tem o próprio ciclo de vida, independente da guia do navegador. Uma simples atualização de página não é suficiente para atualizar um service worker, assim como você não espera que uma atualização de página atualize o código implantado em um servidor. Cada camada tem regras exclusivas de atualização.

No jogo Service Workies, abordamos muitos detalhes do ciclo de vida do service worker e oferecemos muita prática para trabalhar com ele.

Poderoso, mas limitado

Ter um service worker no seu site oferece benefícios incríveis. Seu site pode:

Apesar de tudo o que os workers de serviço podem fazer, eles são limitados pelo design. Eles não podem fazer nada síncrono ou na mesma linha de execução do seu site. Isso significa que não há acesso a:

  • localStorage
  • o DOM
  • a janela

A boa notícia é que há várias maneiras de a página se comunicar com o service worker, incluindo postMessage direto, canais de mensagens individuais e canais de transmissão individuais.

De longa duração, mas de curta duração

Um service worker ativo continua ativo mesmo depois que um usuário sai do seu site ou fecha a guia. O navegador mantém esse worker do serviço para que ele esteja pronto na próxima vez que o usuário retornar ao site. Antes que a primeira solicitação seja feita, o service worker tem a chance de interceptá-la e assumir o controle da página. Isso permite que um site funcione off-line: o service worker pode exibir uma versão armazenada em cache da própria página, mesmo que o usuário não tenha conexão com a Internet.

Em Service Workers, mostramos esse conceito com o Kolohe (um service worker amigável) interceptando e processando solicitações.

Interrompida

Embora os service workers pareçam imortais, eles podem ser interrompidos quase a qualquer momento. O navegador não quer desperdiçar recursos em um service worker que não está fazendo nada no momento. A interrupção não é a mesma coisa que encerramento: o service worker permanece instalado e ativado. Ele é colocado em suspensão. Na próxima vez que for necessário (por exemplo, para processar uma solicitação), o navegador vai reativá-lo.

waitUntil

Devido à constante possibilidade de suspensão, o service worker precisa de uma maneira de informar ao navegador quando ele está fazendo algo importante e não quer tirar uma soneca. É aí que entra a event.waitUntil(). Esse método estende o ciclo de vida em que é usado, evitando que ele seja interrompido e que avance para a próxima fase do ciclo de vida até que estejamos prontos. Isso nos dá tempo para configurar caches, buscar recursos da rede etc.

Este exemplo informa ao navegador que a instalação do service worker não será concluída até que o cache assets seja criado e preenchido com a imagem de uma espada:

self.addEventListener("install", event => {
  event.waitUntil(
    caches.open("assets").then(cache => {
      return cache.addAll(["/weapons/sword/blade.png"]);
    })
  );
});

Cuidado com o estado global

Quando essa inicialização/parada acontece, o escopo global do service worker é redefinido. Portanto, tome cuidado para não usar nenhum estado global no service worker, ou você vai ficar triste na próxima vez que ele for ativado e tiver um estado diferente do esperado.

Considere este exemplo que usa um estado global:

const favoriteNumber = Math.random();
let hasHandledARequest = false;

self.addEventListener("fetch", event => {
  console.log(favoriteNumber);
  console.log(hasHandledARequest);
  hasHandledARequest = true;
});

Em cada solicitação, esse worker de serviço registra um número, digamos 0.13981866382421893. A variável hasHandledARequest também muda para true. Agora, o service worker fica ocioso por um tempo, então o navegador o interrompe. Na próxima vez que houver uma solicitação, o service worker será necessário novamente, então o navegador vai ativá-lo. O script é avaliado novamente. Agora hasHandledARequest foi redefinido para false, e favoriteNumber é algo completamente diferente: 0.5907281835659033.

Não é possível confiar no estado armazenado em um worker de serviço. Além disso, a criação de instâncias de coisas como canais de mensagens pode causar bugs: você vai receber uma instância nova toda vez que o worker de serviço for interrompido/iniciado.

No capítulo 3 dos service workers, mostramos que o service worker parado perde toda a cor enquanto espera para ser ativado.

visualização de um worker de serviço interrompido

Juntos, mas separados

Sua página só pode ser controlada por um service worker por vez. No entanto, é possível ter dois service workers instalados ao mesmo tempo. Quando você faz uma alteração no código do service worker e atualiza a página, não está editando o service worker. Os service workers são imutáveis. Você está criando um novo. Esse novo worker de serviço (vamos chamá-lo de SW2) será instalado, mas ainda não será ativado. Ele precisa wait que o service worker atual (SW1) seja encerrado (quando o usuário sair do site).

Mexer com os caches de outro service worker

Durante a instalação, o SW2 pode configurar as coisas, geralmente criando e preenchendo caches. Mas atenção: esse novo service worker tem acesso a tudo que o service worker atual tem. Se você não tiver cuidado, o novo service worker em espera pode bagunçar o atual. Alguns exemplos que podem causar problemas:

  • O SW2 pode excluir um cache que o SW1 está usando ativamente.
  • O SW2 pode editar o conteúdo de um cache que o SW1 está usando, fazendo com que o SW1 responda com recursos que a página não espera.

Ignorar skipWaiting

Um service worker também pode usar o método skipWaiting() de risco para assumir o controle da página assim que a instalação for concluída. Isso geralmente não é uma boa ideia, a menos que você esteja tentando substituir intencionalmente um worker de serviço com bugs. O novo service worker pode estar usando recursos atualizados que a página atual não espera, o que pode causar erros e bugs.

Começar a limpar

Para evitar que os service workers se sobreponham, use caches diferentes. A maneira mais fácil de fazer isso é criar versões dos nomes de cache usados.

const version = 1;
const assetCacheName = `assets-${version}`;

self.addEventListener("install", event => {
  caches.open(assetCacheName).then(cache => {
    // confidently do stuff with your very own cache
  });
});

Ao implantar um novo service worker, você vai aumentar o version para que ele faça o que precisa com um cache totalmente separado do service worker anterior.

visualização de um cache

Limpeza final

Quando o worker do serviço chega ao estado activated, você sabe que ele assumiu o controle e que o worker anterior está redundante. Nesse ponto, é importante limpar o service worker antigo. Além de respeitar os limites de armazenamento em cache dos usuários, isso também pode evitar bugs não intencionais.

O método caches.match() é um atalho usado com frequência para recuperar um item de qualquer cache em que haja uma correspondência. Mas ele itera os caches na ordem em que foram criados. Digamos que você tenha duas versões de um arquivo de script app.js em dois caches diferentes: assets-1 e assets-2. Sua página espera o script mais recente armazenado em assets-2. No entanto, se você não tiver excluído o cache antigo, o caches.match('app.js') vai retornar o antigo de assets-1 e provavelmente vai quebrar seu site.

Tudo o que é necessário para limpar depois dos service workers anteriores é excluir qualquer cache que o novo service worker não precise:

const version = 2;
const assetCacheName = `assets-${version}`;

self.addEventListener("activate", event => {
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.map(cacheName => {
          if (cacheName !== assetCacheName){
            return caches.delete(cacheName);
          }
        });
      );
    });
  );
});

Impedir que os service workers se choquem exige um pouco de trabalho e disciplina, mas vale a pena.

Mentalidade de service worker

Ter a mentalidade certa ao pensar sobre os workers de serviço vai ajudar você a criar o seu com confiança. Depois de entender como eles funcionam, você poderá criar experiências incríveis para seus usuários.

Se você quer entender tudo isso jogando um jogo, você está com sorte! Acesse o Service Workies (link em inglês) para aprender sobre o service worker e acabar com os problemas off-line.