Guia de armazenamento em cache imperativo

Andrew Guan
Andrew Guan
Demián Renzulli
Demián Renzulli

Alguns sites podem precisar se comunicar com o service worker sem precisar ser informados sobre o resultado. Veja alguns exemplos:

  • Uma página envia ao service worker uma lista de URLs para pré-carregar, de modo que, quando o usuário clica em um link, os subrecursos do documento ou da página já estão disponíveis no cache, tornando a navegação posterior muito mais rápida.
  • A página solicita que o service worker recupere e armazene em cache um conjunto de artigos mais acessados para que eles fiquem disponíveis off-line.

Delegar esses tipos de tarefas não críticas ao service worker tem a vantagem de liberar a linha de execução principal para processar melhor tarefas mais urgentes, como responder às interações do usuário.

Diagrama de uma página que solicita recursos para armazenar em cache em um service worker.

Neste guia, vamos explorar como implementar uma técnica de comunicação de mão única da página para o service worker usando APIs padrão do navegador e a biblioteca Workbox. Chamaremos esses tipos de casos de uso de armazenamento em cache imperativo.

Caso de produção

A 1-800-Flowers.com implementou o armazenamento em cache obrigatório (pré-carregamento) com service workers pelo postMessage() para pré-carregar os principais itens nas páginas de categoria e acelerar a navegação subsequente para as páginas de detalhes do produto.

Logotipo da 1-800 Flowers.

Eles usam uma abordagem mista para decidir quais itens vão ser pré-carregados:

  • No momento do carregamento da página, eles pedem ao worker de serviço para recuperar os dados JSON dos 9 itens principais e adicionar os objetos de resposta resultantes ao cache.
  • Para os itens restantes, eles detectam o evento mouseover , para que, quando um usuário move o cursor sobre um item, ele possa acionar uma busca do recurso "on-demand".

Elas usam a API Cache para armazenar respostas JSON:

Logotipo da 1-800 Flowers.
Pré-busca de dados de produtos JSON nas páginas de listagem de produtos em 1-800Flowers.com.

Quando o usuário clica em um item, os dados JSON associados a ele podem ser coletados do cache, sem a necessidade de acessar a rede, tornando a navegação mais rápida.

Como usar o Workbox

O Workbox oferece uma maneira fácil de enviar mensagens para um worker do serviço pelo pacote workbox-window, um conjunto de módulos que devem ser executados no contexto da janela. Eles complementam os outros pacotes do Workbox que são executados no service worker.

Para comunicar a página com o service worker, primeiro obtenha uma referência de objeto do Workbox para o service worker registrado:

const wb = new Workbox('/sw.js');
wb.register();

Em seguida, você pode enviar a mensagem diretamente de forma declarativa, sem a necessidade de fazer o registro, verificar a ativação ou pensar na API de comunicação:

wb.messageSW({"type": "PREFETCH", "payload": {"urls": ["/data1.json", "data2.json"]}}); });

O service worker implementa um gerenciador message para ouvir essas mensagens. Ele pode retornar uma resposta, mas, em casos como esses, isso não é necessário:

self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'PREFETCH') {
    // do something
  }
});

Como usar APIs do navegador

Se a biblioteca Workbox não for suficiente para suas necessidades, confira como implementar a comunicação de janela para service worker usando APIs do navegador.

A API postMessage pode ser usada para estabelecer um mecanismo de comunicação de mão única da página para o service worker.

A página chama postMessage() na interface do service worker:

navigator.serviceWorker.controller.postMessage({
  type: 'MSG_ID',
  payload: 'some data to perform the task',
});

O service worker implementa um gerenciador message para ouvir essas mensagens.

self.addEventListener('message', (event) => {
  if (event.data && event.data.type === MSG_ID) {
    // do something
  }
});

O atributo {type : 'MSG_ID'} não é totalmente necessário, mas é uma maneira de permitir que a página envie diferentes tipos de instruções para o service worker, ou seja, "para pré-carregar" em vez de "para limpar o armazenamento". O worker de serviço pode se ramificar em diferentes caminhos de execução com base nessa flag.

Se a operação tiver sido bem-sucedida, o usuário poderá aproveitar os benefícios dela. Caso contrário, o fluxo principal do usuário não será alterado. Por exemplo, quando o 1-800-Flowers.com tenta fazer o pré-cache, a página não precisa saber se o service worker teve sucesso. Se isso acontecer, a navegação do usuário será mais rápida. Caso contrário, a página ainda precisa navegar para a nova página. Isso vai demorar um pouco mais.

Exemplo simples de pré-carregamento

Uma das aplicações mais comuns do armazenamento em cache imperativa é o pré-carregamento, ou seja, buscar recursos para um determinado URL antes que o usuário o acesse para acelerar a navegação.

Há diferentes maneiras de implementar o pré-carregamento em sites:

Para cenários de pré-carregamento relativamente simples, como pré-carregar documentos ou recursos específicos (JS, CSS etc.), essas técnicas são a melhor abordagem.

Se uma lógica adicional for necessária, por exemplo, analisar o recurso de pré-carregamento (um arquivo JSON ou página) para buscar os URLs internos, é mais apropriado delegar essa tarefa totalmente ao service worker.

A delegação desses tipos de operação para o service worker tem as seguintes vantagens:

  • Descarregar o trabalho pesado de busca e processamento pós-busca (que será apresentado mais adiante) para uma linha de execução secundária. Ao fazer isso, ele libera a linha de execução principal para processar tarefas mais importantes, como responder às interações do usuário.
  • Permitir que vários clientes (por exemplo, guias) reutilizem uma funcionalidade comum e até mesmo chamem o serviço simultaneamente sem bloquear a linha de execução principal.

Pré-buscar páginas de detalhes do produto

Primeiro, use postMessage() na interface do service worker e transmita uma matriz de URLs para o cache:

navigator.serviceWorker.controller.postMessage({
  type: 'PREFETCH',
  payload: {
    urls: [
      'www.exmaple.com/apis/data_1.json',
      'www.exmaple.com/apis/data_2.json',
    ],
  },
});

No service worker, implemente um gerenciador message para interceptar e processar mensagens enviadas por qualquer guia ativa:

addEventListener('message', (event) => {
  let data = event.data;
  if (data && data.type === 'PREFETCH') {
    let urls = data.payload.urls;
    for (let i in urls) {
      fetchAsync(urls[i]);
    }
  }
});

No código anterior, apresentamos uma pequena função auxiliar chamada fetchAsync() para iterar na matriz de URLs e emitir uma solicitação de busca para cada um deles:

async function fetchAsync(url) {
  // await response of fetch call
  let prefetched = await fetch(url);
  // (optionally) cache resources in the service worker storage
}

Quando a resposta for recebida, você poderá usar os cabeçalhos de armazenamento em cache do recurso. No entanto, em muitos casos, como nas páginas de detalhes do produto, os recursos não são armazenados em cache (o que significa que eles têm um cabeçalho Cache-control de no-cache). Em casos como esses, é possível substituir esse comportamento, armazenando o recurso buscado no cache do service worker. Isso tem o benefício adicional de permitir que o arquivo seja veiculado em cenários off-line.

Além dos dados JSON

Depois que os dados JSON são buscados de um endpoint do servidor, eles geralmente contêm outros URLs que também valem a pena fazer precarregar, como uma imagem ou outros dados de endpoint associados a esses dados de primeiro nível.

Digamos que, no nosso exemplo, os dados JSON retornados sejam as informações de um site de compras de supermercado:

{
  "productName": "banana",
  "productPic": "https://cdn.example.com/product_images/banana.jpeg",
  "unitPrice": "1.99"
 }

Modifique o código fetchAsync() para iterar a lista de produtos e armazenar em cache a imagem principal de cada um deles:

async function fetchAsync(url, postProcess) {
  // await response of fetch call
  let prefetched = await fetch(url);

  //(optionally) cache resource in the service worker cache

  // carry out the post fetch process if supplied
  if (postProcess) {
    await postProcess(prefetched);
  }
}

async function postProcess(prefetched) {
  let productJson = await prefetched.json();
  if (productJson && productJson.product_pic) {
    fetchAsync(productJson.product_pic);
  }
}

É possível adicionar um tratamento de exceção ao redor desse código para situações como 404s. No entanto, o uso de um service worker para pré-carregar dados é ótimo porque ele pode falhar sem grandes consequências para a página e a linha de execução principal. Você também pode ter uma lógica mais elaborada no pós-processamento do conteúdo pré-buscado, tornando-o mais flexível e desacoplado dos dados que ele processa. O céu é o limite.

Conclusão

Neste artigo, abordamos um caso de uso comum de comunicação unidirecional entre a página e o worker de serviço: armazenamento em cache imperativo. Os exemplos discutidos têm como objetivo demonstrar uma maneira de usar esse padrão, e a mesma abordagem pode ser aplicada a outros casos de uso, por exemplo, armazenamento em cache dos principais artigos sob demanda para consumo off-line, inclusão em favoritos e outros.

Para mais padrões de comunicação entre a página e o service worker, confira:

  • Atualizações de transmissão: chamar a página do service worker para informar sobre atualizações importantes (por exemplo, uma nova versão da WebApp está disponível).
  • Comunicação bidirecional: delegar uma tarefa a um worker de serviço (por exemplo, um download pesado) e manter a página informada sobre o progresso.