Para criar uma experiência off-line sólida, a 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 a persistência de dados, os limites e as 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 com 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 o 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 nas plataformas compatíveis. A API Cache Storage e o IndexedDB oferecem acesso assíncrono ao armazenamento persistente para PWAs e podem ser acessados pela linha de execução principal, workers da Web e workers de serviço. Ambos desempenham papéis essenciais para que os PWAs funcionem de maneira confiável quando a rede está instável ou não existe. Mas quando usar cada uma delas?
Use a API Cache Storage para recursos de rede, ou seja, coisas que você acessaria solicitando-as por um URL, como HTML, CSS, JavaScript, imagens, vídeos e áudio.
Use a 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 novo banco de dados se ele não existir.
A IndexedDB é uma API assíncrona, mas ela usa um callback em vez de retornar uma promessa. O exemplo a seguir usa a biblioteca idb de Jake Archibald, que é um wrapper de promessas pequeno 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 de culinária.
Criação e abertura de um banco de dados
Para abrir um banco de dados:
- Use a função
openDB
para criar um novo banco de dados IndexedDB chamadocookbook
. Como os bancos de dados IndexedDB são controlados por 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. - Um objeto de inicialização que contém um callback
upgrade()
é transmitido paraopenDB()
. 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 (em que você gostaria de pesquisar). É aqui que a migração de dados precisa acontecer. Normalmente, a funçãoupgrade()
contém uma instruçãoswitch
sem instruçõesbreak
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 repositório de objetos dentro do banco de dados cookbook
chamado recipes
, com a propriedade id
definida como a chave de índice do repositório e cria outro índice chamado type
, com base na propriedade type
.
Vamos conferir o repositório de objetos que acabou de ser criado. Depois de adicionar receitas ao repositório de objetos e abrir o DevTools em navegadores baseados no Chromium ou no Web Inspector no Safari, você vai encontrar o seguinte:
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 essenciais para evitar gravações simultâneas nos mesmos dados, caso você tenha várias cópias do app em execução. Para adicionar dados:
- Inicie uma transação com o
mode
definido comoreadwrite
. - Acesse o repositório de objetos, onde você vai adicionar dados.
- 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 repositório de objetos. O dicionário precisa ser clonável usando a Clonagem estruturada. Se você quiser atualizar um objeto, chame o métodoput()
.
As transações têm uma promessa done
que é resolvida quando a transação é concluída ou rejeitada com um erro de transação.
Como explicado na documentação da biblioteca IDB, se você estiver gravando no banco de dados, tx.done
é o indicador de que tudo foi confirmado com sucesso no banco de dados. No entanto, é recomendável aguardar operações individuais para identificar erros que causem falhas na 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 estar 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
Confira como receber dados do IndexedDB:
- Inicie uma transação e especifique os repositórios de objetos e, opcionalmente, o tipo de transação.
- Chame
objectStore()
dessa transação. Especifique o nome do repositório de objetos. - 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 da PWA é particularmente importante para armazenar e transmitir respostas de rede corretamente.
A capacidade de armazenamento é compartilhada entre todas as opções de armazenamento, incluindo o armazenamento em cache, o IndexedDB, o armazenamento da Web e até mesmo o arquivo do worker de serviço e suas dependências.
No entanto, a quantidade de armazenamento disponível varia de acordo com o navegador. É improvável que você 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% do espaço total em disco. Para os navegadores que oferecem suporte à API Storage, é possível saber quanto armazenamento ainda está disponível para o app, a cota e o uso.
O exemplo a seguir usa a API Storage para estimar a cota e o uso e, em seguida, calcula a porcentagem usada e os bytes restantes. navigator.storage
retorna uma instância de StorageManager
. Há uma interface Storage
separada, e é fácil confundi-las.
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 conferir a cota do seu site e quanto armazenamento é usado, dividido por quem o usa, abrindo a seção Storage na guia Application.
O Firefox e o Safari não oferecem uma tela de resumo para conferir toda a cota e o uso de armazenamento da origem atual.
Persistência de dados
É possível solicitar ao navegador o armazenamento persistente em plataformas compatíveis para evitar a eliminação automática de dados após a inatividade ou a pressão de armazenamento. Se concedido, o navegador nunca vai 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 sempre têm o controle 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}`);
}
Também é possível verificar se o armazenamento persistente 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 permitem ou negam a persistência com base em uma heurística para determinar a importância do conteúdo para o usuário. Um dos critérios do Google Chrome é, por exemplo, a instalação de PWAs. Se o usuário tiver instalado um ícone para o PWA no sistema operacional, o navegador poderá conceder armazenamento persistente.
Suporte a navegadores de API
Armazenamento da Web
Acesso ao sistema de arquivos
Gerenciador de armazenamento