Dados off-line

Para criar uma experiência off-line sólida, seu PWA precisa gerenciar o armazenamento. No capítulo sobre armazenamento em cache, você aprendeu que o armazenamento em cache é uma opção para salvar dados em um dispositivo. Neste capítulo, mostraremos como gerenciar dados off-line, incluindo a persistência de dados, os limites e as ferramentas disponíveis.

Armazenamento

O armazenamento não é apenas sobre arquivos e recursos, mas pode incluir outros tipos de dados. Em todos os navegadores compatíveis com PWAs, as seguintes APIs estão disponíveis para armazenamento no dispositivo:

  • IndexedDB: uma opção de armazenamento de objetos NoSQL para dados estruturados e blobs (dados binários).
  • WebStorage: uma maneira de armazenar pares de strings de chave-valor usando armazenamento local ou por sessão. Ele não está disponível em um contexto de service worker. Essa API é síncrona, então não é recomendada para armazenamento de dados complexos.
  • Armazenamento em cache: conforme abordado no Módulo de armazenamento em cache.

É possível gerenciar todo o armazenamento do dispositivo com a API Storage Manager em plataformas compatíveis. A API Cache Storage e o IndexedDB fornecem acesso assíncrono ao armazenamento permanente para PWAs e podem ser acessadas na linha de execução principal, nos Web workers e nos service workers. Ambos desempenham papéis essenciais para fazer com que os PWAs funcionem de maneira confiável quando a rede é instável ou inexistente. Mas quando usar cada um?

Use a API Cache Storage para recursos de rede, ou seja, itens que você pode acessar solicitando-os por um URL, como HTML, CSS, JavaScript, imagens, vídeos e áudio.

Use o IndexedDB para armazenar dados estruturados. Isso inclui dados que precisam ser pesquisáveis ou combináveis de maneira semelhante a NoSQL ou outros dados, como dados específicos do usuário, que não correspondem necessariamente a uma solicitação de URL. O IndexedDB não foi projetado para pesquisa de texto completo.

IndexedDB

Para usar o IndexedDB, primeiro abra um banco de dados. Isso cria um novo banco de dados, caso não exista. O IndexedDB é uma API assíncrona, mas usa um callback em vez de uma promessa. O exemplo a seguir usa a biblioteca idb de Jake Archibald, que é um pequeno wrapper de promessa para IndexedDB. As bibliotecas auxiliares não são necessárias para usar o IndexedDB. No entanto, se você quiser usar a sintaxe de promessa, a biblioteca idb será uma opção.

O exemplo a seguir cria um banco de dados para conter receitas culinárias.

Como criar e abrir um banco de dados

Para abrir um banco de dados:

  1. Use a função openDB para criar um novo banco de dados IndexedDB chamado cookbook. Como os bancos de dados IndexedDB têm controle de versões, você precisa aumentar o número da versão sempre que fizer alterações na estrutura do banco de dados. O segundo parâmetro é a versão do banco de dados. No exemplo, foi definido como 1.
  2. Um objeto de inicialização que contém um callback upgrade() é transmitido para openDB(). A função de callback é chamada quando o banco de dados é instalado pela primeira vez ou quando ele faz upgrade para uma nova versão. Essa função é o único lugar em que ações podem acontecer. As ações podem incluir a criação de novos armazenamentos de objetos (as estruturas que o IndexedDB usa para organizar dados) ou índices (em que você gostaria de pesquisar). É também aqui que a migração de dados deve acontecer. Normalmente, a função upgrade() contém uma instrução switch sem instruções break para permitir que cada etapa aconteça em ordem, com base na versão antiga do banco de dados.
import { openDB } from 'idb';

async function createDB() {
  // Using https://github.com/jakearchibald/idb
  const db = await openDB('cookbook', 1, {
    upgrade(db, oldVersion, newVersion, transaction) {
      // Switch over the oldVersion, *without breaks*, to allow the database to be incrementally upgraded.
    switch(oldVersion) {
     case 0:
       // Placeholder to execute when database is created (oldVersion is 0)
     case 1:
       // Create a store of objects
       const store = db.createObjectStore('recipes', {
         // The `id` property of the object will be the key, and be incremented automatically
           autoIncrement: true,
           keyPath: 'id'
       });
       // Create an index called `name` based on the `type` property of objects in the store
       store.createIndex('type', 'type');
     }
   }
  });
}

O exemplo cria um armazenamento de objetos dentro do banco de dados cookbook chamado recipes, com a propriedade id definida como a chave de índice do repositório. Ele também cria outro índice chamado type, com base na propriedade type.

Vamos dar uma olhada no repositório de objetos que acabou de ser criado. Depois de adicionar receitas ao armazenamento de objetos e abrir o DevTools em navegadores baseados no Chromium ou o Web Inspector no Safari, você verá o seguinte:

Safari e Chrome mostrando conteúdo do IndexedDB.

Como adicionar dados

O IndexedDB usa transações. As transações agrupam ações para que aconteçam como uma unidade. Elas ajudam a garantir que o banco de dados esteja sempre em um estado consistente. Eles também são essenciais, se você tiver várias cópias do seu app em execução, para evitar gravações simultâneas nos mesmos dados. Para adicionar dados:

  1. Inicie uma transação com o mode definido como readwrite.
  2. Acesse o repositório de objetos em que você adicionará dados.
  3. Chame add() com os dados que você está salvando. O método recebe dados em forma de dicionário (como pares de chave/valor) e os adiciona ao armazenamento de objetos. O dicionário precisa ser clonável usando a clonagem estruturada. Se você quiser atualizar um objeto existente, chame o método put().

As transações têm uma promessa done que é resolvida quando a transação é concluída com sucesso ou é rejeitada com um erro de transação.

Conforme explicado na documentação da biblioteca do IDB, se você estiver gravando no banco de dados, tx.done será o sinal de que tudo foi confirmado no banco de dados. No entanto, convém aguardar operações individuais para poder ver os erros que causam a falha da transação.

// Using https://github.com/jakearchibald/idb
async function addData() {
  const cookies = {
      name: "Chocolate chips cookies",
      type: "dessert"
        cook_time_minutes: 25
  };
  const tx = await db.transaction('recipes', 'readwrite');
  const store = tx.objectStore('recipes');
  store.add(cookies);
  await tx.done;
}

Depois de adicionar os cookies, a receita estará no banco de dados com outras receitas. O ID é definido e incrementado automaticamente pelo indexadoDB. Se você executar esse código duas vezes, terá duas entradas de cookie idênticas.

Recuperar dados

Veja como receber dados do IndexedDB:

  1. Inicie uma transação e especifique os repositórios de objetos e, se quiser, o tipo de transação.
  2. Chame objectStore() dessa transação. Especifique o nome do repositório do objeto.
  3. Chame get() com a chave que você quer usar. Por padrão, o repositório usa a própria chave como índice.
// Using https://github.com/jakearchibald/idb
async function getData() {
  const tx = await db.transaction('recipes', 'readonly')
  const store = tx.objectStore('recipes');
// Because in our case the `id` is the key, we would
// have to know in advance the value of the id to
// retrieve the record
  const value = await store.get([id]);
}

O gerenciador de armazenamento

Saber gerenciar o armazenamento do PWA é particularmente importante para armazenar e transmitir respostas de rede corretamente.

A capacidade de armazenamento é compartilhada entre todas as opções de armazenamento, incluindo Armazenamento em cache, IndexedDB, Armazenamento na Web e até mesmo o arquivo do service worker e suas dependências. Porém, a quantidade de armazenamento disponível varia de acordo com o navegador. É provável que você não fique sem espaço. Os sites podem armazenar megabytes e até gigabytes de dados em alguns navegadores. O Chrome, por exemplo, permite que o navegador use até 80% do espaço total em disco, e uma origem individual pode usar até 60% de todo o espaço em disco. Para navegadores compatíveis com a API Storage, é possível saber quanto armazenamento ainda está disponível para seu app, a cota e o uso dele. O exemplo a seguir usa a API Storage para estimar a cota e o uso e depois calcula a porcentagem usada e os bytes restantes. Observe que navigator.storage retorna uma instância de StorageManager. Há uma interface Storage separada e é fácil confundi-los.

if (navigator.storage && navigator.storage.estimate) {
  const quota = await navigator.storage.estimate();
  // quota.usage -> Number of bytes used.
  // quota.quota -> Maximum number of bytes available.
  const percentageUsed = (quota.usage / quota.quota) * 100;
  console.log(`You've used ${percentageUsed}% of the available storage.`);
  const remaining = quota.quota - quota.usage;
  console.log(`You can write up to ${remaining} more bytes.`);
}

No Chromium DevTools, você pode ver a cota do seu site e a quantidade de armazenamento usada, dividida pelo que a está usando, abrindo a seção Armazenamento na guia Aplicativo.

Chrome DevTools no aplicativo, seção "Limpar armazenamento"

O Firefox e o Safari não oferecem uma tela de resumo para visualizar toda a cota e o uso de armazenamento da origem atual.

Persistência de dados

Você pode solicitar ao navegador armazenamento persistente em plataformas compatíveis para evitar a remoção automática de dados após inatividade ou pressão de armazenamento. Se ela for concedida, o navegador nunca removerá dados do armazenamento. Essa proteção inclui o registro do service worker, os bancos de dados IndexedDB e os arquivos no armazenamento em cache. Os usuários são sempre no comando e podem excluir o armazenamento a qualquer momento, mesmo que o navegador tenha concedido armazenamento permanente.

Para solicitar armazenamento permanente, chame StorageManager.persist(). Como antes, a interface StorageManager pode ser acessada pela propriedade navigator.storage.

async function persistData() {
  if (navigator.storage && navigator.storage.persist) {
    const result = await navigator.storage.persist();
    console.log(`Data persisted: ${result}`);
}

Você também pode verificar se o armazenamento permanente já foi concedido na origem atual chamando StorageManager.persisted(). O Firefox solicita permissão do usuário para usar o armazenamento persistente. Os navegadores baseados no Chromium concedem ou negam persistência com base em uma heurística para determinar a importância do conteúdo para o usuário. Um critério para o Google Chrome é, por exemplo, a instalação do PWA. Se o usuário tiver instalado um ícone para o PWA no sistema operacional, o navegador poderá conceder armazenamento permanente.

Mozilla Firefox solicitando ao usuário a permissão de persistência de armazenamento.

Suporte ao navegador da API

Armazenamento na Web

Compatibilidade com navegadores

  • 4
  • 12
  • 3,5
  • 4

Origem

Acesso ao sistema de arquivos

Compatibilidade com navegadores

  • 86
  • 86
  • 111
  • 15.2

Origem

Gerenciador de armazenamento

Compatibilidade com navegadores

  • 55
  • 79
  • 57
  • 15.2

Origem

Recursos