javascript

SQLite3 con JavaScript puro y OPFS – Tutorial

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.

Usando SQLite3 en la web

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:

  • Cross-Origin-Opener-Policy: same-origin
  • Cross-Origin-Embedder-Policy: require-corp

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.

Proyecto terminado y código fuente

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:

Instalando librería

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'],
 },
});

El worker

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.

HTML para probar SQLite3

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.

Conclusión y siguientes pasos

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ó.

Estoy aquí para ayudarte 🤝💻


Estoy aquí para ayudarte en todo lo que necesites. Si requieres alguna modificación en lo presentado en este post, deseas asistencia con tu tarea, proyecto o precisas desarrollar un software a medida, no dudes en contactarme. Estoy comprometido a brindarte el apoyo necesario para que logres tus objetivos. Mi correo es parzibyte(arroba)gmail.com, estoy como@parzibyte en Telegram o en mi página de contacto

No te pierdas ninguno de mis posts 🚀🔔

Suscríbete a mi canal de Telegram para recibir una notificación cuando escriba un nuevo tutorial de programación.
parzibyte

Programador freelancer listo para trabajar contigo. Aplicaciones web, móviles y de escritorio. PHP, Java, Go, Python, JavaScript, Kotlin y más :) https://parzibyte.me/blog/software-creado-por-parzibyte/

Ver comentarios

Entradas recientes

Creador de credenciales web – Aplicación gratuita

Hoy te voy a presentar un creador de credenciales que acabo de programar y que…

1 semana hace

Desplegar PWA creada con Vue 3, Vite y SQLite3 en Apache

Ya te enseñé cómo convertir una aplicación web de Vue 3 en una PWA. Al…

2 semanas hace

Arquitectura para wasm con Go, Vue 3, Pinia y Vite

En este artículo voy a documentar la arquitectura que yo utilizo al trabajar con WebAssembly…

2 semanas hace

Vue 3 y Vite: crear PWA (Progressive Web App)

En un artículo anterior te enseñé a crear un PWA. Al final, cualquier aplicación que…

2 semanas hace

Errores de Comlink y algunas soluciones

Al usar Comlink para trabajar con los workers usando JavaScript me han aparecido algunos errores…

2 semanas hace

Esperar promesa para inicializar Store de Pinia con Vue 3

En este artículo te voy a enseñar cómo usar un "top level await" esperando a…

2 semanas hace

Esta web usa cookies.