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 & 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 utilizandorequest.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 especificandokeyPath
y otras opciones. Este proceso debe hacerse dentro del eventoonupgradeneeded
. -
keyPath: 'id'
ConfigurakeyPath
en la propiedadid
, que identifica de forma única cada registro como clave primaria. Esto permite usar automáticamenteid
al agregar, buscar o actualizar datos. -
createIndex
Utiliza el métodocreateIndex
para crear un índice basado en la propiedadname
para realizar búsquedas. Al establecerunique: false
, se permiten múltiples registros con el mismoname
.
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 (objetoIDBDatabase
), 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 usandostore.index('name')
. index.get(value)
obtiene el primer registro con el valor correspondiente. Si hay varios registros con el mismo valor, puedes obtenerlos todos conindex.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 usarIDBKeyRange.only
, puedes apuntar únicamente a los registros cuya clave coincida exactamente conoldName
. Esto es útil cuando deseas acceder directamente a un valor específico. -
cursor.update()
Después de actualizarcursor.value
, al llamar aupdate()
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()
Concursor.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 enonsuccess
.
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
oaddUserAsync
conPromise
permite manejar procesos asíncronos de manera intuitiva conasync/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.