Krypton Solid

Creación de una lista de tareas sin conexión de navegador cruzado simple con IndexedDB y WebSQL

Creación de una lista de tareas sin conexión de navegador cruzado simple con IndexedDB y WebSQL

Vamos a hacer un sencillo sin conexión primero aplicación de tareas pendientes con tecnología HTML5. Esto es lo que hará la aplicación:

  • almacenar datos fuera de línea y cargarlos sin conexión a Internet;
  • permitir que el usuario agregue y elimine elementos en la lista de tareas pendientes;
  • almacenar todos los datos localmente, sin back-end;
  • se ejecuta en la primera y segunda versión más reciente de todos los principales navegadores de escritorio y móviles.

El proyecto completo es listo para bifurcar en GitHub.

Qué tecnologías utilizar

En un mundo ideal, usaríamos solo una tecnología de base de datos de cliente. Desafortunadamente, tendremos que usar dos:

Los veteranos del primer mundo sin conexión ahora podrían estar pensando: «Pero podríamos usar almacenamiento local, que tiene los beneficios de una API mucho más simple, y no tendríamos que preocuparnos por la complejidad de usar tanto IndexedDB como WebSQL «. Si bien eso es técnicamente cierto, localStorage tiene varios problemas, el más importante de los cuales es que la cantidad de espacio de almacenamiento disponible es significativamente menor que IndexedDB y WebSQL.

Afortunadamente, aunque necesitaremos usar ambos, solo tendremos que pensar en IndexedDB. Para admitir WebSQL, usaremos un Polyfill de IndexedDB. Esto mantendrá nuestro código limpio y fácil de mantener, y una vez que todos los navegadores que nos interesan sean compatibles con IndexedDB de forma nativa, simplemente podemos eliminar el polyfill.

Nota: Si está comenzando un nuevo proyecto y está decidiendo si usar IndexedDB o WebSQL, recomiendo encarecidamente usar IndexedDB y polyfill. En mi opinión, no hay razón para escribir ningún código nuevo que se integre directamente con WebSQL.

Seguiré todos los pasos utilizando Google Chrome (y sus herramientas de desarrollo), pero no hay ninguna razón por la que no pueda desarrollar esta aplicación con ningún otro navegador moderno.

1. Andamiaje de la aplicación y apertura de una base de datos

Crearemos los siguientes archivos en un solo directorio:

  • /index.html
  • /application.js
  • /indexeddb.shim.min.js
  • /styles.css
  • /offline.appcache

/index.html

<!DOCTYPE html>
<html>
  <head>
    <link rel="stylesheet" href="https://www.smashingmagazine.com/2014/09/building-simple-cross-browser-offline-todo-list-indexeddb-websql/./styles.css" type="text/css" media="all" />
  </head>
  <body>
    <h1>Example: Todo</h1>
    <form>
      <input placeholder="Type something" />
    </form>
    <ul>
    </ul>
    <script src="./indexeddb.shim.min.js"></script>
    <script src="./application.js"></script>
  </body>
</html>

Nada sorprendente aquí: solo una página web HTML estándar, con un campo de entrada para agregar elementos de tareas pendientes y una lista vacía y desordenada que se completará con esos elementos.

/indexeddb.shim.min.js

Descargar el contenido del polyfill de IndexedDB minificadoy colóquelo en este archivo.

/styles.css

body {
  margin: 0;
  padding: 0;
  font-family: helvetica, sans-serif;
}

* {
  box-sizing: border-box;
}

h1 {
  padding: 18px 20px;
  margin: 0;
  font-size: 44px;
  border-bottom: solid 1px #DDD;
  line-height: 1em;
}

form {
  padding: 20px;
  border-bottom: solid 1px #DDD;
}

input {
  width: 100%;
  padding: 6px;
  font-size: 1.4em;
}

ul {
  margin: 0;
  padding: 0;
  list-style: none;
}

li {
  padding: 20px;
  border-bottom: solid 1px #DDD;
  cursor: pointer;
}

Una vez más, esto debería ser bastante familiar: solo algunos estilos simples para que la lista de tareas se vea ordenada. Puede elegir no tener ningún estilo o crear el suyo propio.

/application.js

(function() {

  // 'global' variable to store reference to the database
  var db;

  databaseOpen(function() {
    alert("The database has been opened");
  });

  function databaseOpen(callback) {
    // Open a database, specify the name and version
    var version = 1;
    var request = indexedDB.open('todos', version);

    request.onsuccess = function(e) {
      db = e.target.result;
      callback();
    };
    request.onerror = databaseError;
  }

  function databaseError(e) {
    console.error('An IndexedDB error has occurred', e);
  }

}());

Todo lo que hace este código es crear una base de datos con indexedDB.open y luego mostrarle al usuario una alerta pasada de moda si tiene éxito. Cada base de datos IndexedDB necesita un nombre (en este caso, todos) y un número de versión (que configuré en 1).

Para comprobar que está funcionando, abra la aplicación en el navegador, abra «Herramientas de desarrollo» y haga clic en la pestaña «Recursos».

En el panel «Recursos», puede verificar si está funcionando.

Al hacer clic en el triángulo junto a «IndexedDB», debería ver que una base de datos llamada todos Ha sido creado.

2. Creación del almacén de objetos

Como muchos formatos de base de datos con los que puede estar familiarizado, puede crear muchas tablas en una sola base de datos IndexedDB. Estas tablas se denominan «objectStores». En este paso, crearemos un almacén de objetos llamado todo. Para hacer esto, simplemente agregamos un detector de eventos en la base de datos upgradeneeded evento.

El formato de datos en el que almacenaremos las tareas pendientes serán los objetos JavaScript, con dos propiedades:

  • timeStamp Esta marca de tiempo también actuará como nuestra clave.
  • text Este es el texto que ha ingresado el usuario.

Por ejemplo:

{ timeStamp: 1407594483201, text: 'Wash the dishes' }

Ahora, /application.js se ve así (el nuevo código comienza en request.onupgradeneeded):

(function() {

  // 'global' variable to store reference to the database
  var db;

  databaseOpen(function() {
    alert("The database has been opened");
  });

  function databaseOpen(callback) {
    // Open a database, specify the name and version
    var version = 1;
    var request = indexedDB.open('todos', version);

    // Run migrations if necessary
    request.onupgradeneeded = function(e) {
      db = e.target.result;
      e.target.transaction.onerror = databaseError;
      db.createObjectStore('todo', { keyPath: 'timeStamp' });
    };

    request.onsuccess = function(e) {
      db = e.target.result;
      callback();
    };
    request.onerror = databaseError;
  }

  function databaseError(e) {
    console.error('An IndexedDB error has occurred', e);
  }

}());

Esto creará un almacén de objetos con la clave timeStamp y nombrado todo.

¿O lo hará?

Habiendo actualizado application.js, si vuelve a abrir la aplicación web, no pasarán muchas cosas. El código en onupgradeneeded nunca corre; intente agregar un console.log en el onupgradeneeded devolución de llamada para estar seguro. El problema es que no hemos incrementado el número de versión, por lo que el navegador no sabe que necesita ejecutar la devolución de llamada de actualización.

¿Cómo solucionar esto?

Siempre que agregue o elimine almacenes de objetos, deberá incrementar el número de versión. De lo contrario, la estructura de los datos será diferente de lo que espera su código y corre el riesgo de romper la aplicación.

Debido a que esta aplicación aún no tiene usuarios reales, podemos solucionar esto de otra manera: eliminando la base de datos. Copie esta línea de código en la «Consola» y luego actualice la página:

indexedDB.deleteDatabase('todos');

Después de actualizar, el panel «Recursos» de «Herramientas de desarrollo» debería haber cambiado y ahora debería mostrar el almacén de objetos que agregamos:

El panel Recursos ahora debería mostrar el almacén de objetos que se agregó.
El panel «Recursos» ahora debería mostrar el almacén de objetos que se agregó.

3. Agregar elementos

El siguiente paso es permitir que el usuario agregue elementos.

/application.js

Tenga en cuenta que he omitido el código de apertura de la base de datos, indicado por puntos suspensivos (…) a continuación:

(function() {

  // Some global variables (database, references to key UI elements)
  var db, input;

  databaseOpen(function() {
    input = document.querySelector('input');
    document.body.addEventListener('submit', onSubmit);
  });

  function onSubmit(e) {
    e.preventDefault();
    databaseTodosAdd(input.value, function() {
      input.value = ’;
    });
  }

[…]

  function databaseTodosAdd(text, callback) {
    var transaction = db.transaction(['todo'], 'readwrite');
    var store = transaction.objectStore('todo');
    var request = store.put({
      text: text,
      timeStamp: Date.now()
    });

    transaction.oncomplete = function(e) {
      callback();
    };
    request.onerror = databaseError;
  }

}());

Hemos agregado dos bits de código aquí:

  • El oyente de eventos responde a cada submit evento, evita la acción predeterminada de ese evento (que de otro modo actualizaría la página), llama databaseTodosAdd con el valor de la input elemento, y (si el elemento se agrega correctamente) establece el valor de la input elemento para estar vacío.
  • Una función llamada databaseTodosAdd almacena la tarea pendiente en la base de datos local, junto con una marca de tiempo, y luego ejecuta un callback.

Para probar que esto funciona, abra la aplicación web nuevamente. Escriba algunas palabras en el input elemento y presione «Enter». Repita esto varias veces y luego abra «Herramientas de desarrollo» en la pestaña «Recursos» nuevamente. Debería ver que los elementos que escribió ahora aparecen en el todo tienda de objetos.

03-step3-dev-tools-opt-500
Después de agregar algunos elementos, deberían aparecer en el todo tienda de objetos. (Ver versión grande)

4. Recuperación de elementos

Ahora que hemos almacenado algunos datos, el siguiente paso es averiguar cómo recuperarlos.

/application.js

Nuevamente, las elipses indican código que ya hemos implementado en los pasos 1, 2 y 3.

(function() {

  // Some global variables (database, references to key UI elements)
  var db, input;

  databaseOpen(function() {
    input = document.querySelector('input');
    document.body.addEventListener('submit', onSubmit);
    databaseTodosGet(function(todos) {
      console.log(todos);
    });
  });

[…]

  function databaseTodosGet(callback) {
    var transaction = db.transaction(['todo'], 'readonly');
    var store = transaction.objectStore('todo');

    // Get everything in the store
    var keyRange = IDBKeyRange.lowerBound(0);
    var cursorRequest = store.openCursor(keyRange);

    // This fires once per row in the store. So, for simplicity,
    // collect the data in an array (data), and pass it in the
    // callback in one go.
    var data = [];
    cursorRequest.onsuccess = function(e) {
      var result = e.target.result;

      // If there's data, add it to array
      if (result) {
        data.push(result.value);
        result.continue();

      // Reach the end of the data
      } else {
        callback(data);
      }
    };
  }

}());

Una vez que se haya inicializado la base de datos, esto recuperará todos los elementos pendientes y los enviará a la consola de «Herramientas para desarrolladores».

Note como el onsuccess se llama a la devolución de llamada después de que se recupera cada elemento del almacén de objetos. Para simplificar las cosas, colocamos cada resultado en una matriz llamada data, y cuando nos quedamos sin resultados (lo que sucede cuando hemos recuperado todos los elementos), llamamos al callback con esa matriz. Este enfoque es simple, pero otros enfoques pueden ser más eficientes.

Si vuelve a abrir la aplicación, la consola de «Herramientas para desarrolladores» debería verse un poco así:

La consola después de reabrir la aplicación.
La consola después de reabrir la aplicación.

5. Visualización de artículos

El siguiente paso después de recuperar los elementos es mostrarlos.

/application.js

(function() {

  // Some global variables (database, references to key UI elements)
  var db, input, ul;

  databaseOpen(function() {
    input = document.querySelector('input');
    ul = document.querySelector('ul');
    document.body.addEventListener('submit', onSubmit);
    databaseTodosGet(renderAllTodos);
  });

  function renderAllTodos(todos) {
    var html = ’;
    todos.forEach(function(todo) {
      html += todoToHtml(todo);
    });
    ul.innerHTML = html;
  }

  function todoToHtml(todo) {
    return '<li>'+todo.text+'</li>';
  }

[…]

Todo lo que hemos agregado son un par de funciones muy simples que representan las tareas pendientes:

  • todoToHtml Esto toma un todos objeto (es decir, el objeto JavaScript simple que definimos anteriormente).
  • renderAllTodos Esto requiere una variedad de todos objetos, los convierte en una cadena HTML y establece la lista desordenada innerHTML lo.

Finalmente, estamos en un punto en el que realmente podemos ver lo que hace nuestra aplicación sin tener que buscar en «Herramientas de desarrollo». Abra la aplicación nuevamente y debería ver algo como esto:

Su aplicación en la vista frontal
Su aplicación en la vista frontal (Ver versión grande)

Pero aún no hemos terminado. Debido a que la aplicación solo muestra elementos cuando se inicia, si agregamos alguno nuevo, no aparecerán a menos que actualice la página.

6. Visualización de nuevos elementos

Podemos solucionar esto con una sola línea de código.

/application.js

El nuevo código es solo la línea databaseTodosGet(renderAllTodos);.

[…]

function onSubmit(e) {
  e.preventDefault();
  databaseTodosAdd(input.value, function() {
    // After new items have been added, re-render all items
    databaseTodosGet(renderAllTodos);
    input.value = ’;
  });
}

[…]

Aunque esto es muy simple, no es muy eficiente. Cada vez que agregamos un elemento, el código recuperará todos los elementos de la base de datos nuevamente y los mostrará en la pantalla.

7. Eliminación de elementos

Para mantener las cosas lo más simples posible, permitiremos a los usuarios eliminar elementos haciendo clic en ellos. (Para una aplicación real, probablemente querríamos un botón «Eliminar» dedicado o mostrar un cuadro de diálogo para que un elemento no se elimine accidentalmente, pero esto estará bien para nuestro pequeño prototipo).

Para lograr esto, seremos un poco hacky y le daremos a cada elemento un conjunto de ID a su timeStamp. Esto habilitará el detector de eventos de clic, que agregaremos al documento body, para detectar cuándo el usuario hace clic en un elemento (a diferencia de cualquier otro lugar de la página).

/application.js

(function() {

  // Some global variables (database, references to key UI elements)
  var db, input, ul;

  databaseOpen(function() {
    input = document.querySelector('input');
    ul = document.querySelector('ul');
    document.body.addEventListener('submit', onSubmit);
    document.body.addEventListener('click', onClick);
    databaseTodosGet(renderAllTodos);
  });

  function onClick(e) {

    // We'll assume that any element with an ID
    // attribute is a to-do item. Don't try this at home!
    if (e.target.hasAttribute('id')) {

      // Because the ID is stored in the DOM, it becomes
      // a string. So, we need to make it an integer again.
      databaseTodosDelete(parseInt(e.target.getAttribute('id'), 10), function() {

        // Refresh the to-do list
        databaseTodosGet(renderAllTodos);
      });
    }
  }

[…]

  function todoToHtml(todo) {
    return '<li id="'+todo.timeStamp+'">'+todo.text+'</li>';
  }

[…]

  function databaseTodosDelete(id, callback) {
    var transaction = db.transaction(['todo'], 'readwrite');
    var store = transaction.objectStore('todo');
    var request = store.delete(id);
    transaction.oncomplete = function(e) {
      callback();
    };
    request.onerror = databaseError;
  }

}());

Hemos realizado las siguientes mejoras:

  • Hemos agregado un nuevo controlador de eventos (onClick) que escucha los eventos de clic y comprueba si el elemento de destino tiene un atributo de ID. Si tiene uno, lo convierte de nuevo en un número entero con parseInt, llamadas databaseTodosDelete con ese valor y, si el elemento se elimina correctamente, vuelve a renderizar la lista de tareas pendientes siguiendo el mismo enfoque que tomamos en el paso 6.
  • Hemos mejorado el todoToHtml función de modo que cada elemento pendiente se genere con un atributo de ID, establecido en su timeStamp.
  • Hemos agregado una nueva función, databaseTodosDelete, que toma eso timeStamp y un callback, elimina el elemento y luego ejecuta el callback.

Nuestra aplicación de tareas pendientes básicamente tiene funciones completas. Podemos agregar y eliminar elementos, y funciona en cualquier navegador que admita WebSQL o IndexedDB (aunque podría ser mucho más eficiente).

Casi llegamos

¿Realmente hemos creado una aplicación para hacer primero fuera de línea? Casi, pero no del todo. Si bien ahora podemos almacenar todos los datos sin conexión, si apaga la conexión a Internet de su dispositivo e intenta cargar la aplicación, no se abrirá. Para solucionar esto, necesitamos usar el Caché de aplicaciones HTML5.

Advertencia

  • Si bien la caché de aplicaciones HTML5 funciona razonablemente bien para una aplicación simple de una sola página como esta, no siempre es así. Minuciosamente investiga cómo funciona antes de considerar si aplicarlo a su sitio web.
  • Trabajador del servicio pronto podría reemplazar la caché de aplicaciones HTML5, aunque actualmente no se puede utilizar en ningún navegador, y ni Apple ni Microsoft se han comprometido públicamente a admitirlo.

8. Verdaderamente sin conexión

Para habilitar la caché de la aplicación, agregaremos un manifest atribuir a la html elemento de la página web.

/index.html

<!DOCTYPE html>
<html manifest="./offline.appcache">
[…]

Luego, crearemos un archivo de manifiesto, que es un archivo de texto simple en el que especificamos de manera cruda los archivos para que estén disponibles sin conexión y cómo queremos que se comporte la caché.

/offline.appcache

CACHE MANIFEST
./styles.css
./indexeddb.shim.min.js
./application.js

NETWORK:
*

La sección que comienza CACHE MANIFEST le dice al navegador lo siguiente:

  • Cuando acceda a la aplicación por primera vez, descargue cada uno de esos archivos y guárdelos en el caché de la aplicación.
  • Cada vez que se necesite alguno de esos archivos a partir de ese momento, cargue las versiones en caché de los archivos, en lugar de volver a descargarlos de Internet.

La sección que comienza NETWORK le dice al navegador que todos los demás archivos deben descargarse nuevos de Internet cada vez que se necesiten.

¡Éxito!

Hemos creado un aplicación de tareas rápida y sencilla que funciona sin conexión y que se ejecuta en todos los principales navegadores modernos, gracias a IndexedDB y WebSQL (a través de un polyfill).

Recursos

Otras lecturas en SmashingMag:

Deja un comentario