Prevenir el bloqueo de la UI

Con el auge de las llamadas asíncronas a través de peticiones AJAX me he acostumbrado a que todas las operaciones con javascript no bloqueen la interfaz gráfica de usuario y que cuando los datos estén listos ésta se actualice sin que el usuario se vea perjudicado por la obtención y procesado de los datos.

Pero claro, ¿qué pasa si los datos ya los tenemos cacheados y tenemos que procesarlos? Lo que ocurre por lo general es que se nos bloquea la interfaz de usuario y mientras se procesan el navegador o el dispositivo móvil se queda sin responder.

Una solución sencilla a este problema es provocar una llamada asincrona de la función que procesa los datos. Para ello podemos utilizar la función de setTimeout de javascript de la siguiente manera:

setTimeout(function() {
    procesadoDeDatos();
}, 100);

Con esto conseguimos que la ejecución de la función procesadoDeDatos se haga de manera asincrona y no se bloquee la interfaz esperan a que acabe el procesado.

Así podemos poner el típico mensaje de procesando, para que el usuario sepa que se está procesando su petición y éste a su vez si quiere puede seguir interactuando con la interfaz mientras espera que se le devuelvan los datos.

Cache de imágenes

Sigo con el desarrollo de mi aplicación para iPad y el siguiente muro con el que me he topado ha sido el tema de cachear las imágenes. Mi aplicación accede a muchas imágenes que están en Internet y por tanto el consumo de ancho de banda es considerable. Lo que quería es que una vez que se accediera a la imagen, está se almacenara en cache y no se tuviera que volver a pedir.

Después de mucha investigación y muchas pruebas, he dado con dos soluciones: una para cuando el número de imágenes a almacenar no es muy grande (este no es mi caso) y otra que de momento promete guardar todas la imágenes que quiera.

LocalStorage, Web DB SQL y Canvas

La primera solución consiste en utilizar una combinación de localStorage y Web DB SQL para almacenar las imágenes y Canvas para obtener la versión base64 de la misma.

Lo primero que necesitamos por tanto es una función a la que le pasamos una URL de una imagen y nos devuelve una representación en base64 de la misma:

function getImgBase64(imagen, callback) {
    var img = document.createElement('img'),
        canvas = document.createElement('canvas'),
        ctx = canvas.getContext('2d');
    img.src = imagen;

    img.addEventListener('load', function() {
        canvas.width = this.width;
        canvas.height = this.height;
        ctx.drawImage(this, 0, 0);
        callback(canvas.toDataURL('image'));
    }, false);
}

Ahora que tenemos la representación de la imagen en base64 necesitamos almacenarla de manera persistente, para ello tenemos localStorage y Web DB SQL. Toca enfrentarse a los límites de tamaño, localStorage permite almacenar 5M y Web DB SQL permite 50M que los puedes repartir en unos 10 BBDD de 5M sin tener que preguntar al usuario si quiere permitir crear una BBDD de tamaño mayor. Cuando estos límites se alcanzan se deja de guardar información.

Si tenemos claro que las imágenes que queremos guardar van a ocupar menos de 5M usaremos localStorage, si es mayor que 5M e inferior a 50M usaremos una combinación de ambas, para minimizar el acceso al Web DB SQL ya que a parte de ser asincrono tiene un retardo, menor que si accediéramos a Internet, pero un retardo que puede quitar fluidez a la aplicación.

La función que almacena en el localStorage sería la siguiente:

function setImg(ref, img) {
    getImgBase64(img, function(base64) {
        var imagenes = localStorage.imagenes && JSON.parse(localStorage.imagenes) || [];
        imagenes[ref] = base64;
        localStorage.imagenes = JSON.stringify(imagenes);
    });
}

Para obtener la imagen del localStorage:

function getImg(ref) {
     var imagenes = localStorage.imagenes && JSON.parse(localStorage.imagenes) || [];
     return imagenes[ref] || '';
}

La función completa que se encarga de hacer una pre-carga de las imágenes quedaría de la siguiente manera:

function preLoadImg(ref, img) {
   var imagen = new Image(),
       data = getImg(ref);

   imagen.addEventListener('load', function() {
        // Acciones una vez que tenemos cargada la imagen.
   }, false);

   if (data === '') {
      imagen.src = img;
      setImg(ref, img);
   }
   else {
      imagen.src = data;
   }
}

Para la versión mixta entre localStorage y Web DB SQL, lo que vamos a hacer es almacenar las referencias y estado en localStorage y la imagen codificada en base64 en el Web DB SQL.

Primero vamos a definir qué es lo que guardamos en el localStorage: las referencias a las imágenes que tenemos guardas en Web DB SQL y el número y el tamaño que llevamos en la BBDD actual.

Al empezar, las referencias serán un array vacío y el número y el tamaño serán 0.

Para inicializar la variables y crear la BBDD haremos una función de inicialización:


var DB = [];

function init() {
    var referencias = localStorage.referencias && JSON.parse(localStorage.referencias) || [],
        actual = localStorage.actual && JSON.parse(localStorage.actual) || {n: 0, t:0},
        DBI;

    for (var i = 0; i <= actual.n; i+=1) {
         DBI = openDatabase('Imagenes_' + i, '1.0', 'Imagenes ' + i, 4.9 * 1024 * 1024);
         DBI.transaction(function(t) {
                t.executeSql('CREATE TABLE IF NOT EXISTS Imagenes(ref TEXT NOT NULL, img TEXT NOT NULL, PRIMARY KEY(ref))', []);
            });

         DB.push(DBI);
    }

}

En la función de inicialización, la primera vez que se inicia se crea la BBDD Imagenes_0 y las demás veces se abren las BBDD que llevemos usadas hasta el momento.

La función que guarda una imagen en BBDD es algo más complicada que su versión únicamente con localStorage ya que tenemos que tener en cuenta el tamaño que llevamos en la BBDD actual y la creación de una nueva BBDD en caso de que sea necesario.

Como podemos ver defino una variable global DB donde voy a ir almacenando las conexiones a las distintas BBDD.

function setImgDB(ref, img) {
    getImgBase64(img, function(base64) {
        var actual = localStorage.actual && JSON.parse(localStorage.actual) || {n: 0; t: 0},
            referencias = localStorage.referencias && JSON.parse(localStorage.referencias) || [];

        if (actual.t >= 1810000) {

            actual.n += 1;
            actual.t = base64.length;

            referencias[ref] = actual.n;

            localStorage.actual = JSON.stringify(actual);
            localStorage.referencias = JSON.stringify(refrencias);

            DBI = openDatabase('Imagenes_' + actual.n , '1.0', 'Imagenes ' + actual.n, 4.9 * 1024 * 1024);
            DBI.transaction(function(t) {
                    t.executeSql('CREATE TABLE IF NOT EXISTS Imagenes(ref TEXT NOT NULL, img TEXT NOT NULL, PRIMARY KEY(ref))', [], function(t, r) {
                            t.executeSql('REPLACE INTO Imagenes(ref, img) VALUES(?, ?)', [ref, base64], function(t, r) {
                                console.log('Insertamos la imagen: ' + ref + ' en BBDD ' + actual.n);
                            });
                    });
                });

            DB.push(DBI);
        }
        else {

            actual.t += base64.length;

            referencias[ref] = actual.n;

            localStorage.actual = JSON.stringify(actual);
            localStorage.referencias = JSON.stringify(refrencias);

            DB[actual.n].transaction(function(t) {
                    t.executeSql('REPLACE INTO Imagenes(ref, img) VALUES(?, ?)', [ref, base64], function(t, r) {
                        console.log('Insertamos la imagen: ' + ref + ' en BBDD ' + actual.n);
                    });
                });

        }
    });
}

El número 1810000 es el tamaño máximo aproximado de imágenes que se pueden almacenar, es un tamaño totalmente empirico y que se podría afinar un poco más.

Ahora necesitamos la función para obtener una imagen de la BBDD:

function getImgDB(ref, callback) {
     var referencias = localStorage.referencias && JSON.parse(localStorage.referencias) || [],
         n = referencias[ref] || -1;

     if (n !== -1) {
         DB[n].transaction(function(t) {
                    t.executeSql('SELECT img FROM Imagenes WHERE ref = ?', [ref], function(t, r) {
                        if (r.rows.length > 0) {
                            callback(r.rows.item(0).img);
                        }
                        else {
                           callback('');
                        }
                    });
                });
     }
     else {
         callback('');
     }
}

Y ahora la función de pre-carga de imágenes:

function preLoadImg(ref, img) {
   var imagen = new Image();

   getImgDB(ref, function(data) {
       if (data === '') {
           imagen.src = img;
           setImgDB(ref, img);
       }
       else {
          imagen.src = data;
       }
    });

   imagen.addEventListener('load', function() {
        // Acciones una vez que tenemos cargada la imagen.
   }, false);
}

Como he dicho antes, estás dos soluciones son para un número pequeño y controlado de imágenes.

En el siguiente apartado veremos cómo hacerlo cuando no sabemos el número de imágenes a cachear.

API de Phonegap

En está solución necesitamos acceder al dispositivo, por lo tanto necesitamos una API que nos proporcione acceso al mismo, en este caso yo he escogido Phonegap. A parte de la API de Phonegap, necesitamos un Plugin que nos permite descargar ficheros a nuestro dispositivo.

No voy a meterme en cómo se instala el Phonegap ni el plugin, voy a suponer que ya está todo hecho y vamos directamente al código.

Primero una función de inicialización, necesaria para usar las API's de Phonegap


var fileSystem = {};

function init() {
    document.addEventListener("deviceready", function() {
        window.requestFileSystem(LocalFileSystem.PERSISTENT, 0, function(fs) {
            fileSystem = fs;
        },
        function() {
            console.log('Error de inicialización');
        });
    }, false);
}

Ahora la función para almacenar una imagen en el dispositivo:

function setImgFile(ref, img) {
    var ext = img.split('.')[3],
        fullPath = fileSystem.root.fullPath + '/',
        prev = 'file://' + fullPath + ref + '.' + ext,
        image = new Image();

   image.addEventListener('error', function() {
       var downloader = new Downloader();

        downloader.downloadFile(
            img,
            fullPath,
            ref + '.' + ext,
            function(file) {
                console.log(file);
            },
            function() {
                console.log('Error al subir la imagen');
        });
   }, false);

   image.src = prev;
}

Para la función anterior, suponemos que la imagen está ya en nuestro dispositivo, y por tanto le asignamos el path que suponemos que tiene, en caso de error es cuando nos la bajamos al dispositivo, con esto hacemos que sea más rápida la búsqueda de una imagen y no tenemos que acceder al sistema de archivos que es mucho más lento.

Con esta solución no nos hace falta una función para obtener la imagen ya que las imagenes se guardan en el dispositivo en una ubicación conocida, por tanto vamos con la función de pre-carga de imagenes que usará el mismo principio de suponer que la imagen ya está en nuestro dispositivo antes de pedirla a Internet.

function preLoadImg(ref, img) {
    var ext = img.split('.')[3],
        fullPath = fileSystem.root.fullPath + '/',
        prev = 'file://' + fullPath + ref + '.' + ext,
        image = new Image();

    image.addEventListener('load', function() {
        // Acciones una vez que tenemos cargada la imagen.
    }, false);

   image.addEventListener('error', function() {
       this.src = img;
   }, false);

   image.src = prev;
}

Conclusión

Está claro que cuanto menos ancho de banda consuma una aplicación y cuanto más rápida sea en mostrar el contenido al usuario, éste se va a sentir más satisfecho y valorará mejor nuestra aplicación, por tanto debemos de ser cautos a la hora de cachear de manera que no se vea afectado el rendimiento de la aplicación por el acceso y la actualización de la misma.

Quiero destacar que las dos primeras soluciones son perfectamente válidas para una aplicación WEB, la segunda al depender del dispositivo solo servirá para dispositivos móviles, en este caso iPad y iPhone.

Referencias

http://stackoverflow.com/questions/934012/get-image-data-in-javascript
http://docs.phonegap.com/en/1.2.0/phonegap_file_file.md.html#File
https://github.com/aflx/pg_downloader_plugin_ios

Zepto vs jQuery

Estoy inmerso en el desarrollo de una aplicación web para iPad, como el tema de los recursos que se consumen es muy importante en este dispositivo, estoy intentando optimizar al máximo y por tanto decidí utilizar una framework de javascript ligero: Zepto.

Al principio todo iba de maravilla, el framework apenas pesa 5K y te ofrece la mayor parte de la funcionalidad de jQuery, framework al que estoy acostumbrado a usar.

Al terminar la primera iteración del desarrollo me pongo a hacer pruebas de rendimiento y compruebo que tarda muchísimo en hacer una llamada AJAX para obtener un JSON con datos para pintar la interfaz.

Al principio pienso que es mi código y que necesita más optimizaciones, reduzco al máximo el código y aún así veo un retraso de unos 7 u 8 segundos en el proceso de la petición.

Tengo que decir que antes de decantarme por Zepto hice una primera versión usando jQuery y el problema lo tenía en el renderizado de la interfaz y no en la llamada AJAX. Así que vuelvo a poner jQuery como framework de la aplicación y como la sintaxis entre ambos frameworks es compatible, veo que sí es cierto que realiza más acciones, que probablemente podré optimizar más adelante, lo que es la llamada AJAX se hace de manera fluida y sin el retardo de 7 segundos.

Analizado el código de ambos frameworks veo que en Zepto se hace un eval cuando el resultado es de una llamada AJAX es un JSON y por tanto es ahí donde se pierde el tiempo y hace que la aplicación se quede momentaneamente congelada.

La moraleja de esta historia es que aunque un framework pese poco y esté especificamente diseñado para un tipo de navegadores, en este caso Webkit, no tiene por qué ser más eficiente a la hora de realizar ciertas acciones y por tanto a la hora de elegirlo habrá que ver que tipo de llamadas hay que hacer y si nos merece la pena usarlo o no.

En defensa de Zepto puedo decir que está en fase beta y que me imagino que irán mejorando con el tiempo, pero por ahora, para una aplicación como la mía que requiere de procesado de JSON en las llamadas AJAX no es válida y tendré que seguir usando jQuery aunque sea más pesada y no sea especifica para Webkit.

Codificación de los feeds

Hoy estaba desarrollando un feed para un portal y me he encontrado con que tenía el feed con codificación UTF-8 y mi servidor estaba configurado para entregar US-ASCII.

Como no quiero tocar el servidor, lo que he hecho ha sido meter el una cabecera Content-type:

Content-type: application/xml

Lo normal hubiera sido poner text/xml, pero poniendo application/xml lo que haces es forzar a que te coja la codificación que le estás indicando en el fichero, no la del servidor.

Firefox – búsqueda rápida

A lo largo del día uso distintos sistemas operativos y distintos programas, lo que hace que a veces se me crucen los cables y mezcle comandos entre ellos por inercia.

Hoy he mezclado el Firefox con el vi, si ya se que es dificil pero bueno. La cuestión es que he visualizado el código fuente de una página y he ido a buscar una palabra dentro de él y sin quere en vez de pulsa CTRL + F, he pulsado SHIFT + 7, es decir la barra /, que es como se realizan las búsquedas en el vi, y cual ha sido mi sorpresa cuando me he dado cuenta de que también funciona.

Al pulsar SHIFT + 7 te sale lo que se denomina como búsqueda rápida y la puedes utilizar tanto en la visualización del código fuente como para buscar texto en la página en si.

Reloj

Leyendo un post en meneame.net he encontrado una página en la cual hay relojes para poner en una página, yo he puesto el mio dentro de un div y posicionandolo de manera absoluta.

La página con los relojes es:

http://www.clocklink.com/

Crear un usuario mediante comandos en MySQL

Igual mucha gente ya lo sabe, pero una manera sencilla de crear un usuario mediante comandos es la siguiente:

GRANT ALL PRIVILEGES ON mibd.* TO miusuario@mihost IDENTIFIED BY 'mipassword'

Con este comando creamos un usuario que tienen todos los privilegios para la base de datos mibd

La verdad que si tienes que dar de alta muchos usuarios o lo haces mediantes SCRIPT’s es bastante útil

Otro comando útil por si usas clientes de MySQL 4.0 es este:

SET PASSWORD miusuario = OLD_PASSWORD('mipassword');

con esto cambias el password al formato de antiguo para que puedan usarlos los clientes de MySQL 4.0

Dominio

Hoy me había quedado sin dominio, gracias a Bad_CRC por avisarme, la cuestión era que la página a través de la cual lo compre tiene una “feature” muy simpática para que se renueve automáticamente, y yo hace meses la active, lo que no se si esa “feature” consiste en enviarme únicamente mensajes a mi cuenta que se gestiona desde esa página o realmente me lo renueva, porque lo que ha sucedido hoy es que no tenía dominio y en la página en cuestión había cuatro mensajes avisandome de que se me iba a caducar.

Igual yo le he entendido mal, pero cuando pone: “Autorenew”, ¿No significa que se renueva solo?

Bueno, al final todo ha pasado, y desde la misma administración había una opción para volver a pedir el dominio y parece que esa opción si ha funcionado.

Por lo demás, la página está bastante bien y por 7 € tienes un dominio para todo un año, asi que la recomiendo:

http://www.namecheap.com

Includes y Rewrites

Me comenta Bad_CRC que hace mucho que no posteo, la verdad es que últimamente no hago cosas muy interesantes y no descubro nada nuevo o es que no estoy atento a lo que hago y me pienso que no hago nada nuevo, conclusión: NECESITO VACACIONES!!!!

Bueno vamos al tema del post, tenemos una WEB con varios ServerAlias y necesitamos crear un include en función de cada dominio, el problema está en que como hay un sólo Virtual no podemos hacer un alias del tipo:

Alias /includes /mi/directorio/includes/dominio

Para hacer esto nos ayudamos del mod_rewrite y de sus multiples posibilidades:

RewiriteRule ^/includes/(.*) http://%{HTTP_HOST}/includes/%{HTTP_HOST}/$1

Con esto conseguimos que todas las páginas que contengan un /includes/loquesea.htm en función del dominio a través del que accedamos nos presente un contenido u otro.

Esto puede ser útil para personalizar un site en función del dominio manteniendo una sola página con el contenido.