miércoles, 4 de noviembre de 2015

Basic performance tips

Performance tips

  • Native for loops rather than jQuery each:
    • Avoid jQuery.each if you can.
    • Allways cache array length (don't calculate it each iteration)
      var arr = ["1","2","3","4"],
          len = arr.length, // Array length cached
          callback = function(ind,val){};
      
      jQuery.each( arr, callback ); // Slower
      for (;len--;){ callback(); }  // Faster
      while (len--){ callback(); }  // Faster --> while()
      
  • Avoid globals: access to globals is slower. Global scope is a polluted. Use of global variables should be minimized.
    • Cache globals you need, as local variables:
      (function(jq, window){
          // here, access to 'jq' is faster than 'jQuery'
      }(jQuery, window));
      
  • Reduce the times you call jQuery:
    • Cache the jQuery object if you are going to need it more than once:
      var jqDog = jQuery("#dog");
      
  • Chainning actions.
    jQuery("#dog")
        .fadeIn()
        .css()
        .addClass();
    
  • Optimize jQuery selectors:
    • Use ID instead of classes:
      jQuery(".selector"); // Slow
      jQuery("#dog");      // Faster
      
    • Keep them simple:
      jQuery(".selector .selector .selector"); // Slow
      
    • Use find() instead of context:
      jQuery(".selector");              // Slower
      jQuery("div.selector");           // Slow
      jQuery("#dog div.selector");      // Slow
      jQuery(".selector", "#dog");      // Fast (context)
      jQuery("#dog").find(".selector"); // Faster --> context.find()
      
  • Group queries with same callback:
    jQuery('div.close').click( callback );    // Slow
    jQuery('button.close').click( callback ); // Slow
    jQuery('input.close').click( callback );  // Slow
    
    jQuery('div.close, button.close, input.close').click( callback ); // Faster
    
  • Check if element exists:
    if ( jQuery( "#someElement" ).length > 0 ){}
    
  • Reduce reflows. DOM manipulation is costly:
    • Use a single append().
    • Restyle before appended.

Maintenable and reusable Javascript Tips

"There are two ways of constructing a software design: One way is to make it so simple that there are obviously no deficiencies, and the other way is to make it so complicated that there are no obvious deficiencies."
By Charles Antony Richard Hoare.

Programs are meant to be read by humans. We have to communicate each other through code.

The point of having style guidelines is to have a common vocabulary of coding so people can concentrate on what you're saying rather than on how you're saying it. If code you add to a file looks drastically different from the existing code around it, it throws readers out of their rhythm when they go to read it. Avoid this.
  • Line length: Avoid lines longer than 80 characters. The less code on one line, the less likely you will find a merge conflict. When a statement will not fit on a single line, it may be necessary to break it. Place the break after an operator, ideally after a comma. A break after an operator decreases the likelihood that a copy-paste error will be masked by semicolon insertion. The next line should be indented 8 spaces.
  • Avoid Null Comparisons. Comparing a variable against only null typically doesn’t give you enough information about the value to determine whether it’s safe to proceed:
    if( whatever !== null ) {}               // Bad
    if( typeof( whatever ) !== "string" ) {} // Good
    
  • Separate configuration data
  • Validate your code (JsHint, JsLint)
  • Variable declarations: The var statement should be the first statement in the function body (Hoisting). This helps make it clear what variables are included in its scope. It is preferred that each variable be given its own line and comment. They should be listed in alphabetical order if possible:
    var currentEntry, // currently selected table entry
        level,        // indentation level
        size;         // size of table
  • Names:
    • Function names: Do not use _ (underscore) as the first or last character of a name. It is sometimes intended to indicate privacy, but it does not actually provide privacy. If privacy is important, use the forms that provide private members. Avoid conventions that demonstrate a lack of competence.
    • Global variables should be in all caps. (JavaScript does not have macros or constants, so there isn't much point in using all caps to signify features that JavaScript doesn't have.)
  • Opperators: Use the === and !== operators. The == and != operators do type coercion and should not be used.
  • Follow some programming principles:
    • Single responsibility: every function should have responsibility over a single part of the functionality. Loose coupling. Gain reusability. If a function needs more than 30 lines, is a bad sign.
    • Don't repeat yourself.
    • Don't do it, if you aren't going to need it.

Sources:

  • Google Javascript Style Guide (https://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml)
  • jQuery JavaScript Style Guide (https://contribute.jquery.org/style-guide/js/)
  • Dojo JavaScript Style Guide (https://dojotoolkit.org/reference-guide/1.9/developer/styleguide.html)
  • IdiomaticJS (https://github.com/rwaldron/idiomatic.js)
  • "Maintainable JavaScript". Nicholas C. Zakas. (http://shop.oreilly.com/product/0636920025245.do)
  • Douglas Crockford (http://javascript.crockford.com/code.html)

martes, 11 de marzo de 2014

Browser Sync: instalación y configuración

Browser Sync

Se trata de un plugin disponible para Grunt, mediante el cual podremos testear nuestra aplicación de manera simultánea, en múltiples dispositivos y navegadores.


Con él resolvemos la tediosa tarea de testear manualmente nuestra aplicación, navegador por navegador, y dispositivo por dispositivo. Ahora podremos hacerlo en paralelo, propagando eventos (click, touch, scroll, etc) a través de todos los dispositivos y navegadores que tengamos conectados, de tal modo que iremos viendo cómo se comporta la aplicación, a la vez con todos ellos.

¿Cómo instalarlo?

Simplemente debemos añadirlo al package.json del proyecto:

package.json

{
  ...

  "dependencies": {
    ...
  },
  "devDependencies": {
    ...
    "grunt-browser-sync": "~0.6.0",
    ...
  },

  ...
}

Configurar browser_sync en Grunt

Suponiendo que cada uno de los plugins lo configuras en un archivo .js diferente, haciendo uso de load-grunt-config, el archivo correspondiente a grunt-browser-sync, sería browser_sync.js:

browser_sync.js

module.exports = {
    dev: {
        files: {
            src : ['<%= myapp.rutadev %>/css/commons.css']
        },
        options: {
            watchTask: true
        } 
    },
    pro: {
        files: {
            src : ['<%= myapp.rutapro %>/css/commons.min.css']
        }
    }
}

En este ejemplo, le estamos indicando dos configuraciones diferentes: 'dev' y 'pro':

  • Para 'dev', le pedimos que se quede observando los cambios producidos en commons.css, y actualice la página si se detectan. También vemos la opción watchTask: true. Hay que ponerlo si ya tenemos nuestra propia tarea watch corriendo por otro lado.
  • En el caso de 'pro', sin embargo, no hemos añadido watchTask: true, ya que no ejecutaremos 'watch' con dicha tarea.

Todo depende de cómo tengamos nuestra configuración. Es importante por tanto, añadir watchTask: true, si vas a lanzar 'watch' también. Por ejemplo, si fuesemos a lanzar la tarea 'server_dev':

grunt.registerTask('server_dev', [
    'connect:livereload',
    'browser_sync:dev',
    'watch'
]);

Añadir scripts a nuestros HTML

Cuando lancemos la tarea, por consola nos aparecerá la lista de scripts que debemos inyectar en nuestros HTML, para que todos los dispositivos se sincronicen. Algo como esto:




Una vez añadidos los scripts, abrimos la web con dos navegadores diferentes, y comprobamos que se propaguen los eventos de uno a otro. Ahora si hacemos un cambio en commons.css, se verá el efecto en todos a la vez.

¡Feliz testeo!

lunes, 3 de marzo de 2014

Análisis de memoria 101

Este artículo es básicamente una traducción de dos documentos publicados por Google:

Para entenderla mejor, recomendamos leer antes, algo sobre Montículos y Teoría de Grafos.

Dicha traducción pretende servir de antesala para una serie de traducciones posteriores sobre depuración de memoria en Javascript. Agradezco cualquier comentario sobre errores o mejoras que hayáis podido encontrar en dicha traducción.

Análisis de memoria 101

Esta es una introducción al análisis de memoria. Los conceptos e ideas descritas aquí, son usadas en el Heap Profiler UI y en la correspondiente documentación. Debes conocerlos previamente, para poder usar la herramienta de un modo más efectivo.

Términos generales

Esta sección describe conceptos comunes usados en el análisis de memoria, y es aplicable a una variedad de herramientas de depuración de memoria, de diferentes lenguajes. Si ya tienes experiencia con, por ejemplo, depuradores para Java o .Net, es muy probable que te sientas familiarizado con ellas.

El montículo, es una red de objetos interconectados. En el mundo matemático, esta estructura es llamada grafo. Un grafo, es una red de nodos, unidos por vértices. Ambos, nodos y vértices, tienen nombres. Los nombres de los nodos usan el nombre de la función constructora que fue usada para crearlos, y los vértices usan los nombres de sus propiedades.

La secuencia de vértices que se necesita atravesar para alcanzar un objeto a partir de otro, es denominada "ruta". Normalmente, sólo estaremos interesados en rutas simples, por ejemplo, aquellas que no pasan por el mismo nodo dos veces.

Tamaño de objetos: shallow size y retained size

Piensa en la memoria como un grafo con tipos primitivos (números y cadenas), y objetos (arrays asociativos). Esto puede ser respresentado en una gráfica con un determinado número de nodos, que serían los objetos, interconectados por vértices, como hemos visto anteriormente.

La memoria puede ser utilizada de dos modos:

  • directamente por un objeto, 
  • o implícitamente almacenando referencias a otros objetos, y así, evitando que sean eliminados automáticamente con el recolector de basura (en adelante GC, por sus siglas en inglés de Garbage Collector).

El tamaño de la memoria que es utilizado por un objeto, es llamado "shallow size" (tamaño trivial o efímero). Los objetos propios de JavaScript tienen cierta memoria reservada para sus descripciones y para almacenar sus valores inmediatos.

Normalmente, sólo los arrays y strings, pueden necesitar un mayor tamaño: un mayor shallow size. De cualquier forma, los strings tienen sus propio lugar de almacenamiento en la memoria de renderizado, con lo cual, sólo exponen un pequeño wrapper object en el montículo JavaScript.

De cualquier forma, incluso un objeto pequeño puede requerir una gran cantidad de memoria de manera indirecta, al evitar que otros objetos sean eliminados por el GC. El tamaño de memoria que sería liberado si un objeto, y los que dependen de él, es liberado al borrarse, se denomita "retained size".

Cuando trabajamos con el Heap Profiler (depurador de montículos) en las DevTools de Google Chrome, verás varias columnas de información. Dos de ellas representan el shallow size y el retained size respectivamente.

Rutas retenidas de objetos (retaining paths)

Llamaremos "ruta retenida" (retaining path), a cualquier ruta desde la raíz del recolector de basura (GC) hasta un objeto en particular. Si no existe ruta alguna para poder llegar a un objeto, entonces ese objeto es llamado "inalcanzable", y se pone a disposición del GC para que sea eliminado.

Nuestras rutas empezarán siempre desde la raíz del GC. Existen muchas raíces internas del GC, las cuales no nos interesan demasiado. Como punto de partida para las aplicaciones, tenemos:

  • El objeto global Window, uno para cada iframe. Hay un campo "distancia" en los heap snapshots, que representan el número de propiedades referenciadas desde la raíz de window, yendo por el retaining path más corto.
  • El árbol del DOM, que consiste en todos los nodos alcanzables desde la raíz. No todos pueden tener wrapper objects asociados, pero en caso de que los tengan, dichos nodos permanecerán en memoria mientras permanezca el documento.
  • Objetos retenidos por el depurador. Algunas veces, los objetos pueden permanecer retenidos por el depurador, tras una evaluación en la consola. Por ello es recomendable realizar heap snapshots con la consola limpia y sin breakpoints en el depurador.

Una de las cosas a tener en cuenta en el depurador, es la distancia desde la raíz del GC. Si casi todos los objetos del mismo tipo, se encuentran a la misma distancia, y sólo unos pocos están a mayor distancia, es algo que susceptible de ser investigado.

Dominadores (Dominators)

El dominador de un objeto A, es otro objeto que existe en cada ruta simple desde la raíz del GC a dicho objeto A. Esto significa, que si eliminamos del montículo el dominador de un objeto, (junto a todas las demás referencias), el objeto A será inalcanzable, y por tanto, eliminado.

Los objetos dominadores tienen una estructura de árbol, ya que todo objeto tiene exactamente un dominador. El dominador de un objeto puede carecer de referencias directas al objeto que domina, es decir, el árbol de dominación no es una estructura que se expande.

Objetos a modo de colecciones, pueden retener una gran cantidad de memoria, cuando dominan otros objetos. Cada nodo del árbol es llamado "punto de acumulación" (accumulation point).

Particularidades del V8

En esta sección describiremos algunos temas relacionados con la memoria, que sólo corresponden a la máquina virtual V8 de Javascript. Su lectura debe ayudar a comprender por qué las capturas de montículos (heap snapshots), tienen dicha apariencia.

Representación de objetos en Javascript

Los números pueden ser presentados tanto como valores inmediatos enteros de 31 bits (small integers), o como objetos de un montículo (heap objects). Éstos últimos, son ulitizados por valores que no encajan dentro de los small integers, como por ejemplo: doubles, o para los casos en los que un valor necesita ser encapsulado, por ejemplo, al establecer propiedades en él.

El contenido de los strings, puede ser almacenado tanto en el montículo de la máquina virtual V8, como externamente, en la memoria de renderizado. El contenido recibido desde la Web (por ejemplo, desde un script), no es copiado dentro del montículo de la máquina virtual, y en su lugar, se crea un wrapper object, para acceder al contenido exterior.

Cuando dos cadenas son concatenadas, sus contenidos son inicialmente almacenados de forma separada, y sólo son unidas de manera lógica, usando un objeto llamado cons string. La unión de los contenidos de los cons strings, es ejecutado sólo cuando se precisa, por ejemplo, cuando necesitamos construir una subcadena resultado de la unión.

Los arrays son usados muy frecuentemente en la V8 para almacenar gran cantidad de datos. Los diccionarios (conjunto de pares llave-valor), son guardados en arrays. Por tanto, los arrays son el bloque de construcción básico de los objetos Javascript. Un objeto en Javascript posee dos arrays: uno para almacenar los nombres de las propiedades, y el otro para almecenar el elementos numéricos. Si el número de propiedades de un objeto es muy pequeño, entonces pueden ser almacenadas internamente en el propio objeto Javascript.

Un map object, describe el tipo de objeto y su estructura. Por ejemplo, dichos objetos se usan para describir las jerarquías implícitas de un objeto, como se describe aquí (enlace externo en inglés).

Grupos de objetos

Cada grupo de objeto nativo se crea a partir de objetos que mantienen una referencia mutua, el uno al otro. Si consideramos por ejemplo un subárbol del DOM: cada uno de sus nodos tiene un enlace a su padre, y enlaces al siguiente hijo y siguiente hermano, lo cual conforma un grafo. Aclarar que los objetos nativos (String, Number, Boolean, Array, Date, Math y RegExp) no son representados en el montículo Javascript - esta es la razón por la que tienen un tamaño igual a 0. En lugar de eso, son creados varios wrapper objects. Cada wrapper object, guarda una referencia de su correspondiente objeto nativo, para órdenes de redirección sobre él. Por lo tanto, un grupo de objetos almacena varios wrapper objects. De cualquier forma, esto no crea un ciclo no recolectable, ya que el GC es lo suficientemente inteligente como para liberar grupos de objetos cuyos wrappers no son referenciados por nadie. Sin embargo, si durante el proceso olvida liberar un simple wrapper, mantendrá a todo el grupo de objetos y sus respectivos wrappers.

¿Qué son los wrapper objects en javascript?

Esta es una traducción del libro "Javascript, The Definitive Guide", Ed. 6th, escrito en inglés por David Flanagan:

Wrapper Objects

Los objetos en JavaScript, tienen valores compuestos: son una colección de propiedades, o nombres de valores. Nos referimos al valor de una propiedad, usando la notación ".". Cuando el valor de una propiedad es una función, lo llamamos método. Para invocar el método m del objeto o, escribimos o.m()

Sabemos que los strings también tienen propiedades y métodos:

var s = "Hola mundo"; // Un string
var word = s.substring(s.indexOf(" ")+1, s.length); // Uso de las propiedades (métodos) del objeto String.

Los strings, en Javascript son valores primitivos, al igual que los números y booleanos, y por tanto, no son objetos. ¿Por qué entonces tienen propiedades, como los objetos? Cada vez que intentamos referirnos a una propiedad del string s, JavaScript convierte el valor real de dicho string en un objeto, como si lo hubiésemos declarado con el constructuor new String(s). Dicho objeto, hereda métodos, y es usado para resolver las referencias a sus propiedades (métodos). Una vez que el valor de la propiedad (método) ha sido resuelto, el nuevo objeto es declarado.

Los números y booleanos, tienen métodos por la misma razón que las cadenas: un objeto temporal se crea cuando los declaras, usando los constructores Number() o Boolean(), y el método que invoquemos, es resuelto usando dicho objeto temporal. A estos objetos temporales que envuelven a los valores primitivos, son llamados Wrapper Objects. No hay wrapper objects para null ni undefined: cada vez que intentemos acceder a una propiedad o método de éstos, nos devolverá un TypeError.

Para esclarecer un poco lo dicho anteriormente, supongamos el siguiente código, y piensa qué sucederá cuando sea ejecutado:

var s = "test";
s.len = 4;
var t = s.len;

Nada más ejecutar el código, el valor de t es undefined. En la segunda línea de código, se crea un objeto String, de modo temporal: un wrapper object, a partir de s, y le añadimos la propiedad len, con valor 4, y después, eliminamos dicho objeto temporal. En la tercera línea volvemos a crear un nuevo objeto temporal, a partir del original s (no modificado), e intentamos leer la propiedad len. Pero no existe, porque es un objeto temporal recién creado, y por tanto, la expresión se resuelve como undefined. Por tanto, el valor de t al final de la ejecución, será undefined. Este código demuestra que los valores primitivos (strings, números y cadenas), se comportan como objetos cuando intentas acceder a sus propiedades o métodos, pero si intentas especificar el valor de una de sus propiedades, dicho intento es ignorado, ya que el cambio es realizado sobre un objeto temporal, o wrapper object. Es decir, sus propiedades son de sólo lectura: inmutables.

lunes, 16 de diciembre de 2013

Testeo automático con Phantom.js

Introducción

Esta artículo pretende explicar cómo Phanjom.js puede ayudarnos a comprobar de manera automática, la adaptabilidad de nuestra aplicación web a diversas resoluciones de pantalla, generando capturas de las páginas HTML de nuestra aplicación.

¿Qué es Phantom.js?

PhantomJS es un navegador sin interfaz gráfica basado en Webkit, es decir, no nos sirve para testear nuestra aplicación con IE ni Firefox, y sólo podemos manejarlo con un terminal, o la consola MSDOS, usando un API Javascript.

¿Qué podemos hacer con Phantom.js?

  • Lanzar test unitarios con Jasmine, QUnit, Mocha, y muchos otros.
  • Simular acciones del usuario, antes de realizar la captura de pantalla: click, mouseover, keypress, etc...
  • Podemos manejar el DOM con librerías con jQuery, si lo necesitamos para testear nuestras páginas.
  • Automatizar la eficiencia de nuestras páginas con YSlow y Jenkins

Prepara el entorno

Lo único que necesitamos es un servidor web, ya sea Apache, Nginx o Node.js, con el que servir los estáticos de nuestra aplicación. Si usamos Grunt, podemos lanzar la tarea grunt-connect, o simplemente podemos crearnos un script para lanzar un servidor web bajo Node.js.

Añadir Express a package.json

Para que el anterior script funcione, será necesario que instaléis antes Express, como un módulo de Node.js. Para ello, simplemente editamos nuestro archivo package.json y añadimos la dependencia:

{
  "name": "hello-world",
  "description": "hello world test app",
  "version": "0.0.1",
  "private": true,
  "dependencies": {
    "express": "3.x"
  }
}

De este modo, una vez añadida, para instalarla ejecutamos:

npm install

Arrancar el servidor Node

El siguiente código de ejemplo, nos sirve para arrancar un servidor Node en el puerto 3000, y suponiendo que los estáticos que queremos testear están en el directorio /dev, y dentro de él, que nos redireccione a /html/index.html. Este código lo guardamos como web.js, por ejemplo:

var express = require( "express" ),
    app = express(),
    port = process.env.PORT || 3000;

process.env.PWD = process.cwd()

app.configure(function(){

    // Directorio donde estará la raíz de nuestro servidor
    app.use(express.static( process.env.PWD + '/dev' ));
    app.use(express.bodyParser());
    app.use(express.methodOverride());

    //Error Handling
    app.use(express.logger());
    app.use(express.errorHandler({
            dumpExceptions: true, 
            showStack: true
    }));

    app.use(app.router);
});

// Redireccionamos a /dev/html/index.html
app.get('/', function(req, res){
    res.redirect( "/html/index.html" );
});

app.listen(port, function() {
    console.log( "Listening on " + port );
});

Para hacerlo funcionar, lo ejecutamos desde la consola:

node web

Añadiendo Phantom.js al PATH

Lo primero que debemos hacer es, obviamente, descargarnos Phantom.js. Una vez descargado, lo guardamos en el directorio C:/phantomjs

Después editamos la variable de entorno PATH, y añadimos dicha ruta, para que phantomjs.exe, esté disponible desde la consola de MSDOS, o terminal. Es posible que tengas que reiniciar para que la variable de entorno la coja bien Windows.

Definiendo los test y capturas de pantalla a realizar con Phantom.js

Como Phantom.js se maneja sólo por comandos, si queremos simular la acción del usuario, o capturar pantallas, lo siguiente que debemos hacer es crearnos un archivo javascript con el que definir las acciones a realizar con dicho navegador. Este código lo guardamos con el nombre phantomtest.js (por ejemplo):

(function(){

    "no strict";

    var resourceWait  = 300,
        maxRenderWait = 10000,
        forcedRenderTimeout,
        clickTimeout,
        renderTimeout,
        url  = 'http://localhost:3000/html/index.html',
        page = require('webpage').create();

    page.onConsoleMessage = function (msg, line, source) {
        console.log( 'console> ' + msg );
    };

    page.onAlert = function (msg) {
        console.log( 'alert!!> ' + msg );
    };

    page.onResourceRequested = function (req) {
        //console.log( 'request> ' + req.id + ' - ' + req.url );
        clearTimeout(renderTimeout);
    };
     
    page.onResourceReceived = function (res) {
        if (!res.stage || res.stage === 'end') {
            console.log( 'loaded> ' + res.id + ' ' + res.status + ' - ' + res.url);
        }
    };

    page.open(url, function (status) {

        if (status !== 'success') {
            console.log('Error al cargar la URL: ' + url);
        } else {

            forcedRenderTimeout = setTimeout(function () {

                page.viewportSize = { width: 1024, height : 512 };
                page.render( 'test-1024.png' );

                page.viewportSize = { width: 768, height : 512 };
                page.render( 'test-768.png' );

                page.viewportSize = { width: 320, height : 512 };
                page.render( 'test-320.png' );

                phantom.exit();

            }, maxRenderWait);
        }     
    });

}());

Ejecutamos los test con Phantom.js

Una vez que tengamos nuestro servidor funcionando en el puerto 3000, sólo nos queda ejecutar phantomtest.js con el navegador. Para hacerlo funcionar, iniciamos otra consola nueva, y escribimos:

phantomjs phantomtest

Lo que sucede a continuación, es que Phantom.js va a ejecutar las acciones que le hayamos indicado en phantomtest.js: tendremos 3 capturas de pantalla, a diferentes resoluciones, de una de las páginas de nuestra aplicación. Si quisiésemos simular acciones de usuario, o modificar el DOM, podemos usar jQuery. En dicho caso, las acciones las debemos englobar dentro de page.evaluate. Nuestro page.open quedaría ahora así:

page.open(url, function (status) {

        if (status !== 'success') {
            console.log('Error al cargar la URL: ' + url);
        } else {

            forcedRenderTimeout = setTimeout(function () {

                page.viewportSize = { width: 1024, height : 512 };
                page.evaluate(function () {
                    var $elm = $("#test3");
                    if( $elm.length > 0 ) {
                        $elm.trigger("click");
                    }
                });
                page.render( 'test-1024-click.png' );

                page.viewportSize = { width: 768, height : 512 };
                page.render( 'test-768.png' );

                page.viewportSize = { width: 320, height : 512 };
                page.render( 'test-320.png' );

                phantom.exit();

            }, maxRenderWait);
        }     
    });

De este modo hemos simulado, antes de hacer la captura de pantalla, que el usuario ha hecho click en el elemento cuyo id es #test3.

Ejemplo de código

Si quieres ver todo junto funcionando, puedes encontrar el código al que hago referencia en este artículo, aquí: https://github.com/alfonsomartinde/prettychecks.

martes, 10 de diciembre de 2013

Deploy de una app hecha con Grunt y Bower, desde Cloud9, en Heroku

¿Qué es Heroku?

Heroku es una plataforma como servicio (PaaS) en la nube, que nos permite arrancar un entorno de desarrollo para el lenguaje que queramos, de un modo transparente, y sin tener que perder tiempo por nuestra parte en configurar nada en nuestro sistema local. Podríamos decir que es un entorno de desarrollo "llave en mano". Al crear una aplicación en Heroku, éste nos ofrece un entorno sobre Debian, preparado ya para programar con ruby, php, java, node, o el lenguaje que necesitemos.

Nos ofrece también un repositorio Git, donde alojar nuestros fuentes. Cada vez que hagamos un push, Heroku compilará nuestro proyecto, y un servidor web al que podremos acceder para ver nuestra aplicación online: http://nuestraapp.herokuapp.com/

Para este pequeño "manual", voy a suponer que ya estás familiarizado con el desarrollo de aplicaciones con Grunt, Less, Jade, etc. Por tanto, entiendo que tu aplicación ya tiene un archivo package.json. Lo que voy a explicar aquí, es cómo modificarla para poder hacer deploys en Heroku, desde Cloud9:

Instalar el motor Node

Para que Node funcione correctamente en Heroku, debemos especificar en package.json que vamos a usar Node.js junto con su versión: algo que no era necesario para compilar en local, pero sí lo es en Heroku. Para ello, debemos añadir la propiedad "engines" a nuestro package.json:

"engines": {
     "node": "0.10.x"
}

No es necesario especificar que necesitaremos npm, ni su versión, ya que npm viene en el lote del motor Node.

Instalar Express, Grunt y Bower en nuestra app de Heroku

Por defecto, cuando compilamos una aplicación en Heroku, ésta se compila en modo producción, por lo tanto, lo que pongamos en "devDependencies" de nuestro package.json será ignorado. Llegados a este punto podemos hacer dos cosas:

  • o cambiamos el entorno a modo desarrollo, de tal modo que se instalen las dependencias definidas en "devDependencies",
  • o especificamos en "dependencies" las dependencias para producción.

Personalmente prefiero esta última opción, es decir, usar el entorno de producción en Heroku. Por tanto, copiamos los paquetes de "devDependencies" a "dependencies", y además, le añadimos también algunos paquetes que por defecto en Heroku no están instalados: express, bower, grunt y grunt-cli. Nuestro package.json quedaría así (por ejemplo):

"dependencies": {
  "express": "~3.3.4",
  "bower": "latest",
  "grunt": "latest",
  "grunt-cli": "latest",
  ...
  resto de paquetes
  ...
},
"devDependencies": {
  "grunt": "latest",
  ...
  resto de paquetes
  ...
}

Ejecutar Bower install en Heroku

Si en nuestra app hemos usado Bower, necesitaremos ejecutar el comando 'bower install' para instalar los repositorios al compilar. Para ello, añadimos la instrucción al bloque "scripts" en package.json:

"scripts": {
    "postinstall": "./node_modules/bower/bin/bower install"
  }

Crear el servidor de archivos estáticos

Imaginemos que nuestra app tiene la siguiente estructura:

/app  --> Archivos fuente
  |--/fonts
  |--/img
  |--/jade
  |--/js
  |--/less
  \--/test
/dev  --> Compilación de desarrollo
  |--/css
  |--/fonts
  |--/html
  |--/img
  |--/js
  \--/test
/pro  --> Compilación de producción (concatenada, minificada y ofuscada)
  |--/css
  |--/fonts
  |--/html
  |--/img
  |--/js
  \--/test

Por defecto, Heroku no nos crea un servidor HTTP. Debemos crearlo nosotros con Node. Para ello nos creamos un archivo javascript al que llamaremos por ejemplo "web.js".

Suponiendo que hemos configurado nuestra app, para que compile los estáticos en la carpeta /dev, y dentro de ella nuestro archivo index se encuentra en html/index.html, este sería nuestro archivo "web.js" para arrancar un servidor de estáticos en Heroku:

web.js

var express = require("express"),
    app = express.createServer(),
    port = process.env.PORT || 3000;

process.env.PWD = process.cwd();

app.configure(function(){
    app.use(express.static(process.env.PWD + '/dev'));
    app.use(express.bodyParser());
    app.use(express.methodOverride());

    //Error Handling
    app.use(express.logger());
    app.use(express.errorHandler({
        dumpExceptions: true, 
        showStack: true
    }));

    app.use(app.router);
});

app.get('/', function(req, res){
    res.redirect("html/index.html");
    res.render("index.html");
});

app.listen(port, function() {
    console.log("Listening on " + port + "\nDirectory: " + process.env.PWD + "/dev");
});

Ahora nos falta decirle a Heroku que ejecute dicho archivo. Para ello debemos crearnos un archivo llamado Procfile. Esto lo explico en el siguiente apartado.

Crear el archivo Procfile

Una vez configurado correctamente package.json, nos creamos un archivo nuevo llamado Procfile, en el que indicamos a Heroku lo que ejecutar al realizar el deploy. Es aquí debemos decirle a Heroku que arranque el servidor web, para que sirva los archivos estáticos generados previamente con grunt. Este sería nuestro Procfile:

web: node web.js

Instalar cliente Heroku en Cloud9

Para poder ejecutar comandos de Heroku, y manejar nuestro entorno desde la terminal de Cloud9, necesitamos instalar el cliente. Para ello escribimos, en el terminal de Cloud9:

wget http://assets.heroku.com/heroku-client/heroku-client.tgz
tar xzfv heroku-client.tgz
cd heroku-client/bin
PATH=$PATH:$PWD

Acceso al bash de Heroku desde Cloud9

Una vez instalado el cliente, desde el terminal de Cloud9 tendremos acceso al bash Heroku. Para ello abrimos un terminal de Cloud9, y escribimos:

heroku run bash -a nombre-de-nuestra-app

Configuramos el Buildpack

Cuando hacemos un git push a nuestro repositorio de Heroku, éste prepara los archivos para ser compilados. El conjunto de acciones que se realizan para compliar, se define en un Buildpack. Heroku dispone de varios Buildpacks, dependiendo del lenguaje y lo que queramos hacer. Sin embargo en este caso, necesitamos un BUILDPACK "no oficial", para que de soporte a Grunt. Para instalar dicho buildpack, escribimos en el terminal:

heroku config:add BUILDPACK_URL=https://github.com/mbuchetics/heroku-buildpack-nodejs-grunt.git -a nombre-de-nuestra-app

En este momento se compilarán nuestros archivos, tal y como lo hubiéramos compilado en Gruntfile.js, y tendremos acceso a ellos desde el servidor web de estáticos, en la URL: http://nombredenuestraapp.herokuapp.com

Añadir deploy en Cloud9

En este apartado veremos cómo lanzar un deploy desde Cloud9:

Después elegimos el servicio Heroku.com:

Configurar el entorno a "development" (opcional)

Este paso es opcional, y explica cómo proceder si quisiéramos cambiar el entorno a desarrollo, en vez de producción que es como lo tiene Heroku por defecto. Desde la terminal de Cloud9 escribimos:

heroku config:set NODE_ENV=development -a nombre-de-nuestra-app
heroku labs:enable user-env-compile -a nombre-de-nuestra-app

Más información

viernes, 26 de julio de 2013

Validación de NIE, NIF y CIF con jQuery Validator

Introducción

El plugin jQuery Validation, fue escrito y es mantenido por Jörn Zaefferer, miembro del equipo jQuery, desarrollador jefe del equipo jQuery UI, y uno de los responsables del mantenimiento de QUnit. Lo que dicho plugin nos ofrece, es validación de formularios en cliente, con una alta capacidad de personalización.

Una de las ventajas, es que nosotros mismos podemos extender la clase y añadir nuestros propios métodos a la librería. Esto lo hacemos creando e importando un .js con nuestros métodos de validación, justo después de hacer la llamada al plugin.

¿Cómo añadir un nuevo método?

El formato que debe tener dicho js, para añadir un nuevo método es el siguiente:

;(function($) {

 jQuery.validator.addMethod(
  "NOMBRE_REGLA", 
  function(value, element) {
   
   return false;
  },
  "MENSAJE DE ERROR"
 );
}(jQuery));

Validación de NIF, NIE y CIF:

Volviendo al tema inicial, así quedaría nuestro archivo js para validar NIF, NIE y CIF:

jQuery.validator.addMethod( "nifES", function ( value, element ) {
 "use strict";

 value = value.toUpperCase();

 // Basic format test 
 if ( !value.match('((^[A-Z]{1}[0-9]{7}[A-Z0-9]{1}$|^[T]{1}[A-Z0-9]{8}$)|^[0-9]{8}[A-Z]{1}$)') ) {
  return false;
 }

 // Test NIF
 if ( /^[0-9]{8}[A-Z]{1}$/.test( value ) ) {
  return ( "TRWAGMYFPDXBNJZSQVHLCKE".charAt( value.substring( 8, 0 ) % 23 ) === value.charAt( 8 ) );
 }
 // Test specials NIF (starts with K, L or M)
 if ( /^[KLM]{1}/.test( value ) ) {
  return ( value[ 8 ] === String.fromCharCode( 64 ) );
 }

 return false;

}, "Please specify a valid NIF number." );


jQuery.validator.addMethod( "nieES", function ( value, element ) {
 "use strict";

 value = value.toUpperCase();

 // Basic format test 
 if ( !value.match('((^[A-Z]{1}[0-9]{7}[A-Z0-9]{1}$|^[T]{1}[A-Z0-9]{8}$)|^[0-9]{8}[A-Z]{1}$)') ) {
  return false;
 }

 // Test NIE
 //T
 if ( /^[T]{1}/.test( value ) ) {
  return ( value[ 8 ] === /^[T]{1}[A-Z0-9]{8}$/.test( value ) );
 }

 //XYZ
 if ( /^[XYZ]{1}/.test( value ) ) {
  return ( 
   value[ 8 ] === "TRWAGMYFPDXBNJZSQVHLCKE".charAt( 
    value.replace( 'X', '0' )
     .replace( 'Y', '1' )
     .replace( 'Z', '2' )
     .substring( 0, 8 ) % 23 
   ) 
  );
 }

 return false;

}, "Please specify a valid NIE number." );



jQuery.validator.addMethod( "cifES", function ( value, element ) {
 "use strict";
 
 var sum,
  num = [],
  controlDigit;
 
 value = value.toUpperCase();
 
 // Basic format test 
 if ( !value.match( '((^[A-Z]{1}[0-9]{7}[A-Z0-9]{1}$|^[T]{1}[A-Z0-9]{8}$)|^[0-9]{8}[A-Z]{1}$)' ) ) {
  return false;
 }
 
 for ( var i = 0; i < 9; i++ ) {
  num[ i ] = parseInt( value.charAt( i ), 10 );
 }
 
 // Algorithm for checking CIF codes
 sum = num[ 2 ] + num[ 4 ] + num[ 6 ];
 for ( var count = 1; count < 8; count += 2 ) {
  var tmp = ( 2 * num[ count ] ).toString(),
   secondDigit = tmp.charAt( 1 );
  
  sum += parseInt( tmp.charAt( 0 ), 10 ) + ( secondDigit === '' ? 0 : parseInt( secondDigit, 10 ) );
 }
 
 // CIF test
 if ( /^[ABCDEFGHJNPQRSUVW]{1}/.test( value ) ) {
  sum += '';
  controlDigit = 10 - parseInt( sum.charAt( sum.length - 1 ), 10 );
  value += controlDigit;
  return ( num[ 8 ].toString() === String.fromCharCode( 64 + controlDigit ) || num[ 8 ].toString() === value.charAt( value.length - 1 ) );
 }
 
 return false;
 
}, "Please specify a valid CIF number." );

Test

He creado unos sencillos test con QUnit para comprobar el correcto funcionamiento de dichos archivos, dada la importancia de dichos valores: no es nada deseable que un usuario introduzca un NIF, o CIF inválido, y el sistema no lo detecte. Los resultados han sido satisfactorios:

A continuación os dejo el código de los test:

;(function($) {

function methodTest( methodName ) {
 var v = jQuery("#form").validate();
 var method = $.validator.methods[methodName];
 var element = $("#test")[0];
 return function(value, param) {
  element.value = value;
  return method.call( v, value, element, param );
 };
}

module("methods");

test("nifES", function() {
 var method = methodTest("nifES");
 ok( method( "11441059P" ), "NIF valid" );
 ok( method( "80054306T" ), "NIF valid" );
 ok( method( "76048581R" ), "NIF valid" );
 ok( method( "28950849J" ), "NIF valid" );
 ok( method( "34048598L" ), "NIF valid" );
 ok( method( "28311529R" ), "NIF valid" );
 ok( method( "34673804Q" ), "NIF valid" );
 ok( method( "92133247P" ), "NIF valid" );
 ok( method( "77149717N" ), "NIF valid" );
 ok( method( "15762034L" ), "NIF valid" );
 ok( method( "05122654W" ), "NIF valid" );
 ok( method( "05122654w" ), "NIF valid: lower case" );
 ok(!method( "1144105R" ), "NIF invalid: less than 8 digits without zero" );
 ok(!method( "11441059 R" ), "NIF invalid: white space" );
 ok(!method( "11441059" ), "NIF invalid: no letter" );
 ok(!method( "11441059PR" ), "NIF invalid: two letters" );
 ok(!method( "11440059R" ), "NIF invalid: wrong number" );
 ok(!method( "11441059S" ), "NIF invalid: wrong letter" );
 ok(!method( "114410598R" ), "NIF invalid: > 8 digits" );
 ok(!method( "11441059-R" ), "NIF invalid: dash" );
 ok(!method( "asdasdasd" ), "NIF invalid: all letters" );
 ok(!method( "11.144.059R" ), "NIF invalid: two dots" );
 ok(!method( "05.122.654R" ), "NIF invalid: starts with 0 and dots" );
 ok(!method( "5.122.654-R" ), "NIF invalid:  dots and dash" );
 ok(!method( "05.122.654-R" ), "NIF invalid: starts with zero and dot and dash" );
});

test("nieES", function() {
 var method = methodTest("nieES");
 ok( method( "X0093999K" ), "NIE valid" );
 ok( method( "X1923000Q" ), "NIE valid" );
 ok( method( "Z9669587R" ), "NIE valid" );
 ok( method( "Z8945005B" ), "NIE valid" );
 ok( method( "Z6663465W" ), "NIE valid" );
 ok( method( "Y7875935J" ), "NIE valid" );
 ok( method( "X3390130E" ), "NIE valid" );
 ok( method( "Y7699182S" ), "NIE valid" );
 ok( method( "Y1524243R" ), "NIE valid" );
 ok( method( "X3744072V" ), "NIE valid" );
 ok( method( "X7436800A" ), "NIE valid" );
 ok( method( "y7875935j" ), "NIE valid: lower case" );
 ok(!method( "X0093999 K" ), "NIE inválido: white space" );
 ok(!method( "X 0093999 K" ), "NIE inválido:  white space" );
 ok(!method( "11441059" ), "NIE inválido: no letter" );
 ok(!method( "11441059PR" ), "NIE inválido: two letters" );
 ok(!method( "11440059R" ), "NIE inválido: wrong number" );
 ok(!method( "11441059S" ), "NIE inválido: wrong letter" );
 ok(!method( "114410598R" ), "NIE inválido: > 8 digits" );
 ok(!method( "11441059-R" ), "NIE inválido: dash" );
 ok(!method( "asdasdasd" ), "NIE inválido: all letters" );
 ok(!method( "11.144.059R" ), "NIE inválido: two dots" );
 ok(!method( "05.122.654R" ), "NIE inválido: starts with 0 and dots" );
 ok(!method( "5.122.654-R" ), "NIE inválido: dots and dash" );
 ok(!method( "05.122.654-R" ), "NIE inválido: starts with zero and dot and dash" );
});

test("cifES", function() {
 var method = methodTest("cifES");
 ok( method( "A79082244" ), "CIF valid" );
 ok( method( "A60917978" ), "CIF valid" );
 ok( method( "A39000013" ), "CIF valid" );
 ok( method( "B43522192" ), "CIF valid" );
 ok( method( "B38624334" ), "CIF valid" );
 ok( method( "G72102064" ), "CIF valid" );
 ok( method( "F41190612" ), "CIF valid" );
 ok( method( "J85081081" ), "CIF valid" );
 ok( method( "S98038813" ), "CIF valid" );
 ok( method( "G32937757" ), "CIF valid" );
 ok( method( "B46125746" ), "CIF valid" );
 ok( method( "C27827559" ), "CIF valid" );
 ok( method( "E48911572" ), "CIF valid" );
 ok( method( "s98038813" ), "CIF valid: lower case" );
 ok(!method( "K48911572" ), "CIF invalid: starts with K" );
 ok(!method( "L48911572" ), "CIF invalid: starts with L" );
 ok(!method( "M48911572" ), "CIF invalid: starts with M" );
 ok(!method( "X48911572" ), "CIF invalid: starts with X" );
 ok(!method( "Y48911572" ), "CIF invalid: starts with Y" );
 ok(!method( "Z48911572" ), "CIF invalid: starts with Z" );
 ok(!method( "M15661515" ), "CIF invalid" );
 ok(!method( "Z98038813" ), "CIF invalid: wrong letter" );
 ok(!method( "B 43522192" ), "CIF invalid: white spaces" );
 ok(!method( "43522192" ), "CIF invalid: missing letter" );
 ok(!method( "BB43522192" ), "CIF invalid: two letters" );
 ok(!method( "B53522192" ), "CIF invalid: wrong number" );
 ok(!method( "B433522192" ), "CIF invalid: > 8 digits" );
 ok(!method( "B3522192" ), "CIF invalid: < 8 digits" );
 ok(!method( "B-43522192" ), "CIF invalid: dash" );
 ok(!method( "Basdasdas" ), "CIF invalid: all letters" );
 ok(!method( "B43.522.192" ), "CIF invalid: dots" );
 ok(!method( "B-43.522.192" ), "CIF invalid: dots and dash" );
});

})(jQuery);