// apis
Tutorial IndexedDB: construir uma app de notas que funciona offline
Um tutorial IndexedDB passo a passo. Abrir uma base de dados, criar um object store, adicionar e ler registos, consultar por indice, eliminar entradas e gerir upgrades de versao, tudo numa pequena app de notas executavel no browser.
O guia conceptual explica o que e o IndexedDB e quando o usar. Este tutorial constroi uma pequena aplicacao de notas de raiz para mostrar cada passo: abrir uma base de dados, criar um store, escrever registos, rele-los, consultar por indice e eliminar entradas.
Tudo corre no browser. Sem build tools, sem servidor, sem dependencias. Copie cada bloco para um unico ficheiro HTML e abra-o.
Passo 1: abrir a base de dados
Toda interacao com o IndexedDB comeca com indexedDB.open(). Passa-se um nome e um numero de versao. Se a base de dados ainda nao existir, ou se a versao for superior a que esta em disco, o browser emite upgradeneeded.
function openDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('notes-app', 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains('notes')) {
const store = db.createObjectStore('notes', {
keyPath: 'id',
autoIncrement: true,
});
store.createIndex('by_tag', 'tag', { unique: false });
store.createIndex('by_date', 'createdAt', { unique: false });
}
};
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
} Algumas coisas a notar. keyPath: 'id' diz ao IndexedDB qual propriedade de cada objeto e a chave primaria. autoIncrement: true faz a base de dados gerar essa chave automaticamente. As duas chamadas createIndex permitem consultar notas por tag ou por data mais tarde, sem percorrer todos os registos.
Passo 2: adicionar um registo
A escrita no IndexedDB acontece sempre dentro de uma transacao. Abre-se uma sobre o object store necessario, obtem-se uma referencia ao store e chama-se add() ou put().
async function addNote(db, text, tag) {
return new Promise((resolve, reject) => {
const tx = db.transaction('notes', 'readwrite');
const store = tx.objectStore('notes');
const record = {
text,
tag: tag || 'general',
createdAt: new Date().toISOString(),
};
const request = store.add(record);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
} add() lanca um erro se ja existir um registo com a mesma chave. put() sobrep-o. Para registos novos, add() e mais seguro porque apanha duplicados acidentais.
Passo 3: ler todos os registos
Para ler tudo o que um store contem, abre-se uma transacao readonly e chama-se getAll():
async function getAllNotes(db) {
return new Promise((resolve, reject) => {
const tx = db.transaction('notes', 'readonly');
const store = tx.objectStore('notes');
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
} Se so for preciso um registo e se conhecer a sua chave, usar store.get(key). Devolve um unico objeto em vez de um array.
Passo 4: consultar por indice
Os indices permitem encontrar registos sem percorrer o store inteiro. Para obter todas as notas com a tag "work":
async function getNotesByTag(db, tag) {
return new Promise((resolve, reject) => {
const tx = db.transaction('notes', 'readonly');
const store = tx.objectStore('notes');
const index = store.index('by_tag');
const request = index.getAll(tag);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
} Tambem se pode usar um key range para consultar um intervalo de valores. Por exemplo, para obter notas criadas apos uma certa data:
const range = IDBKeyRange.lowerBound('2026-01-01T00:00:00.000Z');
const request = store.index('by_date').getAll(range); Os quatro construtores de range sao lowerBound, upperBound, bound (ambos os extremos) e only (correspondencia exata). Cada um aceita um booleano opcional para excluir o valor limite.
Passo 5: atualizar um registo
A atualizacao usa put(). E preciso incluir a propriedade chave (id no nosso schema) para que o IndexedDB saiba qual registo sobrescrever:
async function updateNote(db, id, newText) {
return new Promise((resolve, reject) => {
const tx = db.transaction('notes', 'readwrite');
const store = tx.objectStore('notes');
const getReq = store.get(id);
getReq.onsuccess = () => {
const note = getReq.result;
if (!note) { reject(new Error('Not found')); return; }
note.text = newText;
const putReq = store.put(note);
putReq.onsuccess = () => resolve();
putReq.onerror = () => reject(putReq.error);
};
getReq.onerror = () => reject(getReq.error);
});
} Passo 6: eliminar um registo
Chamar store.delete(key) dentro de uma transacao readwrite:
async function deleteNote(db, id) {
return new Promise((resolve, reject) => {
const tx = db.transaction('notes', 'readwrite');
const store = tx.objectStore('notes');
const request = store.delete(id);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
} Para limpar todos os registos de um store, usar store.clear().
Passo 7: gerir os upgrades de versao
Quando e preciso adicionar um novo indice ou um novo object store depois de os utilizadores ja terem a versao 1, incrementar o numero de versao e gerir a migracao em onupgradeneeded:
request.onupgradeneeded = (event) => {
const db = event.target.result;
const oldVersion = event.oldVersion;
if (oldVersion < 1) {
const store = db.createObjectStore('notes', {
keyPath: 'id',
autoIncrement: true,
});
store.createIndex('by_tag', 'tag', { unique: false });
store.createIndex('by_date', 'createdAt', { unique: false });
}
if (oldVersion < 2) {
const tx = event.target.transaction;
const store = tx.objectStore('notes');
store.createIndex('by_priority', 'priority', { unique: false });
}
}; Verificar event.oldVersion para executar apenas as migracoes que o utilizador ainda nao viu. Um utilizador que passa da versao 0 (sem base de dados) executa ambos os blocos. Um utilizador que passa da versao 1 executa apenas o segundo.
Juntar tudo
Eis um script minimo que exercita todas as funcoes descritas acima:
(async () => {
const db = await openDB();
const id = await addNote(db, 'Buy groceries', 'personal');
console.log('Added note with id:', id);
const all = await getAllNotes(db);
console.log('All notes:', all);
const tagged = await getNotesByTag(db, 'personal');
console.log('Personal notes:', tagged);
await updateNote(db, id, 'Buy groceries and cook dinner');
console.log('Updated note', id);
await deleteNote(db, id);
console.log('Deleted note', id);
})(); Abrir a consola do browser para ver o resultado. Cada operacao e assincrona, cada escrita passa por uma transacao e os dados sobrevivem ao recarregamento da pagina sem qualquer servidor.
Erros comuns
| Erro | O que acontece | Correcao |
|---|---|---|
Criar um store fora de onupgradeneeded | InvalidStateError | Todas as alteracoes ao schema vao em onupgradeneeded |
Usar await dentro do callback de uma transacao | A transacao fecha-se automaticamente antes de o await resolver | Manter todas as operacoes no store sincronas dentro de um tick da transacao, ou abrir uma nova transacao apos o await |
| Esquecer de incrementar o numero de versao | onupgradeneeded nunca dispara | Incrementar o inteiro de versao cada vez que se altera o schema |
| Passar uma string onde a chave espera um numero | Registo nao encontrado | Fazer corresponder o tipo: se autoIncrement gera numeros, consultar com um numero |
O IndexedDB e verboso, mas cada peca tem um proposito: a versao protege o schema, as transacoes protegem os dados e os indices mantem as leituras rapidas. Quando estes sete passos se tornarem rotina, tera tudo o que precisa para armazenar dados estruturados no cliente, seja para suporte offline, caching ou manter estado alem da sessao.
Perguntas frequentes
- Como se abre uma base de dados IndexedDB?
- Chama-se indexedDB.open(nome, versao). O primeiro argumento e uma string que identifica a base de dados, o segundo e um inteiro de versao. A chamada devolve um IDBOpenDBRequest. Escuta-se o seu evento onsuccess para obter o handle da base de dados e onupgradeneeded para criar ou modificar object stores quando a versao muda.
- O que e o onupgradeneeded no IndexedDB?
- O onupgradeneeded dispara quando o browser ainda nao tem uma base de dados com esse nome, ou quando o numero de versao passado e superior ao que esta em disco. E o unico ponto onde se podem criar ou eliminar object stores e indices. Codigo que tente faze-lo fora deste evento lanca um erro.
- O IndexedDB funciona offline?
- Sim. O IndexedDB armazena dados no browser sem necessidade de rede. Associado a um service worker que coloca em cache o HTML, CSS e JavaScript, obtem-se uma aplicacao totalmente offline. Os dados persistem entre sessoes ate que o utilizador limpe os dados do site ou a pressao de armazenamento leve o browser a elimina-los.
- Em que difere o IndexedDB do localStorage?
- O localStorage e sincrono, armazena apenas strings e esta limitado a cerca de 5 MB. O IndexedDB e assincrono, armazena dados estruturados incluindo Blobs e ArrayBuffers, suporta indices e transacoes e pode conter centenas de megabytes. Usar localStorage para alguns pequenos flags; usar IndexedDB para tudo o que for maior ou estruturado.