Javascript: almacenando datos en el navegador de los visitantes

Todos estamos como locos venga a poner mensajes de cookies en las webs 🤪 pero hay otras 4 formas, que aquí dejo en estos codekatas, para guardar información en los navegadores de los visitantes. Estas otras formas, además, permiten guardar incluso más información que con las cookies 🤔 información totalmente segura y única para cada visitante.

Esta es la evolución natural de las webs hacia el mundo de los escritorios de los PC y aplicaciones para móviles. Es el siguiente paso para convertir una web normal, en una aplicación web instalable en PCs y móviles. Estas webs que se pueden instalar, y pueden trabajar sin conexión a Internet, se llaman PWAs (Progressive Web Apps). Además, las PWAs no necesitan instalarse desde la App Store de Google, ni de Apple, ni de Microsoft 😜 se instalan directamente porque son la misma web.. pero esto es tema para otro post..

Entendiendo esto anterior, es más que lógico, incluso necesario, poder guardar mucha información desde una web en el navegador de un visitante. Entre los visitantes no se compartirá esta información. La única comunicación de esta información será directa entre uno de los navegadores, y el servidor desde donde se sirve la web. Esto llega a tal nivel de seguridad, que si la web no tiene SSL, no se podrán usar todos los almacenes de información. Esto nos permitirá hacer cosas, como por ejemplo, convertir una web en una aplicación offline de escritorio.

Almacenes de información de Aplicación Web vistos desde el navegador Chrome.

Recapitulando, las formas disponibles a fecha de hoy para almacenar información son:

  • Cookies.
  • Cache storage.
  • Local storage.
  • Session storage.
  • IndexedDB.

Vamos al grano..

Cookies

Para guardar cookies en el navegador podemos usar código como el siguiente que he hecho:

'use strict';
var cookieExpirationDays = 30;

// Cookies /////////////////////////////////////
function setCookie(name, value) {
    var theDate = new Date();
    var currentMiliseconds = theDate.getTime();
    var expirationMiliseconds = currentMiliseconds + cookieExpirationDays * 24 * 60 * 60 * 1000;
    theDate.setTime(expirationMiliseconds);
    document.cookie = name + "=" + value + ";path=/;expires="
        + theDate.toUTCString();
}
function getCookie(name) {
    var cookiesString = document.cookie.split('; ');
    for (var i = 0; i < cookiesString.length; i++) {
        var cookieString = cookiesString[i].trim();
        if (cookieString.indexOf(name + '=') == 0) {
            return cookieString.substring((name + '=').length, cookieString.length);
        }
    }
    return "";
}
for (var i = 0; i < 3; i++) {
    setCookie("testing" + i, "This is a test " + i);
}
console.log("The value of que cookie 'testing1' is: " + getCookie("testing1"));

Esto lo podemos poner en un fichero llamado app.js y llamarlo desde la siguiente web así:

<!DOCTYPE html>
<html lang="es" dir="ltr">

    <head>
        <meta charset="utf-8">
        <title>Testing Browser Storage</title>
    </head>

    <body>
        <h1>Testing Browser Storage</h1>
        <script type="text/javascript" src="/app.js"> </script>
    </body>

</html>

Este pequeño programita de arriba, lo único que hace es declarar dos funciones que he creado. Una función es para grabar cookies, y la otra para obtener su contenido. Simplemente grabará 3 cookies en el navegador, que si inspeccionamos la web, tenemos que ver algo parecido a lo siguiente..

Y en la consola del navegador tenemos que ver un mensaje como el siguiente:

The value of que cookie 'testing1' is: This is a test 1

Lo especial de las cookies es que tienen un path y un tiempo de expiración. Por debajo del path se pueden consultar desde subdirectorios. Pero si el patch es un subdirectorio, en los directorios superiores no. Expirarán de forma automática y se borrarán con el tiempo definido, o al borrar los datos del navegador.

Cache Storage

La siguiente caché es una caché muy especial, que se guarda a base de peticiones al servidor. Esta caché está pensada para hacer Aplicaciones Web Progresivas (PWA para los amigos). Se suele usar en los Service Workers, y cachean los contenidos de la web que le digamos mediante sencillas sentencias. Por ejemplo:

// Cache storage /////////////////////////////////////
caches.open('testing-local-storage').then(
    cache => {
        for (var i = 0; i < 100; i++) {
            cache.add('item-' + i);
        }
    }
);

Si añadimos el código este Javascript en una página en localhost, creará un almacén de caché llamado testing-local-storage. Entonces cacheará 100 elementos haciendo consultas al servidor así:

http://localhost/item-0
http://localhost/item-1
http://localhost/item-2
..
http://localhost/item-97
http://localhost/item-98
http://localhost/item-99

De prueba podemos poner algo en un fichero llamado item-99 como lo siguiente, para ver cómo se cachea y se lee luego:

<!-- Something here -->

..entonces para ver el contenido de dicho fichero podemos poner en el fichero app.js del programita lo siguiente:

caches.open('testing-local-storage').then(cacheStorage => {
    cacheStorage.match('item-99').then(cachedResponse => {
        console.log(cachedResponse.text());
    });
});

..simplemente a base de promesas Javascript se puede leer el contenido. Esto si lo vemos por consola inspeccionando la web tendríamos que ver algo parecido a lo siguiente:

Cache storage leyendo item cacheado

El objeto cachedResponse devuelto en el código anterior es un stream con la respuesta del servidor cacheado en la Cache Storage. Lo dejo aquí porque esto de nuevo es material para otro post sobre los Service Workers y cómo hacer Aplicaciones Web Progresivas 😜

Local Storage

Este almacenamiendo en el navegador es de los más sencillos de usar. Muy útil y fácil, igual que el siguiente de Session Storage. Este almacenamiento es para almacenar cosas en forma de clave/valor. Se pueden almacenar, leer, actualizar y borrar. Una de las forma de uso más sencillas que hay es la del código del ejemplo siguiente:

// Local storage /////////////////////////////////////
for (var i = 0; i < 100; i++) {
    localStorage.setItem('local' + i, 'test' + i);
}
console.log('Storing in local storage, local99 is: ' + localStorage.local99);

El código fuente se explica por si mismo, por consola del navegador se tiene que ver algo como lo siguiente:

Storing in local storage, local99 is: test99

Esta caché nunca expira, mientras que el usuario no limpie los almacenes de datos del navegador.

Session Storage

Esta caché es totalmente igual a la local, pero sí que expira, sólo dura hasta que se termina la sesión, hasta que el visitante cierra el navegador. Para hacer lo mismo que lo anterior pero ahora en sesión bastaría con modificar lo anterior así:

// Session storage /////////////////////////////////////
for (var i = 0; i < 100; i++) {
    sessionStorage.setItem('session' + i, 'test' + i);
}
console.log('Storing in session storage, session99 is: ' + sessionStorage.session99);

..y veríamos en la consola del navegador lo siguiente:

Storing in session storage, session99 is: test99

IndexedDB

Y llegamos a la tecnología más completa para almacenar datos en el navegador del visitante. Esto es lo más parecido a una base de datos relacional tradicional. Digo lo más parecido, porque NO es relacional, NO hay relaciones entre los datos.

Habrá bases de datos que podremos dar de alta, y dentro de cada base de datos tendremos almacenes de objetos. Podremos entonces simular estas relaciones si fuera necesario a base de identificadores, pero no hay integridad referencial asegurada. Podremos almacenar para cada item del almacén de datos: cadenas, números, fechas y objetos. Todo esto da mucho juego..

Una forma sencilla para dar de alta una indexedDB y guardar información de 3 aplicaciones sería:

// IndexedDB
var requestDB, db, appsObjectStore;
var indexedDbName = "MyIndexedDB";
var indexedDbVersion = 1;
const theData = [{
        id: "1",
        name: "App name 1",
        website: "https://website1.com"
    },
    {
        id: "2",
        name: "App name 2",
        website: "https://website2.com"
    }, {
        id: "3",
        name: "App name 3",
        website: "https://website3.com"
    }
];
if (!indexedDB) {
    console.log("Your browser doesn't support indexedDB..");
} else {
    console.log("IndexedDB available!")
}
// IndexedDB, open for store data..
requestDB = indexedDB.open(indexedDbName, indexedDbVersion);
requestDB.onerror = function(event) {
    console.log('Error indexedDB', event);
};
requestDB.onupgradeneeded = function(event) {
    console.log('Actualizando indexedDB');
    db = event.target.result;
    var objectStore = db.createObjectStore("apps", {
        keyPath: "id"
    });
    objectStore.createIndex("name_indexx", "name", {
        unique: false
    });
    objectStore.createIndex("website_index", "website", {
        unique: true
    });
    objectStore.transaction.oncomplete = function(event) {
        appsObjectStore = db.transaction("apps", "readwrite").objectStore("apps");
        for (var i in theData) {
            appsObjectStore.add(theData[i]);
        }
    }
};

El resultado de esto visto en Chrome debe ser algo parecido a lo siguiente:

Almacenando unos datos de prueba en IndexedDB.

Mira que hemos dado de alta como clave primaria la columna id, entonces podremos hacer búsquedas por dicha columna. Otra forma sencilla a modo de ejemplo para consultar la información de la App con id 3 podría ser:

// IndexedDB, open for retrieve item..
requestDB = indexedDB.open(indexedDbName, indexedDbVersion);
requestDB.onerror = function(event) {
    console.log('Error indexedDB', event);
};
requestDB.onsuccess = function(event) {
    console.log('Éxito indexedDB', event.target.result);
    db = event.target.result;

    appsObjectStore = db.transaction("apps", "readwrite").objectStore("apps");
    var app3 = appsObjectStore.get("3").onsuccess = (event) => {
        console.log('The App 3 is ', event.target.result);
    };
};

Esto por consola debería de escribir algo como lo siguiente:

Resultado en consola de una consulta de prueba a IndexedDB

Terminando, enlaces, información..

Para terminar, sólo me queda decir que esto es el principio. Estas piezas dan mucho juego para montar muchas cosas para las webs. Dejo aquí enlaces a documentaciones que me han ayudado mucho para el que quiera seguir con ello:

Es muy recomendable también Dexie.js como librería Javascript para usar IndexedDB más fácilmente. Y aquí dejo todo el programita que he hecho, por si has llegado hasta aquí y lo quieres probar todo junto. Esto se graba en el fichero app.js para correrlo con el index.html del principio del post:

'use strict';
var cookieExpirationDays = 30;

// Cookies /////////////////////////////////////
function setCookie(name, value) {
    var theDate = new Date();
    var currentMiliseconds = theDate.getTime();
    var expirationMiliseconds = currentMiliseconds + cookieExpirationDays * 24 * 60 * 60 * 1000;
    theDate.setTime(expirationMiliseconds);
    document.cookie = name + "=" + value + ";path=/;expires=" +
        theDate.toUTCString();
}

function getCookie(name) {
    var cookiesString = document.cookie.split('; ');
    for (var i = 0; i < cookiesString.length; i++) {
        var cookieString = cookiesString[i].trim();
        if (cookieString.indexOf(name + '=') == 0) {
            return cookieString.substring((name + '=').length, cookieString.length);
        }
    }
    return "";
}
for (var i = 0; i < 3; i++) {
    setCookie("testing" + i, "This is a test " + i);
}
console.log("The value of que cookie 'testing1' is: " + getCookie("testing1"));

// Cache storage /////////////////////////////////////
caches.open('testing-local-storage').then(
    cache => {
        for (var i = 0; i < 100; i++) {
            cache.add('item-' + i);
        }
    }
);
caches.open('testing-local-storage').then(cacheStorage => {
    cacheStorage.match('item-99').then(cachedResponse => {
        console.log(cachedResponse.text());
    });
});

// Local storage /////////////////////////////////////
for (var i = 0; i < 100; i++) {
    localStorage.setItem('local' + i, 'test' + i);
}
console.log('Storing in local storage, local99 is: ' + localStorage.local99);

// Session storage /////////////////////////////////////
for (var i = 0; i < 100; i++) {
    sessionStorage.setItem('session' + i, 'test' + i);
}
console.log('Storing in session storage, session99 is: ' + sessionStorage.session99);

// IndexedDB /////////////////////////////////////
var requestDB, db, appsObjectStore;
var indexedDbName = "MyIndexedDB";
var indexedDbVersion = 1;
const theData = [{
        id: "1",
        name: "App name 1",
        website: "https://website1.com"
    },
    {
        id: "2",
        name: "App name 2",
        website: "https://website2.com"
    }, {
        id: "3",
        name: "App name 3",
        website: "https://website3.com"
    }
];
if (!indexedDB) {
    console.log("Your browser doesn't support indexedDB..");
} else {
    console.log("IndexedDB available!")
}
// IndexedDB, open for store data..
requestDB = indexedDB.open(indexedDbName, indexedDbVersion);
requestDB.onerror = function(event) {
    console.log('Error indexedDB', event);
};
requestDB.onupgradeneeded = function(event) {
    console.log('Actualizando indexedDB');
    db = event.target.result;
    var objectStore = db.createObjectStore("apps", {
        keyPath: "id"
    });
    objectStore.createIndex("name_indexx", "name", {
        unique: false
    });
    objectStore.createIndex("website_index", "website", {
        unique: true
    });
    objectStore.transaction.oncomplete = function(event) {
        appsObjectStore = db.transaction("apps", "readwrite").objectStore("apps");
        for (var i in theData) {
            appsObjectStore.add(theData[i]);
        }
    }
};
// IndexedDB, open for retrieve item..
requestDB = indexedDB.open(indexedDbName, indexedDbVersion);
requestDB.onerror = function(event) {
    console.log('Error indexedDB', event);
};
requestDB.onsuccess = function(event) {
    console.log('Éxito indexedDB', event.target.result);
    db = event.target.result;

    appsObjectStore = db.transaction("apps", "readwrite").objectStore("apps");
    var app3 = appsObjectStore.get("3").onsuccess = (event) => {
        console.log('The App 3 is ', event.target.result);
    };
};

Está probado en Chrome y Firefox. Para cualquier cosa no dudes en dejar un comentario aquí abajo. Recuerda también que puedes suscribirte y sólo te enviaré un email cuando escriba un nuevo post 😉 Otro día más.. ¡Un saludo!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *