JavaScript e IndexedDB

JavaScript e IndexedDB

En este artículo, explicaremos JavaScript e IndexedDB.

Este tutorial proporciona una explicación paso a paso de JavaScript e IndexedDB, incluyendo código de muestra práctico en cada paso para ayudar a profundizar tu comprensión.

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 es una base de datos asíncrona de almacenamiento de clave-valor incorporada en los navegadores. Ofrece características similares a las bases de datos relacionales y te permite almacenar y buscar grandes cantidades de datos estructurados en el lado del cliente. Es especialmente útil para aplicaciones que funcionan sin conexión y PWAs (Aplicaciones Web Progresivas).

Características de IndexedDB

  • Funciona de manera asíncrona y orientada a eventos.
  • Los objetos JavaScript pueden almacenarse en object stores (almacenes de objetos).
  • Es posible realizar búsquedas mediante consultas o índices.
  • Tiene una gran capacidad de almacenamiento (cientos de MB o más), lo que te permite almacenar mucho más datos que cookies o localStorage.

Abrir y crear una base de datos

Para usar IndexedDB, primero debes abrir una base de datos. Si no existe, se crea automáticamente.

1const request = indexedDB.open('MyDatabase', 1); // Specify DB name and version
  • El método open abre la base de datos de forma asíncrona y dispara los siguientes tres 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};
  • El evento onsuccess se activa cuando la base de datos se abre correctamente. Las operaciones posteriores deben realizarse utilizando request.result, que está disponible en este momento.

onerror

1// Fired when database fails to open
2request.onerror = (event) => {
3    console.error('Failed to open database:', event.target.error);
4};
  • El evento onerror se activa cuando la base de datos no puede abrirse. Aquí se debe realizar el registro y manejo de errores.

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};
  • onupgradeneeded se dispara cuando la base de datos es creada por primera vez o cuando la versión especificada es superior a la actual. La creación de tablas (almacenes de objetos) y la definición del esquema debe hacerse en este momento.

Creando un Object Store

Primero, crea un 'object store' (equivalente a una tabla) dentro de 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};

Aquí se aplican las siguientes configuraciones:.

  • createObjectStore createObjectStore es un método para crear un nuevo object store en la base de datos. Puedes almacenar registros en un object store y definir cómo se gestiona la información especificando keyPath y otras opciones. Este proceso debe hacerse dentro del evento onupgradeneeded.

  • keyPath: 'id' Configura keyPath en la propiedad id, que identifica de forma única cada registro como clave primaria. Esto permite usar automáticamente id al agregar, buscar o actualizar datos.

  • createIndex Utiliza el método createIndex para crear un índice basado en la propiedad name para realizar búsquedas. Al establecer unique: false, se permiten múltiples registros con el mismo name.

Manejo de conexiones exitosas a la base de datos

Asigna los procesos que deben ejecutarse tras una conexión exitosa de la base de datos al evento onsuccess. Dentro de este proceso, obtienes la instancia de la base de datos y te preparas para leer y escribir datos.

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};
  • Este proceso se ejecuta cuando la conexión a la base de datos de IndexedDB se completa exitosamente.

  • event.target.result contiene la instancia abierta de la base de datos (objeto IDBDatabase), que se usa para iniciar transacciones y acceder a almacenes de objetos.

  • Las operaciones reales de lectura y escritura como agregar, recuperar, actualizar y eliminar datos se realizan utilizando el objeto db.

En este punto, la base de datos está lista, así que puedes iniciar transacciones de forma segura.

Agregar datos

Así es como se agrega nueva información a 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};
  • Crea una transacción con db.transaction() y especifica el almacén de objetos sobre el que operar y el modo (en este caso, readwrite).
  • Agrega nueva información usando el método store.add().
  • Las transacciones se confirman automáticamente, pero si deseas agrupar varias operaciones, puedes gestionarlas utilizando eventos de finalización de transacciones.

Obtener datos (búsqueda por clave primaria)

Aquí se muestra cómo recuperar datos específicos usando la clave primaria.

 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};
  • Crea una transacción de solo lectura con db.transaction().
  • Usa store.get(id) para recuperar los datos correspondientes a la clave primaria especificada.
  • onsuccess se llama cuando la recuperación es exitosa y el resultado se muestra si está disponible. Si no hay resultado, se considera como 'no hay datos correspondientes.'.

Usando búsqueda por índice

Si deseas buscar por propiedades distintas a la clave primaria, utiliza el índice que creaste previamente.

 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};
  • Accede al índice name precreado usando store.index('name').
  • index.get(value) obtiene el primer registro con el valor correspondiente. Si hay varios registros con el mismo valor, puedes obtenerlos todos con index.getAll(value).

Actualizar datos

Para actualizar datos existentes (sobrescribir), usa el método put().

Si existe un registro con la clave primaria correspondiente, se actualiza; si no, se añade un nuevo registro.

 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() es un método conveniente que permite tanto actualizar como agregar registros.
  • Si ya existe un dato con la misma clave primaria, será sobrescrito.
  • Si quieres comprobar la existencia antes de actualizar, puedes usar get() para confirmarlo previamente.

Eliminar datos

Para eliminar datos correspondientes a una clave primaria especificada, usa el 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};
  • Utiliza store.delete(id) para eliminar datos con la clave primaria correspondiente.
  • Ten en cuenta que, aunque los datos no existan, no se producirá ningún error y se considerará una operación exitosa.
  • Implementar el manejo de errores dará como resultado un código más robusto.

Obtener todos los datos

getAll()

Para obtener todos los registros en el almacén de objetos, usa el 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 los registros del almacén de objetos especificado como un arreglo.
  • Incluso al recuperar grandes cantidades de datos de una vez, se procesa de manera eficiente.
  • Los resultados se almacenan como un arreglo en request.result.
  • Agrega siempre manejo de errores para poder gestionar posibles fallos.

openCursor()

openCursor() es un método para recorrer secuencialmente los registros en un almacén de objetos o índice. Es útil cuando deseas procesar los datos uno por uno en vez de recuperar grandes cantidades de una sola 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};
  • Al usar openCursor(), inicias un cursor y recuperas todos los registros del almacén de objetos uno por uno.
  • Utiliza cursor.value para obtener el objeto de datos del registro actual.
  • Mueve el cursor al siguiente registro con cursor.continue().
  • Cuando cursor === null, se han recorrido todos los registros.

Ejemplo de un proceso de actualización utilizando openCursor()

Por ejemplo, el proceso para cambiar el nombre de un usuario cuyo name es Alice a Alicia sería así:.

 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) Al usar IDBKeyRange.only, puedes apuntar únicamente a los registros cuya clave coincida exactamente con oldName. Esto es útil cuando deseas acceder directamente a un valor específico.

  • cursor.update() Después de actualizar cursor.value, al llamar a update() se sobrescribirá el registro correspondiente.

  • Manejo de múltiples coincidencias Al llamar cursor.continue(), puedes mover el cursor al siguiente registro coincidente. Esto te permite procesar varios registros que coincidan con la misma clave o condición en secuencia.

  • Gestión de errores Al mostrar registros en onerror cuando un proceso falla, se facilita la investigación de las causas y la solución de problemas durante la operación.

Ejemplo de un proceso de eliminación utilizando openCursor()

Por ejemplo, el proceso para eliminar todos los usuarios cuyo name es Bob sería así:.

 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() Con cursor.delete(), se elimina el registro en la posición actual del cursor. Como el resultado se devuelve de forma asíncrona, puedes comprobar el proceso en onsuccess.

Notas sobre transacciones y procesamiento asíncrono

IndexedDB es asíncrono y orientado a eventos. Todas las operaciones deben gestionarse mediante los eventos onsuccess o onerror. Cuando deseas agrupar varios procesos, es conveniente envolverlos en una 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 funciones como openDatabase o addUserAsync con Promise permite manejar procesos asíncronos de manera intuitiva con async/await.
  • Esto evita el infierno de callbacks y hace que el código sea más legible.

Resumen

IndexedDB es una característica muy poderosa cuando se desea realizar una gestión avanzada de datos en el lado del navegador. Al principio, el procesamiento asíncrono basado en eventos puede resultar confuso, pero una vez comprendas la estructura, podrás realizar operaciones de datos a gran escala en el lado del cliente.

En particular, tener en cuenta los siguientes puntos te ayudará a utilizarlo de manera más fluida:.

  • Realiza la configuración inicial con onupgradeneeded.
  • Presta atención al modo de lectura/escritura de las transacciones.
  • Los índices permiten búsquedas eficientes.
  • Los datos pueden almacenarse como objetos, lo que proporciona alta compatibilidad con JSON.

Al dominar IndexedDB, la gestión de datos para PWAs y aplicaciones offline se vuelve mucho más sencilla.

Puedes seguir el artículo anterior utilizando Visual Studio Code en nuestro canal de YouTube. Por favor, también revisa nuestro canal de YouTube.

YouTube Video