</> HTML5Advent
ENFRESDEITPT

// 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.

Uma pessoa em pe diante de racks de servidores com paredes de vidro num data center, com um portatil aberto na mao, iluminacao azul e logotipos visiveis nos paineis de vidro

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.

Primeiro plano de um editor de codigo com tema escuro num monitor, mostrando codigo de configuracao Ruby com realce sintatico verde e branco, uma arvore de ficheiros a esquerda e numeros de linha a direita
Escrever codigo num editor escuro. O IndexedDB segue o mesmo padrao de pedidos e callbacks visivel na maioria das APIs do browser, aqui envolvido em promises para legibilidade.

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

ErroO que aconteceCorrecao
Criar um store fora de onupgradeneededInvalidStateErrorTodas as alteracoes ao schema vao em onupgradeneeded
Usar await dentro do callback de uma transacaoA transacao fecha-se automaticamente antes de o await resolverManter 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 versaoonupgradeneeded nunca disparaIncrementar o inteiro de versao cada vez que se altera o schema
Passar uma string onde a chave espera um numeroRegisto nao encontradoFazer 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.