Dados off-line

Para criar uma experiência off-line sólida, seu PWA precisa de gerenciamento de 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, vamos mostrar como gerenciar dados off-line, incluindo persistência, limites e ferramentas disponíveis.

Armazenamento

O armazenamento não se limita a arquivos e recursos, mas pode incluir outros tipos de dados. Em todos os navegadores que oferecem suporte a 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 string 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.
  • Cache Storage: conforme abordado no módulo de 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 persistente para PWAs e podem ser acessados na linha de execução principal, em web workers e em service workers. Ambos desempenham papéis essenciais para que os PWAs funcionem de maneira confiável quando a rede está instável ou inexistente. Mas quando usar cada uma delas?

Use a API Cache Storage para recursos de rede, coisas que você acessaria solicitando-as por um URL, como HTML, CSS, JavaScript, imagens, vídeos e áudio.

Use IndexedDB para armazenar dados estruturados. Isso inclui dados que precisam ser pesquisáveis ou combináveis de maneira semelhante ao 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 banco de dados se ele não existir. A IndexedDB é uma API assíncrona, mas usa um callback em vez de retornar uma promessa. O exemplo a seguir usa a biblioteca idb (link em inglês) de Jake Archibald, que é um wrapper pequeno de Promise para IndexedDB. As bibliotecas auxiliares não são necessárias para usar o IndexedDB, mas, se você quiser usar a sintaxe de promessa, a biblioteca idb é uma opção.

O exemplo a seguir cria um banco de dados para armazenar 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 banco de dados IndexedDB chamado cookbook. Como os bancos de dados IndexedDB têm controle de versões, é necessário aumentar o número da versão sempre que você fizer mudanças na estrutura do banco de dados. O segundo parâmetro é a versão do banco de dados. No exemplo, ele está 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 é atualizado para uma nova versão. Essa função é o único lugar em que as ações podem acontecer. As ações podem incluir a criação de novos repositórios de objetos (as estruturas que o IndexedDB usa para organizar dados) ou índices (que você quer pesquisar). É também onde 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 no banco de dados cookbook chamado recipes, com o conjunto de propriedades id definido como a chave de índice do armazenamento, e cria outro índice chamado type, com base na propriedade type.

Vamos conferir o armazenamento 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 o conteúdo do IndexedDB.

Como adicionar dados

O IndexedDB usa transações. As transações agrupam ações para que elas aconteçam como uma unidade. Elas ajudam a garantir que o banco de dados esteja sempre em um estado consistente. Elas também são importantes se você tiver várias cópias do 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. Receba o armazenamento de objetos, onde você vai adicionar dados.
  3. Chame add() com os dados que você está salvando. O método recebe dados no formato 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ê quisesse atualizar um objeto, chamaria o método put().

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

Conforme explicado na documentação da biblioteca IDB, se você estiver gravando no banco de dados, tx.done será o sinal de que tudo foi confirmado com sucesso. No entanto, é recomendável aguardar operações individuais para que você possa ver 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 vai ficar no banco de dados com outras receitas. O ID é definido e incrementado automaticamente pelo indexedDB. Se você executar esse código duas vezes, terá duas entradas de cookie idênticas.

Recuperar dados

Veja como conseguir dados da IndexedDB:

  1. Inicie uma transação e especifique o repositório ou repositórios de objetos e, opcionalmente, o tipo de transação.
  2. Chame objectStore() dessa transação. Especifique o nome do repositório de objetos.
  3. Chame get() com a chave que você quer receber. Por padrão, a loja usa a chave como um í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 como gerenciar o armazenamento do PWA é especialmente importante para armazenar e transmitir respostas de rede corretamente.

A capacidade de armazenamento é compartilhada entre todas as opções, incluindo Cache Storage, IndexedDB, Web Storage e até mesmo o arquivo do service worker e as dependências dele. No entanto, a quantidade de armazenamento disponível varia de navegador para navegador. É improvável que você fique sem espaço. Os sites podem armazenar megabytes e até gigabytes de dados em alguns navegadores. Por exemplo, o Chrome 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. Em navegadores que oferecem suporte à 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 receber a estimativa de cota e uso e, em seguida, 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 confundir as duas.

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 quanto armazenamento é usado, dividido pelo que está usando. Para isso, abra a seção Armazenamento na guia Aplicativo.

Chrome DevTools na seção "Aplicativo", "Limpar armazenamento"

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

Persistência de dados

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

Para solicitar armazenamento permanente, chame StorageManager.persist(). Como antes, a interface StorageManager é 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 pede permissão ao usuário para usar o armazenamento permanente. Os navegadores baseados no Chromium concedem ou negam a 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 de PWA. Se o usuário tiver instalado um ícone para o PWA no sistema operacional, o navegador poderá conceder armazenamento permanente.

O Mozilla Firefox pedindo permissão de persistência de armazenamento ao usuário.

Suporte do navegador para API

Armazenamento da Web

Browser Support

  • Chrome: 4.
  • Edge: 12.
  • Firefox: 3.5.
  • Safari: 4.

Source

Acesso ao sistema de arquivos

Browser Support

  • Chrome: 86.
  • Edge: 86.
  • Firefox: 111.
  • Safari: 15.2.

Source

Gerenciador de armazenamento

Browser Support

  • Chrome: 55.
  • Edge: 79.
  • Firefox: 57.
  • Safari: 15.2.

Source

Recursos