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, mostraremos como gerenciar dados off-line, incluindo persistência de dados, limites e as ferramentas disponíveis.

O armazenamento não envolve apenas 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, por isso 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 oferecem 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 forma confiável quando a rede é instável ou inexistente. Mas quando usar cada uma delas?

Use a API Cache Storage para recursos de rede, itens que você acessaria 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 combinados em estilo 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 um. IndexedDB é uma API assíncrona, mas ela recebe um callback em vez de retornar uma promessa. O exemplo a seguir usa a biblioteca idb de Jake Archibald, que é um wrapper de promessa pequeno para IndexedDB. As bibliotecas auxiliares não são obrigató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 novo banco de dados do IndexedDB chamado cookbook. Como os bancos de dados do IndexedDB têm controle de versão, é necessário aumentar o número da versão sempre que você fizer alterações na estrutura do banco de dados. O segundo parâmetro é a versão do banco de dados. No exemplo, é definido como 1.
  2. Um objeto de inicialização contendo um callback upgrade() é transmitido para openDB(). A função de callback é chamada quando o banco de dados é instalado pela primeira vez ou quando é atualizado 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 os dados) ou índices (que você gostaria de pesquisar). É aqui também 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 ocorra 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 armazenamento, e cria outro índice chamado type, com base na propriedade type.

Vamos dar uma olhada no armazenamento de objetos que acabou de ser criado. Depois de adicionar roteiros ao armazenamento de objetos e abrir o DevTools em navegadores baseados no Chromium ou o Web Inspector no Safari, o resultado será o seguinte:

Safari e Chrome mostrando conteúdo do IndexedDB.

Como adicionar dados

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

  1. Inicie uma transação com o mode definido como readwrite.
  2. Acesse o armazenamento de objetos em que você vai adicionar dados.
  3. Chame add() com os dados que você está salvando. O método recebe dados em formato de dicionário (como pares de chave-valor) e os adiciona ao armazenamento de objetos. O dicionário precisa ser clonável usando clonagem estruturada. Se você quiser atualizar um objeto já 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, é vantajoso aguardar operações individuais para 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 biscoitos, o roteiro estará no banco de dados com os outros roteiros. O ID é definido e incrementado automaticamente pelo IndexDB. Se você executar esse código duas vezes, terá duas entradas de cookies idênticas.

Recuperar dados

Veja como receber dados do IndexedDB:

  1. Inicie uma transação e especifique o armazenamento ou os armazenamentos 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, o armazenamento 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 da rede corretamente.

A capacidade de armazenamento é compartilhada entre todas as opções de armazenamento, incluindo armazenamento em cache, IndexedDB, Web Storage e até mesmo o arquivo do service worker e suas dependências. No entanto, a quantidade de armazenamento disponível varia de acordo com o navegador. Você provavelmente não vai acabar. sites podiam 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 espaço de armazenamento ainda está disponível para o app, a cota e o uso dele. O exemplo a seguir usa a API Storage para conseguir a cota e o uso estimados e 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 os usuários.

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, abra a seção Armazenamento na guia Aplicativo para ver a cota do seu site e a quantidade de armazenamento usada detalhada pelo uso.

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

O Firefox e o Safari não oferecem uma tela de resumo que permite a visualização de toda a cota e uso de armazenamento da origem atual.

Persistência de dados

Você pode solicitar armazenamento persistente ao navegador em plataformas compatíveis para evitar a remoção automática de dados após inatividade ou sob pressão de armazenamento. Se a permissão for concedida, o navegador nunca vai remover os dados do armazenamento. Essa proteção inclui o registro do service worker, bancos de dados do IndexedDB e arquivos no armazenamento em cache. Observe que os usuários estã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 é um acesso pela propriedade navigator.storage.

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

Também é possível 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 permanente. Os navegadores baseados no Chromium oferecem 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, instalação de PWA. Se o usuário tiver instalado um ícone para o PWA no sistema operacional, o navegador poderá conceder armazenamento permanente.

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

.

Suporte ao navegador da API

Armazenamento na Web

Compatibilidade com navegadores

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

Origem

Acesso ao sistema de arquivos

Compatibilidade com navegadores

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

Origem

Gerenciador de armazenamento

Compatibilidade com navegadores

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

Origem

Recursos