// 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.
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.
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?
| localStorage | IndexedDB | |
|---|---|---|
| Estilo de API | Síncrona, bloqueia a thread principal | Assíncrona, baseada em eventos |
| Valores | Só cadeias | Valores clonáveis estruturados |
| Tamanho indicativo | Cerca de 5 MB por origem | Uma parte do espaço livre em disco |
| Consultas | Só por chave | Por chave, por índice, por intervalo com cursores |
| Bom para | Tema, flags, pequenas preferências | Dados 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.