En este post te mostraré cómo usar SQLite3 directamente en el navegador web usando la librería original escrita en C a través de WebAssembly y el OPFS para alojar la base de datos. De este modo tendremos SQLite3 original en la web para trabajar en el navegador con JS del lado del cliente.
Podremos hacer esto con JavaScript puro o con cualquier framework; pero te mostraré un ejemplo básico y bien explicado de SQLite3 con JavaScript. Luego te enseñaré un ejemplo con Tailwind, Svelte y Progressive Web apps.
El uso de esta librería no es nuevo; lo que es nuevo es el OPFS que en palabras simples permite un sistema de archivos en el navegador web, y justamente era eso lo que faltaba para poder usar SQLite3 en el navegador.
Para que esto funcione, siempre vamos a necesitar agregar estos encabezados al servir el WASM de la librería:
Y para interactuar con la base de datos con OPFS es obligatorio usar un Web Worker. Verás que no es complejo, pero no olvides estas 2 cosas importantes.
Si quieres ir al código completo mira el repositorio de GitHub: https://github.com/parzibyte/hola-sqlite3
O la demostración en línea: https://stackblitz.com/edit/vitejs-vite-jbwamt?file=main.js
Recomiendo leer el post completo para solucionar todas tus posibles dudas, y al final del mismo, si quieres explorar más, puedes ver el siguiente vídeo:
Podemos instalar la librería con:
npm install @sqlite.org/sqlite-wasm
Aunque vamos a usar JS puro y del lado del cliente, necesitaremos NPM para gestionar los paquetes. Si tú ya usas NPM entonces entenderás de lo que hablo, simplemente instala esa dependencia.
En caso de que tengas tu proyecto limpio y no quieras ensuciarlo tanto, yo recomiendo Vite, pues no nos obliga a usar ningún framework, es extremadamente ligero, permite usar JS puro y es rápido. Yo he creado mi proyecto con:
npm create vite@latest
Después de eso instalé la librería de sqlite-wasm. Por cierto, si usas vite necesitas agregar o modificar el vite.config.js para que quede así:
import { defineConfig } from 'vite';
export default defineConfig({
base: "./",
server: {
headers: {
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
},
},
optimizeDeps: {
exclude: ['@sqlite.org/sqlite-wasm'],
},
});
Ahora veamos el worker principal donde vamos a iniciar la base de datos y a hacer las operaciones. Para este caso voy a insertar una persona con su nombre y fecha de nacimiento, así como obtener todas las personas. Todo usando consultas SQL.
El worker se divide en las funciones principales que interactúan con la BD:
const iniciar = async () => {
const sqlite3 = await sqlite3InitModule({
print: console.log,
printErr: console.error,
});
if ('opfs' in sqlite3) {
db = new sqlite3.oo1.OpfsDb(NOMBRE_BASE_DE_DATOS);
console.log('OPFS is available, created persisted database at', db.filename);
} else {
db = new sqlite3.oo1.DB(NOMBRE_BASE_DE_DATOS, 'ct');
console.log('OPFS is not available, created transient database', db.filename);
}
await db.exec(`CREATE TABLE IF NOT EXISTS personas(
id INTEGER PRIMARY KEY AUTOINCREMENT,
nombre TEXT NOT NULL,
fechaNacimiento TEXT NOT NULL)`);
}
const insertarPersona = async (nombre, fechaNacimiento) => {
const filas = await db.exec({
sql: "INSERT INTO personas(nombre, fechaNacimiento) VALUES (?, ?) RETURNING *",
bind: [nombre, fechaNacimiento],
returnValue: "resultRows",
rowMode: "object",
});
return filas[0];
}
const obtenerPersonas = async () => {
return await db.exec({
sql: "SELECT id, nombre, fechaNacimiento FROM personas",
returnValue: "resultRows",
rowMode: "object",
});
}
Y en la comunicación con el hilo principal, pues recordemos que el worker no puede modificar el DOM directamente:
self.onmessage = async (evento) => {
const accion = evento.data[0];
const argumentos = evento.data[1];
switch (accion) {
case "iniciar":
await iniciar();
self.postMessage(["iniciado"]);
break;
case "insertar_persona":
const personaRecienInsertada = await insertarPersona(argumentos.nombre, argumentos.fechaNacimiento);
self.postMessage(["persona_insertada", personaRecienInsertada]);
break;
case "obtener_personas":
const personas = await obtenerPersonas();
self.postMessage(["personas_obtenidas", personas]);
break;
}
}
Nota: puedes ver el código completo de db.js en el repositorio de GitHub. Es importante mencionar que al invocar a sqlite3InitModule
se va a descargar el WASM.
Ya tenemos nuestro worker listo para ser invocado desde cualquier lugar. Veamos nuestra interfaz:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<button id="btnIniciarBaseDeDatos">Iniciar base de datos</button>
<br>
<br>
<input disabled value="Parzibyte" id="nombre" type="text" placeholder="Nombre">
<input disabled value="2050-01-01" id="fechaNacimiento" type="date">
<button disabled id="btnInsertar">Insertar persona</button>
<br>
<br>
<button disabled id="btnObtener">Obtener lista de personas</button>
<div id="contenedorPersonas"></div>
<script type="module" src="/main.js"></script>
</body>
</html>
Solo tenemos botones y algunos campos de texto. Lo importante es el archivo main.js
pues ahí es donde juntaremos ambas cosas: el DOM y SQLite3 a través del worker.
El código queda como se ve a continuación, simplemente escuchamos el clic de los botones, invocamos a una función del worker y también escuchamos cuando el worker traiga los resultados:
const worker = new Worker(new URL("./db.js", import.meta.url), { type: "module" });
const $iniciarBaseDeDatos = document.querySelector("#btnIniciarBaseDeDatos"),
$insertar = document.querySelector("#btnInsertar"),
$obtener = document.querySelector("#btnObtener"),
$nombre = document.querySelector("#nombre"),
$fechaNacimiento = document.querySelector("#fechaNacimiento"),
$contenedorPersonas = document.querySelector("#contenedorPersonas");
$insertar.addEventListener("click", () => {
worker.postMessage(["insertar_persona", { nombre: $nombre.value, fechaNacimiento: $fechaNacimiento.value }]);
});
$obtener.addEventListener("click", () => {
worker.postMessage(["obtener_personas"]);
});
$iniciarBaseDeDatos.onclick = () => {
worker.postMessage(["iniciar"]);
}
worker.onmessage = evento => {
const accion = evento.data[0];
const argumentos = evento.data[1];
switch (accion) {
case "iniciado":
[$nombre, $fechaNacimiento, $insertar, $obtener].forEach(elemento => elemento.disabled = false);
break;
case "persona_insertada":
console.log({ argumentos });
break;
case "personas_obtenidas":
const personas = argumentos;
$contenedorPersonas.innerHTML = "";
for (const persona of personas) {
$contenedorPersonas.innerHTML += `<strong>${persona.nombre}</strong> ${persona.fechaNacimiento}<br>`;
}
break;
}
}
Básicamente ese es todo el código. Hay mucho código que se usa para comunicar al worker con el DOM, pero podríamos simplificarlo con alternativas como Comlink.
Como te dije al inicio, este es un ejemplo básico de SQLite3 con OPFS directamente en el navegador web con JavaScript puro del lado del cliente. Si quieres puedes ver la explicación en vídeo.
Mis primeras impresiones ya las publiqué en mi post anterior sobre cómo usar SQLite3 con Svelte, y también he desarrollado una app de notas que todavía no he publicado (aunque he documentado el proceso de creación).
No creí que llegaría el día en el que los Workers, las PWA y SQLite3 estuvieran en un mismo lugar, pero la magia de JS lo logró.
Ya te enseñé cómo convertir una aplicación web de Vue 3 en una PWA. Al…
En este artículo voy a documentar la arquitectura que yo utilizo al trabajar con WebAssembly…
En un artículo anterior te enseñé a crear un PWA. Al final, cualquier aplicación que…
Al usar Comlink para trabajar con los workers usando JavaScript me han aparecido algunos errores…
En este artículo te voy a enseñar cómo usar un "top level await" esperando a…
Ayer estaba editando unos archivos que son servidos con el servidor Apache y al visitarlos…
Esta web usa cookies.
Ver comentarios
Excelente Información, muchas gracias