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 informado sobre o resultado. Confira alguns exemplos:

  • Uma página envia ao service worker uma lista de URLs para pré-busca. Assim, quando o usuário clicar em um link, os sub-recursos do documento ou da página já estão disponíveis no cache, tornando a navegação subsequente muito mais rápida.
  • A página solicita que o service worker recupere e armazene em cache um conjunto de artigos principais para disponibilizá-los 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 lidar melhor com tarefas mais urgentes, como responder a interações do usuário.

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

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

Caso de produção

O 1-800-Flowers.com implementou o armazenamento em cache imperativo (pré-busca) com service workers via postMessage() para pré-buscar os principais itens nas páginas de categoria e acelerar a navegação subsequente até 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é-buscados:

  • No tempo de carregamento da página, eles pedem que o servicer worker recupere os dados JSON dos nove itens principais e adicione os objetos de resposta resultantes ao cache.
  • Para os itens restantes, eles detectam o evento mouseover para que, quando um usuário mover o cursor sobre um item, ele possa acionar uma busca do recurso sob "demanda".

Eles usam a API Cache para armazenar respostas JSON:

Logotipo da 1-800 Flowers.
Pré-busca de dados do produto JSON a partir de páginas de informações do produto 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.

Usar o Workbox

O Workbox oferece uma maneira fácil de enviar mensagens a um service worker usando o pacote workbox-window, um conjunto de módulos destinados a serem executados no contexto da janela. Eles complementam os outros pacotes do Workbox executados no service worker.

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

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

Em seguida, é possível enviar diretamente a mensagem de maneira declarativa, sem precisar obter o registro, verificar a ativação ou pensar na API de comunicação subjacente:

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. No entanto, em casos como estes, 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 do Workbox não for suficiente para suas necessidades, confira como implementar a comunicação entre janelas com o service worker usando APIs de navegador.

A API postMessage pode ser usada para estabelecer um mecanismo de comunicação unidirecional 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 é absolutamente 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é-buscar" ou "para limpar o armazenamento". O service worker 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, mas, caso contrário, o fluxo principal do usuário não vai ser alterado. Por exemplo, quando o 1-800-Flowers.com tenta pré-armazenar em cache, a página não precisa saber se o service worker foi bem-sucedido. Se for o caso, o usuário terá uma navegação mais rápida. Se isso não acontecer, a página ainda precisará navegar para a nova página. Vai demorar um pouco mais.

Um exemplo simples de pré-busca

Uma das aplicações mais comuns de armazenamento em cache imperativo é a pré-busca, ou seja, a busca de recursos para um determinado URL antes que o usuário vá até ele, a fim de acelerar a navegação.

Há maneiras diferentes de implementar a pré-busca em sites:

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

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

Delegar esses tipos de operações ao service worker tem as seguintes vantagens:

  • Descarregamento do trabalho pesado de processamento de busca e pós-busca (que será introduzido posteriormente) em uma linha de execução secundária. Ao fazer isso, a linha de execução principal fica liberada para lidar com tarefas mais importantes, como responder a interações do usuário.
  • Permitir que vários clientes (por exemplo, guias) reutilizem uma funcionalidade comum e até mesmo chamar o serviço simultaneamente sem bloquear a linha de execução principal.

Fazer uma pré-busca das páginas de detalhes do produto

Primeiro, use postMessage() na interface do service worker e transmita uma matriz de URLs para armazenar em 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, introduzimos uma pequena função auxiliar chamada fetchAsync() para iterar a 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, será possível confiar nos cabeçalhos de armazenamento em cache do recurso. Em muitos casos, como nas páginas de detalhes do produto, os recursos não são armazenados em cache, ou seja, eles têm um cabeçalho Cache-control de no-cache. Nesses casos, é possível substituir esse comportamento armazenando o recurso buscado no cache do service worker. Isso tem a vantagem adicional de permitir que o arquivo seja exibido em cenários off-line.

Além dos dados JSON

Quando os dados JSON são buscados em um endpoint do servidor, eles geralmente contêm outros URLs que também podem ser pré-buscados, como uma imagem ou outros dados de endpoint associados a esses dados de primeiro nível.

Digamos que, em 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 armazene 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);
  }
}

Você pode adicionar alguma manipulação de exceção em torno desse código para situações como 404s. No entanto, a beleza de usar um service worker para a pré-busca é que ele pode falhar sem muita consequência para a página e a linha de execução principal. Também é possível ter uma lógica mais elaborada no pós-processamento do conteúdo pré-buscado, tornando-o mais flexível e separado dos dados processados. 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 service worker: armazenamento em cache imperativo. Os exemplos discutidos servem apenas para demonstrar uma maneira de usar esse padrão, e a mesma abordagem também pode ser aplicada a outros casos de uso, por exemplo, armazenando em cache os principais artigos sob demanda para consumo off-line, adição aos favoritos e outros.

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

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