La velocidad juega un papel muy im­po­r­ta­n­te al navegar por internet porque nadie suele estar dispuesto a esperar una eternidad a que su web se termine de cargar. Para que las páginas se carguen lo más rápido posible, conviene que al menos una parte de los datos se haya al­ma­ce­na­do del lado del usuario y, de este modo, no sea necesario volver a enviarlos. Una opción para hacerlo es IndexedDB, un sistema de bases de datos que se integra en el navegador del usuario y puede uti­li­zar­se para cualquier sitio web. ¿Cómo funciona?

¿Para qué sirve IndexedDB?

Es re­co­me­n­da­ble que la in­fo­r­ma­ción de los programas cliente no se almacene solo en los se­r­vi­do­res, sino que también algunos datos se­le­c­cio­na­dos de los sitios web se guarden del lado del cliente. De esta manera, como los datos no deben cargarse cada vez que se accede a la web, se acelera la velocidad de na­ve­ga­ción. Además, este pro­ce­di­mie­n­to permite utilizar apli­ca­cio­nes web sin conexión a internet y alojar los registros del usuario del lado del cliente. De hecho, para esto último se crearon las cookies, pero estas ofrecen una capacidad muy limitada que resulta in­su­fi­cie­n­te para las apli­ca­cio­nes web modernas. Asimismo, las cookies tienen que enviarse a la red con cada petición HTTP.

La primera solución a este problema fue el al­ma­ce­na­mie­n­to web (a menudo también llamado al­ma­ce­na­mie­n­to DOM). El concepto de esta te­c­no­lo­gía es muy similar al de las cookies y, aunque el tamaño de sus archivos es mucho mayor, de 10 MB en lugar de unos pocos kilobytes, sigue sin ser muy extenso. Estos archivos, también llamados su­pe­r­coo­kies, se es­tru­c­tu­ran de manera muy simple y no presentan las ca­ra­c­te­rí­s­ti­cas de las bases de datos modernas. Como vemos, las cookies y las su­pe­r­coo­kies no son la mejor opción debido a su tamaño reducido, pero eso no es todo: ninguno de estos formatos permite es­tru­c­tu­rar ni indexar los datos y, por tanto, excluyen la po­si­bi­li­dad de realizar búsquedas.

No fue hasta el de­sa­rro­llo de Web SQL que apareció un método to­ta­l­me­n­te distinto y pro­me­te­dor: un sistema de bases de datos del lado del cliente basado en SQL. Sin embargo, el World Wide Web Co­n­so­r­tium (W3C), or­ga­ni­za­ción para el de­sa­rro­llo de es­tá­n­da­res web, decidió abandonar este proyecto y su­s­ti­tui­r­lo por el de IndexedDB. De este modo, y bajo la dirección de Mozilla, se acabó creando un estándar que, a día de hoy, es co­m­pa­ti­ble con la mayoría de na­ve­ga­do­res modernos.

Na­ve­ga­do­res co­m­pa­ti­bles con IndexedDB

Chrome Firefox Opera Opera mini Safari IE Edge

¿Cómo funciona IndexedDB?

En primer lugar, este estándar es una interfaz co­n­fi­gu­ra­da en el navegador, donde se pueden ir al­ma­ce­na­n­do di­re­c­ta­me­n­te los datos de las páginas web mediante Ja­va­S­cri­pt. Es posible es­ta­ble­cer una base de datos para cada sitio web, que solo tendrá permiso para acceder a su co­rre­s­po­n­die­n­te IndexedDB (abre­via­ción de Indexed Database API). De esta manera, se preserva la co­n­fi­de­n­cia­li­dad de los datos. Las bases de datos se es­tru­c­tu­ran en base a almacenes de objetos que permiten guardar diversos formatos: cadenas (strings), cifras, objetos, fechas y matrices (arrays).

IndexedDB no es una base de datos re­la­cio­nal, sino un sistema tabular basado en índices. De hecho, se trata de una base de datos NoSQL, como también lo es por ejemplo MongoDB. Los registros siempre se definen en pares de clave y valor, en los que el valor co­n­s­ti­tu­ye un objeto y la clave, la ca­ra­c­te­rí­s­ti­ca del mismo. A esto se le añaden los índices, que permiten buscar rá­pi­da­me­n­te los datos.

En IndexedDB, las ope­ra­cio­nes siempre se realizan en forma de tra­n­sac­cio­nes: cada pro­ce­di­mie­n­to de escritura, lectura o mo­di­fi­ca­ción se integra en una tra­n­sac­ción, lo que garantiza que los cambios en la base de datos se realicen co­m­ple­ta­me­n­te o no se realicen en absoluto. Una ventaja de IndexedDB es que no requiere una tra­n­s­mi­sión síncrona de los datos (en la mayoría de los casos), sino que las ope­ra­cio­nes son así­n­cro­nas, lo que garantiza que el navegador web no se bloquee durante el envío de datos y que el usuario pueda continuar uti­li­zá­n­do­lo.

La seguridad ocupa el primer plano en el caso de IndexedDB, porque es fu­n­da­me­n­tal ga­ra­n­ti­zar que las páginas web no tengan acceso a las bases de datos de otras webs. Con ese fin, IndexedDB ha es­ta­ble­ci­do una política del mismo origen: para que los datos estén di­s­po­ni­bles, tanto el dominio como el protocolo de nivel de apli­ca­ción y el puerto deben ser idénticos. De hecho, es pe­r­fe­c­ta­me­n­te posible que las su­b­ca­r­pe­tas de un dominio accedan al IndexedDB de otra su­b­ca­r­pe­ta, ya que ambas tienen el mismo origen. En cualquier caso, no se permitirá el acceso si se utiliza otro puerto o si el protocolo cambia de HTTP a HTTPS o viceversa.

Tutorial de IndexedDB: la te­c­no­lo­gía en práctica

Vamos a explicar IndexedDB con un ejemplo. No olvides que, antes de poder crear bases de datos y object stores (almacenes de objetos), es re­co­me­n­da­ble llevar a cabo una ve­ri­fi­ca­ción. Aunque ac­tua­l­me­n­te casi todos los na­ve­ga­do­res web modernos soportan IndexedDB, la API no fu­n­cio­na­rá si no están ac­tua­li­za­dos. Por ello, debemos comprobar en primer lugar si el nuestro es co­m­pa­ti­ble. Para hacerlo, ve­ri­fi­ca­re­mos el objeto window.

Nota

Mediante la consola de las he­rra­mie­n­tas de de­sa­rro­lla­dor que hay integrado en el navegador, puedes seguir estos ejemplos de código y ver los IndexedDB de otras páginas.

if (!window.IndexedDB) {
	alert("¡IndexedDB no es compatible!");
}

En caso de que el navegador del usuario no sea co­m­pa­ti­ble con IndexedDB, nos avisará una ventana de diálogo. Si lo prefieres, puedes generar un mensaje de error en el archivo de registro con el comando console.error.

Co­me­n­ce­mos abriendo una base de datos. En teoría, una página web puede abrir varias bases de datos, pero en la práctica se ha co­m­pro­ba­do que es mejor crerar una IndexedDB por dominio. El sistema nos da la po­si­bi­li­dad de trabajar con varios almacenes de objetos. Abriremos una base de datos a través de una petición –una solicitud asíncrona.

var request = window.IndexedDB.open("Mibasededatos", 1);

Al abrirla, deberás in­tro­du­cir dos ar­gu­me­n­tos: primero, el nombre que elijas (como string) y, después, el número de versión (como integer, es decir, como número entero). Como es lógico, co­me­n­za­mos con la versión 1. El objeto re­su­l­ta­n­te puede provocar uno de estos tres eventos:

  • error: se ha producido un error al crear la base de datos.
  • up­gra­de­nee­ded: se ha mo­di­fi­ca­do la versión de la base de datos. Este mensaje aparece también al crearla porque se cambia el número de versión (de in­e­xi­s­te­n­te a 1).
  • success: la base de datos se ha creado con éxito.

Ahora ya podemos empezar a definir la base de datos y un object store.

request.onupgradeneeded = function(event) {
	var db = event.target.result;
	var objectStore = db.createObjectStore("Usuario", { keyPath: "id", autoIncrement: true });
}

Nuestro object store lleva el nombre de “Usuario”. La clave es “id”, una nu­me­ra­ción sencilla que se irá in­cre­me­n­ta­n­do de manera co­n­ti­nua­da mediante “au­toI­n­cre­me­nt”. Para comenzar a in­tro­du­cir in­fo­r­ma­ción en la base de datos y en el object store, primero deberás definir uno o varios índices. En nuestro ejemplo, vamos a definir un índice para el nombre de usuario y otro para la dirección de correo ele­c­tró­ni­co utilizada.

objectStore.createIndex("Nickname", " Nickname", { unique: false });
objectStore.createIndex("email", "email", { unique: true });

De esta manera, se pueden buscar los registros de datos fá­ci­l­me­n­te in­tro­du­cie­n­do el nombre del usuario o su dirección de correo ele­c­tró­ni­co. Ambos índices se di­fe­re­n­cian en que el nombre de usuario puede asignarse más de una vez, mientras que para cada dirección de correo ele­c­tró­ni­co solo puede existir un único registro.

Ya podemos comenzar a anotar registros en la base de datos. Estas ope­ra­cio­nes se realizan por medio de tra­n­sac­cio­nes, de las cuales existen tres tipos:

  • readonly: lee los datos de un object store. Se pueden solapar sin problemas varias tra­n­sac­cio­nes de este tipo, incluso si se refieren al mismo ámbito.
  • readwrite: lee y escribe un registro. Se podrán de­sa­rro­llar varias de estas tra­n­sac­cio­nes al mismo tiempo solo en caso de que se refieran a ámbitos distintos.
  • ve­r­sio­n­cha­n­ge: realiza mo­di­fi­ca­cio­nes en un object store o en un índice, creando y mo­di­fi­ca­n­do también registros. Esta función no puede co­n­fi­gu­rar­se de manera manual, sino que se des­en­ca­de­na au­to­má­ti­ca­me­n­te con el evento up­gra­de­nee­ded.

Para crear un nuevo registro, re­cu­rri­mos entonces a readwrite.

const dbconnect = window.IndexedDB.open('Mibasededatos', 1);
dbconnect.onupgradeneeded = ev => {
	console.log('Actualizar BD');
	const db = ev.target.result;
	const store = db.createObjectStore('Usuario', { keyPath: 'id', autoIncrement: true });
	store.createIndex('Nickname', 'Nickname', { unique: false });
	store.createIndex('email', 'email', { unique: true });
}
dbconnect.onsuccess = ev => {
	console.log('BD-Actualización exitosa');
	const db = ev.target.result;
	const transaction = db.transaction('Usuario', 'readwrite');
	const store = transaction.objectStore('Usuario');
	const data = [
		{Nickname: 'Rapaz123', email: 'rapaz@example.com'},
		{Nickname: 'Dino2', email: 'dino@example.com'}
	];
	data.forEach(el => store.add(el));
	transaction.onerror = ev => {
		console.error('¡Se ha producido un error!', ev.target.error.message);
	};
	transaction.oncomplete = ev => {
		console.log('¡Los datos se han añadido con éxito!');
		const store = db.transaction('Usuario', 'readonly').objectStore('Usuario');
		//const query = store.get(1); // Einzel-Query
		const query = store.openCursor()
		query.onerror = ev => {
			console.error('¡Solicitud fallida!', ev.target.error.message);
		};
		/*
		// Procesamiento de la consulta individual
		query.onsuccess = ev => {
			if (query.result) {
				console.log(Registro 1', query.result.Nickname, query.result.eMail);
			} else {
				console.warn('¡Ningún registro disponible!');
			}
		};
		*/
		query.onsuccess = ev => {
			const cursor = ev.target.result;
			if (cursor) {
				console.log(cursor.key, cursor.value.Nickname, cursor.value.eMail);
				cursor.continue();
			} else {
				console.log('¡No hay más registros disponibles!');
			}
		};
	};
};

Con este código se in­tro­du­cen datos en el object store y se obtienen en la consola los avisos en función del éxito de la tra­n­sac­ción. Pero igual que has guardado los datos en una IndexedDB, también deberías poder leerlos. Para ello, uti­li­za­mos get.

var transaction = db.transaction(["Usuario"]);
var objectStore = transaction.objectStore("Usuario");
var request = objectStore.get(1);
request.onerror = function(event) {
	console.log("¡Solicitud fallida!");
}
request.onsuccess = function(event) {
	if (request.result) {
		console.log(request.result.Nickname);
		console.log(request.result.eMail);
	} else {
		console.log("¡Ningún registro disponible!");
	}
};

Este código te permite buscar el registro con la clave 1, es decir, con el “id” de valor 1. Si la tra­n­sac­ción no puede efe­c­tuar­se, aparece un mensaje de error. En cambio, si la tra­n­sac­ción tiene éxito, aparecerá el contenido de los registros del nombre de usuario (“nickname”) y el correo ele­c­tró­ni­co. En caso de no encontrar ningún registro bajo ese número, el sistema también mostrará un aviso.

Si no deseas buscar un solo registro, sino varios a la vez, te ayudará el uso de un cursor. Esta función permite obtener un registro detrás de otro, teniendo en cuenta todo el conjunto de la base de datos o se­le­c­cio­na­n­do solo un ámbito de clave concreto.

var objectStore = db.transaction("Usuario").objectStore("Usuario");
objectStore.openCursor().onsuccess = function(event) {
	var cursor = event.target.result;
	if (cursor) {
		console.log(cursor.key);
		console.log(cursor.value.Nickname);
		console.log(cursor.value.eMail);
		cursor.continue();
	} else {
		console.log("¡No hay más registros disponibles!");
	}
};

También es posible recuperar in­fo­r­ma­ción de dos índices con el comando get.

var index = objectStore.index("Alias");
index.get("Raptor123").onsuccess = function(event) {
	console.log(event.target.result.eMail);
};

Fi­na­l­me­n­te, si deseas eliminar un registro de la base de datos, el modo de hacerlo es muy similar al que acabamos de explicar: mediante una tra­n­sac­ción readwrite.

var request = db.transaction(["Usuario"], "readwrite")
	.objectStore("Usuario")
	.delete(1);
request.onsuccess = function(event) {
	console.log("¡Registro borrado con éxito!");
};
En resumen

Este artículo te servirá para dar los primeros pasos con IndexedDB. Si deseas más in­fo­r­ma­ción, puedes consultar, por ejemplo, el tutorial de Mozilla o el de Google. Ten en cuenta que Google utiliza una bi­blio­te­ca especial en su código de muestra que presenta algunas di­fe­re­n­cias respecto al de Mozilla.

Ir al menú principal