IndexedDB: tutorial para utilizar el sistema de bases de datos en el navegador

La velocidad juega un papel muy importante 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 almacenado 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 utilizarse para cualquier sitio web. ¿Cómo funciona?

¿Para qué sirve IndexedDB?

Es recomendable que la información de los programas cliente no se almacene solo en los servidores, sino que también algunos datos seleccionados 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 navegación. Además, este procedimiento permite utilizar aplicaciones 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 insuficiente para las aplicaciones 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 almacenamiento web (a menudo también llamado almacenamiento DOM). El concepto de esta tecnologí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 supercookies, se estructuran de manera muy simple y no presentan las características de las bases de datos modernas. Como vemos, las cookies y las supercookies no son la mejor opción debido a su tamaño reducido, pero eso no es todo: ninguno de estos formatos permite estructurar ni indexar los datos y, por tanto, excluyen la posibilidad de realizar búsquedas.

No fue hasta el desarrollo de Web SQL que apareció un método totalmente distinto y prometedor: un sistema de bases de datos del lado del cliente basado en SQL. Sin embargo, el World Wide Web Consortium (W3C), organización para el desarrollo de estándares web, decidió abandonar este proyecto y sustituirlo 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 compatible con la mayoría de navegadores modernos.

Navegadores compatibles con IndexedDB

Chrome Firefox Opera Opera mini Safari IE Edge

¿Cómo funciona IndexedDB?

En primer lugar, este estándar es una interfaz configurada en el navegador, donde se pueden ir almacenando directamente los datos de las páginas web mediante JavaScript. Es posible establecer una base de datos para cada sitio web, que solo tendrá permiso para acceder a su correspondiente IndexedDB (abreviación de Indexed Database API). De esta manera, se preserva la confidencialidad de los datos. Las bases de datos se estructuran 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 relacional, 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 constituye un objeto y la clave, la característica del mismo. A esto se le añaden los índices, que permiten buscar rápidamente los datos.

En IndexedDB, las operaciones siempre se realizan en forma de transacciones: cada procedimiento de escritura, lectura o modificación se integra en una transacción, lo que garantiza que los cambios en la base de datos se realicen completamente o no se realicen en absoluto. Una ventaja de IndexedDB es que no requiere una transmisión síncrona de los datos (en la mayoría de los casos), sino que las operaciones son asíncronas, lo que garantiza que el navegador web no se bloquee durante el envío de datos y que el usuario pueda continuar utilizándolo.

La seguridad ocupa el primer plano en el caso de IndexedDB, porque es fundamental garantizar que las páginas web no tengan acceso a las bases de datos de otras webs. Con ese fin, IndexedDB ha establecido una política del mismo origen: para que los datos estén disponibles, tanto el dominio como el protocolo de nivel de aplicación y el puerto deben ser idénticos. De hecho, es perfectamente posible que las subcarpetas de un dominio accedan al IndexedDB de otra subcarpeta, 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 tecnologí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 recomendable llevar a cabo una verificación. Aunque actualmente casi todos los navegadores web modernos soportan IndexedDB, la API no funcionará si no están actualizados. Por ello, debemos comprobar en primer lugar si el nuestro es compatible. Para hacerlo, verificaremos el objeto window.

Nota

Mediante la consola de las herramientas de desarrollador 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 compatible 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.

Comencemos 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 comprobado que es mejor crerar una IndexedDB por dominio. El sistema nos da la posibilidad 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 introducir dos argumentos: 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, comenzamos con la versión 1. El objeto resultante puede provocar uno de estos tres eventos:

  • error: se ha producido un error al crear la base de datos.
  • upgradeneeded: se ha modificado 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 inexistente 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 numeración sencilla que se irá incrementando de manera continuada mediante “autoIncrement”. Para comenzar a introducir informació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 electrónico utilizada.

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

De esta manera, se pueden buscar los registros de datos fácilmente introduciendo el nombre del usuario o su dirección de correo electrónico. Ambos índices se diferencian en que el nombre de usuario puede asignarse más de una vez, mientras que para cada dirección de correo electrónico solo puede existir un único registro.

Ya podemos comenzar a anotar registros en la base de datos. Estas operaciones se realizan por medio de transacciones, de las cuales existen tres tipos:

  • readonly: lee los datos de un object store. Se pueden solapar sin problemas varias transacciones de este tipo, incluso si se refieren al mismo ámbito.
  • readwrite: lee y escribe un registro. Se podrán desarrollar varias de estas transacciones al mismo tiempo solo en caso de que se refieran a ámbitos distintos.
  • versionchange: realiza modificaciones en un object store o en un índice, creando y modificando también registros. Esta función no puede configurarse de manera manual, sino que se desencadena automáticamente con el evento upgradeneeded.

Para crear un nuevo registro, recurrimos 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 introducen datos en el object store y se obtienen en la consola los avisos en función del éxito de la transacción. Pero igual que has guardado los datos en una IndexedDB, también deberías poder leerlos. Para ello, utilizamos 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 transacción no puede efectuarse, aparece un mensaje de error. En cambio, si la transacción tiene éxito, aparecerá el contenido de los registros del nombre de usuario (“nickname”) y el correo electrónico. 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 seleccionando 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 informació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);
};

Finalmente, si deseas eliminar un registro de la base de datos, el modo de hacerlo es muy similar al que acabamos de explicar: mediante una transacció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 información, puedes consultar, por ejemplo, el tutorial de Mozilla o el de Google. Ten en cuenta que Google utiliza una biblioteca especial en su código de muestra que presenta algunas diferencias respecto al de Mozilla.

Utilizamos cookies propias y de terceros para mejorar nuestros servicios y mostrarle publicidad relacionada con sus preferencias mediante el análisis de sus hábitos de navegación. Si continua navegando, consideramos que acepta su uso. Puede obtener más información, o bien conocer cómo cambiar la configuración de su navegador en nuestra. Política de Cookies.