</> HTML5Advent
ENFRESDEITPT

// apis · Web Platform Advent #23

O que é o IndexedDB? A base de dados do navegador explicada

O IndexedDB é a verdadeira base de dados do navegador: assíncrona, transacional e muito maior do que o localStorage. Como funciona o onupgradeneeded, porque as transações se fecham sozinhas, para que servem os índices e quando usá-lo em vez do Web Storage.

Uma parede de gavetas de ficheiro em madeira com porta-etiquetas de latão e números escritos à mão

O IndexedDB é a verdadeira base de dados do navegador. Onde o Web Storage lhe dá uma caixa síncrona de cadeias limitada a uns 5 MB, o IndexedDB oferece um armazém assíncrono e transacional para dados estruturados, dimensionado em centenas de megabytes ou mais. Não é SQL. É um armazém chave-valor com índices, e assim que isso encaixa o resto da API vem por si.

Abrir uma base, e o único sítio onde pode mudar a sua forma

Abre-se uma base pelo nome e por um número de versão. Se a versão que pede for superior à que está no disco, o navegador dispara upgradeneeded, e esse manipulador é o único lugar onde pode criar ou apagar object stores e índices.

const request = indexedDB.open('notes-db', 1);

request.onupgradeneeded = (event) => {
  const db = event.target.result;
  const store = db.createObjectStore('notes', { keyPath: 'id', autoIncrement: true });
  store.createIndex('by_tag', 'tag', { unique: false });
};

request.onsuccess = (event) => {
  const db = event.target.result;
};

request.onerror = () => console.error(request.error);

Isto apanha as pessoas desprevenidas porque inverte o hábito. Não pode criar um store preguiçosamente na primeira vez que precisa dele. O esquema vive numa única função, presa a um número de versão que sobe quando a estrutura muda.

Tudo é um pedido, e os pedidos são assíncronos

Quase todas as chamadas do IndexedDB devolvem um IDBRequest em vez de um valor. O resultado lê-se no onsuccess e a falha trata-se no onerror. Nada bloqueia a thread principal, e é toda a razão para o preferir ao localStorage assim que o volume conta.

Uma gaveta aberta de fichas com separadores numerados de 166 a 169. Um índice do IndexedDB é exatamente isto: uma segunda ordenação dos mesmos registos, mantida para se poder procurá-los por outra coisa que não a sua chave.
Uma gaveta aberta de fichas com separadores numerados de 166 a 169. Um índice do IndexedDB é exatamente isto: uma segunda ordenação dos mesmos registos, mantida para se poder procurá-los por outra coisa que não a sua chave.

Leituras e escritas acontecem dentro de uma transação

Não se toca num store diretamente. Abre-se uma transação sobre um ou mais stores, em modo readonly ou readwrite, e trabalha-se através dela.

const tx = db.transaction('notes', 'readwrite');
const store = tx.objectStore('notes');

store.add({ title: 'Buy milk', tag: 'errand', done: false });

tx.oncomplete = () => console.log('written');
tx.onerror = () => console.error(tx.error);

Aqui está a armadilha que convém conhecer de antemão. Uma transação confirma automaticamente assim que não lhe resta trabalho pendente. Se esperar por algo alheio a meio, por exemplo um fetch, a transação fecha-se durante a espera e a chamada seguinte lança TransactionInactiveError. Faça primeiro o trabalho de fora e abra a transação depois.

Os índices, que explicam o nome

Por omissão os registos são obtidos pela chave primária. Um índice é uma segunda ordenação dos mesmos registos, que permite consultar por outra propriedade.

const tx = db.transaction('notes', 'readonly');
const index = tx.objectStore('notes').index('by_tag');

const req = index.getAll('errand');
req.onsuccess = () => console.log(req.result);

Para intervalos em vez de correspondências exatas, combine um índice com IDBKeyRange e um cursor, que percorre os resultados um a um em vez de os carregar todos em memória.

O que pode guardar, e quanto

O IndexedDB aceita tudo o que o algoritmo de clonagem estruturada trata: objetos, arrays, Date, Blob, File, ArrayBuffer. Funções e nós do DOM não são clonáveis e lançam erro. Não é preciso passar por JSON.stringify, o que também significa que os números continuam números e as datas continuam datas.

A quota é uma parte do disco disponível e não um teto fixo de 5 MB, por isso é o sítio certo para respostas de API em cache, documentos offline ou multimédia. Esse armazenamento é best effort por omissão e pode ser despejado sob pressão de espaço; chame navigator.storage.persist() se os dados tiverem de sobreviver a isso, e navigator.storage.estimate() para ver quanto está a usar.

IndexedDB ou localStorage?

localStorageIndexedDB
Estilo de APISíncrona, bloqueia a thread principalAssíncrona, baseada em eventos
ValoresSó cadeiasValores clonáveis estruturados
Tamanho indicativoCerca de 5 MB por origemUma parte do espaço livre em disco
ConsultasSó por chavePor chave, por índice, por intervalo com cursores
Bom paraTema, flags, pequenas preferênciasDados offline, caches, ficheiros, listas grandes

Ambos estão limitados à origem e nenhum é sítio para segredos: qualquer script da página os consegue ler.

Vale a pena usar um wrapper?

A API em bruto é verbosa porque é anterior às promessas. Pequenas bibliotecas embrulham os pedidos em promessas e retiram quase todo o código repetitivo, e são uma escolha por omissão razoável para código de aplicação. Ainda assim, aprenda primeiro o modelo por baixo: upgradeneeded, as transações e a sua confirmação automática comportam-se da mesma maneira por dentro, e todos os erros desconcertantes que vai encontrar vêm desses três pontos, não do wrapper.

Escolha o IndexedDB quando os dados forem estruturados, grandes ou tiverem de estar disponíveis offline, muitas vezes ao lado de um service worker. Guarde o localStorage para o punhado de pequenas flags onde uma leitura síncrona é mesmo mais simples.