JavaScript e IndexedDB

JavaScript e IndexedDB

Neste artigo, vamos explicar JavaScript e IndexedDB.

Este tutorial fornece uma explicação passo a passo de JavaScript e IndexedDB, incluindo exemplos de código prático em cada etapa para ajudar a aprofundar seu entendimento.

YouTube Video

javascript-indexed-db.html
  1<!DOCTYPE html>
  2<html lang="en">
  3<head>
  4  <meta charset="UTF-8">
  5  <title>JavaScript &amp; HTML</title>
  6  <style>
  7    * {
  8        box-sizing: border-box;
  9    }
 10
 11    body {
 12        margin: 0;
 13        padding: 1em;
 14        padding-bottom: 10em;
 15        font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
 16        background-color: #f7f9fc;
 17        color: #333;
 18        line-height: 1.6;
 19    }
 20
 21    .container {
 22        max-width: 800px;
 23        margin: 0 auto;
 24        padding: 1em;
 25        background-color: #ffffff;
 26        border: 1px solid #ccc;
 27        border-radius: 10px;
 28        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
 29    }
 30
 31    .container-flex {
 32        display: flex;
 33        flex-wrap: wrap;
 34        gap: 2em;
 35        max-width: 1000px;
 36        margin: 0 auto;
 37        padding: 1em;
 38        background-color: #ffffff;
 39        border: 1px solid #ccc;
 40        border-radius: 10px;
 41        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
 42    }
 43
 44    .left-column, .right-column {
 45        flex: 1 1 200px;
 46        min-width: 200px;
 47    }
 48
 49    h1, h2 {
 50        font-size: 1.2rem;
 51        color: #007bff;
 52        margin-top: 0.5em;
 53        margin-bottom: 0.5em;
 54        border-left: 5px solid #007bff;
 55        padding-left: 0.6em;
 56        background-color: #e9f2ff;
 57    }
 58
 59    button {
 60        display: block;
 61        margin: 1em auto;
 62        padding: 0.75em 1.5em;
 63        font-size: 1rem;
 64        background-color: #007bff;
 65        color: white;
 66        border: none;
 67        border-radius: 6px;
 68        cursor: pointer;
 69        transition: background-color 0.3s ease;
 70    }
 71
 72    button:hover {
 73        background-color: #0056b3;
 74    }
 75
 76    #output {
 77        margin-top: 1em;
 78        background-color: #1e1e1e;
 79        color: #0f0;
 80        padding: 1em;
 81        border-radius: 8px;
 82        min-height: 200px;
 83        font-family: Consolas, monospace;
 84        font-size: 0.95rem;
 85        overflow-y: auto;
 86        white-space: pre-wrap;
 87    }
 88
 89    .highlight {
 90        outline: 3px solid #ffc107; /* yellow border */
 91        background-color: #fff8e1;  /* soft yellow background */
 92        transition: background-color 0.3s ease, outline 0.3s ease;
 93    }
 94
 95    .active {
 96        background-color: #28a745; /* green background */
 97        color: #fff;
 98        box-shadow: 0 0 10px rgba(40, 167, 69, 0.5);
 99        transition: background-color 0.3s ease, box-shadow 0.3s ease;
100    }
101  </style>
102</head>
103<body>
104    <div class="container">
105        <h1>JavaScript Console</h1>
106        <button id="executeBtn">Execute</button>
107        <div id="output"></div>
108    </div>
109
110    <script>
111        // Override console.log to display messages in the #output element
112        (function () {
113            // Override console.log
114            const originalLog = console.log;
115            console.log = function (...args) {
116                originalLog.apply(console, args);
117                const message = document.createElement('div');
118                message.textContent = args
119                    .map(arg => (typeof arg === "object" && arg !== null ? JSON.stringify(arg) : String(arg)))
120                    .join(" ");
121                output.appendChild(message);
122            };
123
124            // Override console.error
125            const originalError = console.error;
126            console.error = function (...args) {
127                originalError.apply(console, args);
128                const message = document.createElement('div');
129                message.textContent = args
130                    .map(arg => (typeof arg === "object" && arg !== null ? JSON.stringify(arg) : String(arg)))
131                    .join(" ");
132                message.style.color = 'red'; // Color error messages red
133                output.appendChild(message);
134            };
135        })();
136
137        document.getElementById('executeBtn').addEventListener('click', () => {
138            // Prevent multiple loads
139            if (document.getElementById('externalScript')) return;
140
141            const script = document.createElement('script');
142            script.src = 'javascript-indexed-db.js';
143            script.id = 'externalScript';
144            //script.onload = () => console.log('javascript-indexed-db.js loaded and executed.');
145            //script.onerror = () => console.log('Failed to load javascript-indexed-db.js.');
146            document.body.appendChild(script);
147        });
148    </script>
149</body>
150</html>

JavaScript e IndexedDB

IndexedDB é um banco de dados de armazenamento de chave-valor assíncrono incorporado nos navegadores. Oferece recursos semelhantes aos bancos de dados relacionais e permite armazenar e pesquisar grandes quantidades de dados estruturados no lado do cliente. É especialmente útil para aplicativos com suporte offline e PWAs (Progressive Web Apps).

Características do IndexedDB

  • Opera de forma assíncrona e orientada a eventos.
  • Objetos JavaScript podem ser armazenados em object stores.
  • É possível pesquisar por consultas ou índices.
  • Possui grande capacidade de armazenamento (centenas de MB ou mais), permitindo armazenar muito mais dados do que cookies ou localStorage.

Abrindo e Criando um Banco de Dados

Para usar o IndexedDB, você deve primeiro abrir um banco de dados. Se não existir, ele será criado automaticamente.

1const request = indexedDB.open('MyDatabase', 1); // Specify DB name and version
  • O método open abre o banco de dados de forma assíncrona e dispara os seguintes três eventos.

onsuccess

1// Fired when database is successfully opened
2request.onsuccess = (event) => {
3    const db = event.target.result; // Database instance
4    console.log('Database opened successfully:', db.name);
5};
  • O evento onsuccess é disparado quando o banco de dados é aberto com sucesso. As operações subsequentes devem ser realizadas usando request.result, que estará disponível a partir deste ponto.

onerror

1// Fired when database fails to open
2request.onerror = (event) => {
3    console.error('Failed to open database:', event.target.error);
4};
  • O evento onerror é disparado quando o banco de dados falha ao abrir. O registro e o tratamento de erros devem ser feitos aqui.

onupgradeneeded

 1// Fired when database is newly created or upgraded
 2request.onupgradeneeded = (event) => {
 3    const db = event.target.result;
 4    console.log('Database upgrade needed (or newly created):', db.name);
 5
 6    // Example: Create an object store (like a table) if it doesn’t exist
 7    if (!db.objectStoreNames.contains('users')) {
 8        db.createObjectStore('users', { keyPath: 'id' });
 9        console.log('Object store "users" created');
10    }
11};
  • O evento onupgradeneeded é disparado quando o banco de dados é criado pela primeira vez ou quando a versão especificada é maior que a versão atual. A criação de tabelas (object stores) e definição do esquema devem ser feitas neste momento.

Criando um Object Store

Primeiro, crie um 'object store' (equivalente a uma tabela) dentro do onupgradeneeded.

1request.onupgradeneeded = function (event) {
2    const db = event.target.result;
3    console.log("onupgradeneeded triggered. Database version:", db.version);
4    if (!db.objectStoreNames.contains('users')) {
5        console.log("Creating object store: users");
6        const store = db.createObjectStore('users', { keyPath: 'id' });
7        store.createIndex('name', 'name', { unique: false });
8    }
9};

Aqui, as seguintes configurações são aplicadas:.

  • createObjectStore createObjectStore é um método para criar um novo object store no banco de dados. Você pode armazenar registros em um object store e definir como os dados serão gerenciados especificando o keyPath e outras opções. Este processo deve ser feito dentro do evento onupgradeneeded.

  • keyPath: 'id' Defina o keyPath para a propriedade id, que identifica exclusivamente cada registro como chave primária. Isso permite que o id seja usado automaticamente ao adicionar, pesquisar ou atualizar dados.

  • createIndex Use o método createIndex para criar um índice baseado na propriedade name para pesquisas. Ao definir unique: false, vários registros com o mesmo name serão permitidos.

Lidando com Conexões Bem-sucedidas ao Banco de Dados

Assigne processos a serem executados após uma conexão bem-sucedida ao banco de dados ao evento onsuccess. Neste processo, você obtém a instância do banco de dados e se prepara para ler e gravar dados.

1request.onsuccess = function (event) {
2    const db = event.target.result;
3    console.log('Database opened successfully');
4    // Use db for reading and writing in subsequent operations
5};
  • Esse processo é executado quando a conexão com o banco de dados IndexedDB é concluída com sucesso.

  • event.target.result contém a instância de banco de dados aberta (objeto IDBDatabase), que é usada para iniciar transações e acessar object stores.

  • As operações reais de leitura e escrita, como adicionar, recuperar, atualizar e excluir dados, são realizadas usando o objeto db.

Neste ponto, o banco de dados está pronto, então você pode iniciar transações com segurança.

Adicionando Dados

Veja como adicionar novos dados ao IndexedDB.

 1function addUser(db, user) {
 2    const transaction = db.transaction('users', 'readwrite');
 3    const store = transaction.objectStore('users');
 4    const request = store.add(user);
 5
 6    request.onsuccess = () => {
 7        console.log('User added:', user);
 8    };
 9
10    request.onerror = () => {
11        console.error('Add failed:', request.error);
12    };
13}
14
15// Example: Add a user
16request.onsuccess = function (event) {
17    const db = event.target.result;
18    addUser(db, { id: 1, name: 'Alice' });
19    addUser(db, { id: 2, name: 'Bob' });
20    addUser(db, { id: 3, name: 'John' });
21};
  • Crie uma transação com db.transaction() e especifique o object store a ser operado e o modo (neste caso, readwrite).
  • Adicione novos dados usando o método store.add().
  • Transações são confirmadas automaticamente, mas se você quiser agrupar várias operações, pode gerenciá-las usando eventos de término de transação.

Recuperando Dados (Busca por Chave Primária)

Veja como recuperar dados específicos usando a chave primária.

 1function getUserById(db, id) {
 2    const transaction = db.transaction('users', 'readonly');
 3    const store = transaction.objectStore('users');
 4    const request = store.get(id);
 5
 6    request.onsuccess = () => {
 7        if (request.result) {
 8            console.log('User found:', request.result);
 9        } else {
10            console.log('User not found');
11        }
12    };
13
14    request.onerror = () => {
15        console.error('Error retrieving user:', request.error);
16    };
17}
18
19// Example: Get a user by id
20request.onsuccess = function (event) {
21    const db = event.target.result;
22    getUserById(db, 1);
23};
  • Crie uma transação somente leitura com db.transaction().
  • Use store.get(id) para recuperar os dados correspondentes à chave primária especificada.
  • onsuccess é chamado quando a recuperação é bem-sucedida, e o resultado é exibido se estiver disponível. Se não houver resultado, é tratado como 'nenhum dado correspondente.'.

Usando Busca por Índice

Se você quiser pesquisar por propriedades além da chave primária, use o índice que você criou anteriormente.

 1function getUserByName(db, name) {
 2    const transaction = db.transaction('users', 'readonly');
 3    const store = transaction.objectStore('users');
 4    const index = store.index('name');
 5    const request = index.get(name);
 6
 7    request.onsuccess = () => {
 8        if (request.result) {
 9            console.log('User by name:', request.result);
10        } else {
11            console.log('No user found with that name');
12        }
13    };
14
15    request.onerror = () => {
16        console.error('Error retrieving user by name:', request.error);
17    };
18}
19
20// Example: Get a user by id
21request.onsuccess = function (event) {
22    const db = event.target.result;
23    getUserByName(db, 'Alice');
24};
  • Acesse o índice name criado previamente usando store.index('name').
  • index.get(value) recupera o primeiro registro com o valor correspondente. Se vários registros tiverem o mesmo valor, você pode recuperá-los todos com index.getAll(value).

Atualizando Dados

Para atualizar dados existentes (sobrescrever), use o método put().

Se existir um registro com a chave primária correspondente, ele será atualizado; caso contrário, um novo registro será adicionado.

 1function updateUser(db, updatedUser) {
 2    const transaction = db.transaction('users', 'readwrite');
 3    const store = transaction.objectStore('users');
 4    const request = store.put(updatedUser);
 5
 6    request.onsuccess = () => {
 7        console.log('User updated:', updatedUser);
 8    };
 9
10    request.onerror = () => {
11        console.error('Update failed:', request.error);
12    };
13}
14
15// Example: Update user
16request.onsuccess = async (event) => {
17    const db = event.target.result;
18
19    // Test : update existing user
20    updateUser(db, { id: 3, name: 'John Updated' });
21
22    // Test : insert new user
23    updateUser(db, { id: 4, name: 'Charlie' });
24};
  • put() é um método conveniente que suporta tanto atualização quanto adição.
  • Se já existir um dado com a mesma chave primária, ele será sobrescrito.
  • Se você quiser verificar a existência antes de atualizar, pode usar o get() para confirmar previamente.

Excluindo Dados

Para excluir dados correspondentes a uma chave primária especificada, use o método delete().

 1function deleteUser(db, id) {
 2    const transaction = db.transaction('users', 'readwrite');
 3    const store = transaction.objectStore('users');
 4    const request = store.delete(id);
 5
 6    request.onsuccess = () => {
 7        console.log(`User with id=${id} deleted successfully`);
 8    };
 9
10    request.onerror = () => {
11        console.error(`Failed to delete user with id=${id}:`, request.error);
12    };
13}
14
15// Example: Delete a user by id
16request.onsuccess = function (event) {
17    const db = event.target.result;
18    deleteUser(db, 4);
19};
  • Use store.delete(id) para excluir dados com a chave primária correspondente.
  • Observe que mesmo que os dados não existam, nenhum erro ocorrerá e será considerado um sucesso.
  • Implementar o tratamento de erros resultará em um código mais robusto.

Recuperando Todos os Dados

getAll()

Para recuperar todos os registros no object store, use o método getAll().

 1function getAllUsers(db) {
 2    const transaction = db.transaction('users', 'readonly');
 3    const store = transaction.objectStore('users');
 4    const request = store.getAll();
 5
 6    request.onsuccess = () => {
 7        console.log('All users:', request.result);
 8    };
 9
10    request.onerror = () => {
11        console.error('Failed to retrieve users:', request.error);
12    };
13}
14
15// Example: Get all users
16request.onsuccess = function (event) {
17    const db = event.target.result;
18    getAllUsers(db);
19};
  • getAll() recupera todos os registros no object store especificado como um array.
  • Mesmo ao recuperar grandes quantidades de dados de uma só vez, é processado de forma eficiente.
  • Os resultados são armazenados como um array em request.result.
  • Sempre adicione tratamento de erros para poder lidar com falhas.

openCursor()

openCursor() é um método para percorrer sequencialmente os registros em um object store ou índice. É útil quando você quer processar um item de cada vez, ao invés de recuperar grandes quantidades de uma só vez.

 1function getAllUsersWithCursor(db) {
 2    const transaction = db.transaction('users', 'readonly');
 3    const store = transaction.objectStore('users');
 4    const request = store.openCursor();
 5
 6    request.onsuccess = () => {
 7        const cursor = request.result;
 8        if (cursor) {
 9            console.log('User:', cursor.value); // Process the current record
10            cursor.continue(); // Move to the next record
11        } else {
12            console.log('All users have been processed.');
13        }
14    };
15
16    request.onerror = () => {
17        console.error('Failed to open cursor:', request.error);
18    };
19}
20
21// Example: Get all users
22request.onsuccess = function (event) {
23    const db = event.target.result;
24    getAllUsersWithCursor(db);
25};
  • Ao usar openCursor(), você inicia um cursor e recupera todos os registros no object store um por um.
  • Use cursor.value para obter o objeto de dados do registro atual.
  • Mova o cursor para o próximo registro com cursor.continue().
  • Quando cursor === null, todos os registros foram percorridos.

Exemplo de um processo de atualização usando openCursor()

Por exemplo, o processo de alterar o nome de um usuário cujo name é Alice para Alicia seria assim:.

 1function updateUserName(db, oldName, newName) {
 2    const transaction = db.transaction('users', 'readwrite');
 3    const store = transaction.objectStore('users');
 4    const index = store.index('name'); // Use the 'name' index
 5    const request = index.openCursor(IDBKeyRange.only(oldName));
 6
 7    request.onsuccess = () => {
 8        const cursor = request.result;
 9        if (cursor) {
10            const user = cursor.value;
11            user.name = newName; // Update the name
12            const updateRequest = cursor.update(user);
13
14            updateRequest.onsuccess = () => {
15                console.log('Updated user:', user);
16            };
17
18            updateRequest.onerror = () => {
19                console.error('Failed to update user:', updateRequest.error);
20            };
21
22            cursor.continue();
23        } else {
24            console.log('All matching users have been updated.');
25        }
26    };
27
28    request.onerror = () => {
29        console.error('Cursor error:', request.error);
30    };
31}
32
33// Example: Update user name
34request.onsuccess = function (event) {
35    const db = event.target.result;
36    updateUserName(db, 'Alice', 'Alicia');
37};
  • IDBKeyRange.only(oldName) Usando IDBKeyRange.only, você pode selecionar apenas os registros cuja chave corresponde exatamente a oldName. Isso é útil quando você quer acessar um valor específico diretamente.

  • cursor.update() Após atualizar cursor.value, chamar update() irá sobrescrever o registro correspondente.

  • Tratando Múltiplas Correspondências Chamando cursor.continue(), você pode mover o cursor para o próximo registro correspondente. Isso permite processar vários registros que correspondem à mesma chave ou condição em sequência.

  • Tratamento de Erros Ao exibir logs em onerror quando um processo falha, torna-se mais fácil investigar as causas e solucionar problemas durante a operação.

Exemplo de um processo de exclusão usando openCursor()

Por exemplo, o processo de excluir todos os usuários cujo name é Bob seria assim:.

 1function deleteUsersByName(db, targetName) {
 2    const transaction = db.transaction('users', 'readwrite');
 3    const store = transaction.objectStore('users');
 4    const index = store.index('name');
 5    const request = index.openCursor(IDBKeyRange.only(targetName));
 6
 7    request.onsuccess = () => {
 8        const cursor = request.result;
 9        if (cursor) {
10            const deleteRequest = cursor.delete();
11
12            deleteRequest.onsuccess = () => {
13                console.log('Deleted user:', cursor.value);
14            };
15
16            deleteRequest.onerror = () => {
17                console.error('Failed to delete user:', deleteRequest.error);
18            };
19
20            cursor.continue();
21        } else {
22            console.log('All matching users have been deleted.');
23        }
24    };
25
26    request.onerror = () => {
27        console.error('Cursor error:', request.error);
28    };
29}
30
31// Example: Delete user by name
32request.onsuccess = function (event) {
33    const db = event.target.result;
34    deleteUsersByName(db, 'Bob');
35};
  • cursor.delete() Com cursor.delete(), o registro na posição atual do cursor é excluído. Como o resultado é retornado de forma assíncrona, você pode verificar o processo em onsuccess.

Notas sobre Transações e Processamento Assíncrono

O IndexedDB é assíncrono e orientado a eventos. Todas as operações devem ser tratadas com os eventos onsuccess ou onerror. Quando você quer agrupar vários processos, é conveniente envolvê-los em uma Promise.

 1function openDatabase() {
 2    return new Promise((resolve, reject) => {
 3        const request = indexedDB.open('MyDatabase', 1);
 4
 5        request.onupgradeneeded = function (event) {
 6            const db = event.target.result;
 7
 8            // Initialization process (creating object stores and indexes, etc.)
 9            const store = db.createObjectStore('users', { keyPath: 'id' });
10            store.createIndex('name', 'name', { unique: false });
11        };
12
13        request.onsuccess = function (event) {
14            const db = event.target.result;
15            resolve(db);
16        };
17
18        request.onerror = function () {
19            reject(request.error);
20        };
21    });
22}
23
24function addUserAsync(db, user) {
25    return new Promise((resolve, reject) => {
26        const transaction = db.transaction('users', 'readwrite');
27        const store = transaction.objectStore('users');
28        const request = store.add(user);
29
30        request.onsuccess = () => resolve();
31        request.onerror = () => reject(request.error);
32    });
33}
34
35async function main() {
36    try {
37        const db = await openDatabase();
38        await addUserAsync(db, { id: 1, name: 'Alice' });
39        console.log('User added successfully');
40    } catch (error) {
41        console.error('Error:', error);
42    }
43}
  • Envolver funções como openDatabase ou addUserAsync com uma Promise torna possível lidar com processos assíncronos de forma intuitiva usando async/await.
  • Isso evita o 'callback hell' e torna o código mais legível.

Resumo

O IndexedDB é um recurso muito poderoso quando você deseja realizar gerenciamento de dados avançado no lado do navegador. No início, você pode se confundir com o processamento assíncrono baseado em eventos, mas, ao entender a estrutura, você poderá realizar operações de dados completas no lado do cliente.

Em particular, ter os seguintes pontos em mente ajudará você a usá-lo de forma mais eficiente:.

  • Realize a configuração inicial com onupgradeneeded.
  • Preste atenção ao modo de leitura/escrita das transações.
  • Os índices permitem buscas eficientes.
  • Os dados podem ser armazenados como objetos, tornando-o altamente compatível com JSON.

Ao dominar o IndexedDB, o gerenciamento de dados para PWAs e aplicativos offline se torna muito mais fácil.

Você pode acompanhar o artigo acima usando o Visual Studio Code em nosso canal do YouTube. Por favor, confira também o canal do YouTube.

YouTube Video