Mostrando entradas con la etiqueta js. Mostrar todas las entradas
Mostrando entradas con la etiqueta js. Mostrar todas las entradas

2011/11/14

Colocando el cursor con javascript

Seguro que alguna vez hemos querido colocar el cursor de un cuadro de texto o un textarea para cualquier cosa que se nos haya ocurrido, la pega es que no hay métodos nativos para ello, pero jugando un poco con los rangos de selección, es fácil de hacer.

A continuación os pongo dos pequeñas funciones para conseguir el setter/getter de la posición del cursor. Es una "tontá", pero es efectiva. Además si lo minimizas ocupa una miseria.

function getCursorPosition( elem ) {

    var CaretPos = 0;
    // IE Support
    if( document.selection ) {

        elem.focus();
        var Sel = document.selection.createRange();

        Sel.moveStart( 'character', -elem.value.length );

        CaretPos = Sel.text.length;
    }
    // Firefox support
    else if ( elem.selectionStart || elem.selectionStart == '0' )
        CaretPos = elem.selectionStart;

    return( CaretPos );
}


function setCursorPosition( elem, pos )
{
    if( elem.setSelectionRange )
    {
        elem.focus();
        elem.setSelectionRange( pos,pos );
    }
    else if( elem.createTextRange ) {
        var range = elem.createTextRange();
        range.collapse( true );
        range.moveEnd( 'character', pos );
        range.moveStart( 'character', pos );
        range.select();
    }
}

2011/07/02

Plugin de pasos de wizard

Inevitable como las mareas... si hace unos días os presentaba los CSSs para generar pasos de wizard, ahora llega el plugin que automatiza la asignación de estilos para los vagos como yo.

Como es habitual, el código con un ejemplo funcionando, en mi jsFiddle


Y ahora el código completo del plugin
/**
 * jquery.steps-0.1,js - Steps plugin for jQuery
 *  ================================================
 *  (C) 2011 José Ramón Díaz - jrdiazweb@gmail.com
 * 
 * Instantiation: $('.stepsDiv').steps()
 * 
 * Functions:
 *     $('.stepsDiv').steps('getStep') - Returns current step for stepDiv
 *     $('.stepsDiv').steps('start')   - Resets state to initial step
 *     $('.stepsDiv').steps('finish')  - Sets state to completed
 *     $('.stepsDiv').steps('next')    - Sets state to next state
 *     $('.stepsDiv').steps('prev')    - Sets state to previous state
 *     
 * Classes needed and meaning
 *     N/A              - Uncompleted step. Greyed by default
 *     end              - Last uncompleted step. Greyed and no "arrow"
 *     last             - Boundary class. Marks last completed step and last step
 *     current          - Current step
 *     completed        - Completed step
 *     completedLast    - Last completed step
 *     completedLastEnd - Last step when all steps are completed
 * 
 * Extending steps with representation
 *     You can also use the steps CSS. You can find it at
 *     http://3nibbles.blogspot.com/2011/06/pasos-de-wizard.html
 *     
 * Legal stuff
 *     You are free to use this CSS, but you must give credit or keep header intact.
 *     Please, tell me if you find it useful. An email will be enough.
 *     If you enhance this code or correct a bug, please, tell me.
 * 
 */
(function( $ ){

 $.fn.steps = function( options ) {  
  
  var self = this;
  
  
  //////////////////////////////////////////////////////////////////////////////////
  // DEFAULT VALUES
  var defaults = {};

  //////////////////////////////////////////////////////////////////////////////////
  // PRIVATE FUNCTIONS
  var init = function( options ) {
   
   // If options exist, merge them with default settings
   if ( options ) $.extend( defaults, options );
  
   var $this = $(this);
   var data = $this.data('steps');
   
   return this.each(function() {
    
    var $this = $(this);
    data      = $this.data('steps');
    
    // The plugin hasn't been initialized yet
    if ( ! data ) {
     
     // Initializes the plugin data
     $(this).data('steps', { 
      target : $this,
      step   : 0
     });
 
    };
   });   
   
  };
  
  var destroy = function( ) { 
   return this.each(function() { self.removeData('steps'); })
  };

  //////////////////////////////////////////////////////////////////////////////////
  // PUBLIC FUNCTIONS
  var methods = {
    getStep: function( ) { return self.data('steps')['step']; },
    start:   function( ) { return methods.setStep(0); },
    finish:  function( ) { 
     var l = self.find('ul li').length;
     methods.setStep(l);
     self.data('steps')['step'] = l; 
     return l;
    },
    prev:    function( ) { 
     var step = methods.getStep();
     var l    = self.find('ul li').length;
     if(step == l) step = step-1;
     return methods.setStep(step - 1); 
    },
    next:    function( ) {  
     return methods.setStep(methods.getStep() + 1);
    },
    setStep: function( stepNumber ) {  
     // Sets the step number
     var l = self.find('ul li').length;
     if(stepNumber < 0) stepNumber = 0;
     if(stepNumber > l) stepNumber = l;
     
     // Resets styles
     self.find('ul li').removeClass('current completed completedLast last end completedLastEnd');

     // Styles for intermediate steps
     self.find('ul li:lt(' + ((stepNumber < l) ? stepNumber : l-1) + ')').addClass('completed');

     if(stepNumber > 0 && stepNumber < l) 
      self.find('ul li:nth(' + (stepNumber-1) + ')').addClass('completedLast');
     if(stepNumber < l)
      self.find('ul li:nth(' + stepNumber + ')').addClass('current');

     // Last step style
     if(stepNumber == l)  
      self.find('ul li:last').addClass('completedLastEnd');
     //if(stepNumber == l-1)  
     // self.find('ul li:last').addClass('currentEnd');
     else 
      self.find('ul li:last').addClass('end');
     
     self.data('steps')['step'] = stepNumber; 
     return stepNumber;
    }
    
  };
  
  /////////////////////////////////////////////////////////////////////////////////////
  // Decides what to do
  if ( methods[options] ) {
   return methods[options].apply( self, Array.prototype.slice.call( arguments, 1 ));
  } else if ( typeof options === 'object' || ! options ) {
   return init.apply( self, arguments );
  } else {
   //$.error( 'Method ' +  options + ' does not exist on jQuery.tooltip' );
   return init.apply( self, {} );
  }    
    
 };
})( jQuery );

$('.steps').steps();
El uso es muy sencillo. Sobre el elemento contenerdor de los pasos (.steps en nuestro caso) se invoca la función ".steps()" para inicializar el plugin y posteriormente se pueden llamar a los métodos definidos mediante "obj.steps("metodo", [param]);" Los métodos de los que se deispone son:
  • 'getStep' - Estado en el que se encuentra
  • 'start' - Resetea el estado al principio
  • 'finish' - Finaliza todos los pasos
  • 'next' - Siguiente estado
  • 'prev' - Estado anterior

La versión minimizada es poco más de 1Kb (a un 10% Wow!) y la comprimida aún menos. Ya no hay excusas para usarlo.

2011/06/28

Lazy load de vídeos

Seguimos exprimiendo milisegundos a la carga de páginas. Si nuestra página tiene vídeos, veremos como la instanciación del objeto del vídeo puede matar nuestro rendimiento, sobre todo si se tienen varios vídeos en una misma página. Para agilizarlo, la mejor manera es realizar la carga del objeto de reproducción bajo demanda de la misma manera que se hace por ejemplo en el navegador de Android. Vamos a ver dos aproximaciones, una con jQuery y otra con javascript, pero ambas se basan en "ocultar" el código real del objeto del reproductor con un comentario HTML poniendo delante una fachada de cartón piedra para ocupar el espacio del vídeo. Una vez se hace ckick en el hueco del vídeo, se elimina el comentario y se carga el objeto real de modo que comience la reproducción de forma automática.

Para el usuario el proceso es transparente, pero la velocidad de la página y su uso de memoria nos lo agradecerán. Esta técnica se usa en Facebook por ejemplo.

Utilizando jQuery
La imagen que se usará como fachada, se coloca de fondo de un hipervínculo que apunta a la dirección real del vídeo (por accesibilidad) y a éste, se le asocia un evento de click que se dispare una única vez. Una vez se hace click, se elimina el href y el comentario del objeto del vídeo.



En el evento de document ready, se procesarán los hipervínculos de la clase "video-in-link" para asignar el evento de click que se ejecutará una única vez mediante el método "one".


Usando javascript a pelo
En esta versión, se usará el evento onClick para llamar a una función que recibirá el hipervínculo pulsado mediante "this".



Una vez se pulsa el hipervínculo y se llama a la función, se elemina el manejador de evento de click del hipervínculo que hemos pulsado para evitar que se llame de nuevo. También se elimina el comentario y el href para obtener el mismo funcionamiento que antes.


Notas

El vídeo debe estar en modo de "autoplay" porque si no, el usuario tendrá que hacer click 2 veces. En youtube habrá que poner el parámetro "autoplay=1" al final.

Para evitar los flashes blancos al instanciar el objeto flash del vídeo, hay que poner el parámetro "wmode" con valor "transparent".

2011/06/07

Raphaël - Haciendo más sencillo el uso de SVG y VML

Si alguno ha estado en este mundillo en los últimos 9-10 años, sabrá que uno de los grandes desconocidos del desarrollo web son los gráficos vectoriales. Siempre ha sido muy sencillo añadir un gif a una página, pero a la hora de por ejemplo, dibujar líneas arbitrariamente, por ejemplo encima de la imagen o generación de gráficas dinámicamente, las elecciones tecnológicas eran pocas y además no estándar en general.

A la hora de hacer estas virguerías, siempre le he tenido un ojo echado primero al VML y luego al SVG, pero quien haya tocado el tema aunque minimamente sabrá que es muy potente, pero muy engorroso.

En este aspecto nos echa una mano Raphäel, una librería javascript que permite el uso de SVG y VML de forma (muy) sencilla y además es multiplataforma soportando incluso IE6 (quizás hasta IE5.5 SP2 que es donde yo empecé a hacer pinitos con VML).

Un ejemplo sencillo de Raphaël para dibujar un círculo sacado directamente de su web
// Creates canvas 320 × 200 at 10, 50
var paper = Raphael(10, 50, 320, 200);

// Creates circle at x = 50, y = 40, with radius 10
var circle = paper.circle(50, 40, 10);
// Sets the fill attribute of the circle to red (#f00)
circle.attr("fill", "#f00");

// Sets the stroke attribute of the circle to white
circle.attr("stroke", "#fff");

Ahora un ejemplo más complejo




Bastante sorprendente verdad? pues os invito a pasaros por la web de Raphaël para ver éste y otros ejemplos interactivos.

2011/05/20

Detectando la rotación de pantalla en Android

Snippet para detectar cambios en la orientación de pantalla en dispositivos móviles. Está pensado para Android, pero supongo que serviría para cualquier plataforma móvil. Realmente no controla que se rote, si no que controla si existe el evento de "onorientationchange" y asigna una función a él. En el caso de que el evento no esté soportado, vincula el evento a resize.

// Detect whether device supports orientationchange event,
// otherwise fall back to the resize event.
var supportsOrientationChange = "onorientationchange" in window,
    orientationEvent = supportsOrientationChange ? "orientationchange" : "resize";

window.addEventListener(orientationEvent, function() {
    alert('HOLY ROTATING SCREENS BATMAN:' + window.orientation + " " + screen.width);
}, false);

Como guinda del pastel, en el caso de que se vincule al evento "resize", se podrían definir 2 variables globales (al contexto) llamadas por ejemplo "screenW" y "screenH" que se controlan en el evento de modo que si están intercambiadas, se ha cambiado la orientación. Con ello, nuestro código quedaría así.

var _screenW = screen.width;
var _screenH = screen.height;

// Detect whether device supports orientationchange event,
// otherwise fall back to the resize event.
var supportsOrientationChange = "onorientationchange" in window,
    orientationEvent = supportsOrientationChange ? "orientationchange" : "resize";

window.addEventListener(orientationEvent, function() {
    if(supportsOrientationChange ||
      (!supportsOrientationChange && _screenW == screen.height && _screenH == screen.width))
   {
       alert('HOLY ROTATING SCREENS BATMAN:' + window.orientation + " " + screen.width);
   }
}, false);

2011/05/18

"Tú no sabes javascript"

He encontrado la transcripción de un artículo que mete el dedo en la llaga sobre la gente que hace con el javascript igual que con el "nivel alto de inglés" y que si fuera al extranjero se moriría de hambre :P

Lo mejor de todo son los enlaces a cada uno de los apartados apra ampliar información sobre el tema (y también algo de alivio porque la mayoría de los temas ya los he tratado yo en entradas anteriores jejeje)

Bueno, sin más dilación os dejo el artículo.

Tú no sabes Javascript
Con este llamativo título, Michael Woloszynowicz escribía recientemente una entrada en su blog a modo de llamada de atención para los desarrolladores Javascript.

Al estilo del artículo Los programadores que no programan, Michael denunciaba que muchos desarrolladores están engordando sus currículums afirmando ser expertos en lenguajes que realmente no conocen. Esta preocupante tendencia parece agudizarse cuando nos referimos a la familia de lenguajes web: HTML, CSS y, especialmente, Javascript.

La falsa sensación de dominio total
El motivo de todo esto es doble: por un lado, los lenguajes web están de moda y todo desarrollador debe ser capaz de programar sin dificultad en cualquiera de ellos; por otro, suelen ser consideradoslenguajes fáciles y muy asequibles: en realidad, tanto HTML como CSS podrían no considerarse estrictamente como lenguajes ya que apenas participan de una lógica computacional seria. Con tan solo un puñado de palabras reservadas y estructuras repetidas hasta la saciedad, es relativamente sencillo construir un sitio.

Pero nada más lejos de la realidad. Conocer el marcado HTML y la semántica que hay detrás no resulta tan trivial como le puede parecer a quien se acerca por primera vez: con solo ojear el código fuente de cualquier proyecto web, podremos  ver cómo las normas más básicas o recomendaciones del W3C son pasadas por alto. En cuanto a las CSS, el argumento es similar: aquellos que realmente las dominen sabrán la complejidad que algunos maquetados (desgraciadamente) encierran. En definitiva, es extremadamente raro encontrar a un desarrollador web que haya reparado en leerse las especificaciones de los lenguajes que utiliza a diario…

En el caso de Javascript, el error no puede ser mayor: la mayoría de los programadores necesitan recurrir a este lenguaje en uno u otro momento. Cuando apenas se conocen sus conceptos básicos, la tendencia más común es lanzarse a buscar ejemplos de códigos (snippets) que puedan ser implementados ràpidamente a través de un sencillo copy/paste. Dada la gran flexibilidad de algunosscripts, esta técnica suele funcionar, especialmente cuando se trata de plugins para bibliotecas tipo jQuery o Mootools. El principal problema de esta metodología de aprendizaje es que realmente no se llega a comprender el comportamiento que hay detrás de cada uno de esos bloques de código dando una falsa sensación sobre nuestro dominio del lenguaje.

Cuando llegan los encargos para construir aplicaciones complejas, los tutoriales en la web comienzan a mostrar conceptos exóticos como módulos, herencia prototípica, patrones de diseño, delegación, constructores o clausuras y con ellos, nuestra falsa idea inicial de dominio del lenguaje comienza a difuminarse.

Comienzan así las pruebas a ciegas donde echamos mano del patrón copy/paste; pero ahora las piezas no encajan: los errores se multiplican, nuestros conocimientos en otros lenguajes como por ejemplo Java o PHP no sirven pues la naturaleza de Javascript es diferente y la aplicación no funciona como se espera. Terminamos maldiciendo al lenguaje y comentándole al cliente que su proyecto no es consistente, que Javascript no ofrece la potencia, flexibilidad o seguridad necesaria para llevarlo a cabo y tratamos de sugerir alternativas en las que nos sintamos más cómodos… Y es que, por supuesto, es difícil terminar admitiendo que no conocemos el lenguaje: Javascript es un lenguaje web y, como tal, tiene que ser fácil y debemos dominarlo sólo por el hecho de haber implementado alguna vez un plugin jQuery con un bonito slideshow…

Niveles de conocimiento
Para poder crear un rasero con el que medir nuestro conocimiento real del lenguaje, podemos dividir sus conceptos en tres niveles diferentes: básico, intermedio y avanzado.

Cada una de estas categorías recogería las siguientes habilidades:
 
Un nivel básico de Javascript requiere:
  • Conocer la sintaxis básica de programación: bucles, condicionales, try/catch, etc…
  • Entender cómo se declaran las funciones (incluyendo las diferentes formas posibles para hacerlo así como las llamadas funciones anónimas).
  • Entender los principios básicos de los distintos ámbitos (scopes): ámbito global vs ámbito de objeto. Excluímos aquí las clausuras.
  • Entender el rol de los distintos contextos y el uso de la palabra reservada this en cada uno.
  • Entender las diferentes maneras de declarar o instanciar un objeto así como las formas de convertir las funciones en éstos.
  • Entender los operadores de comparación Javascript como ‘<’, ‘>’, ‘==’, ‘===’; valores falsy y cómo funciona la comparación entre objetos y cadenas así como el cambio de tipos (casting).
Un nivel intermedio debería requerir:
  • Entender los tiempos de ejecución, cómo funcionan, y cuándo/cómo pueden ser útiles. También incluimos los métodos de ejecución asíncronos.
  • Conocimiento profundo de los callbacks y de los métodos call y apply para controlar el contecto y los argumentos que se envían a las funciones.
  • Comprender en profundidad la notación JSON y el funcionamiento de eval.
  • Entender las clausuras, cómo afectan al rendimiento del código, y cómo pueden ser usadas para crear variables privadas mediante el patrón módulo.
  • AJAX y serialización de objetos.
Un nivel avanzado de conocimiento incluye:
  • Conocer el valor arguments y cómo puede ser utilizado para sobreescribir funciones: recursividad.
  • Clausuras avanzadas como los patrones memoization, curry y aplicaciones parciales.
  • Prototipos, cadena prototípica y cómo usar estos conceptos para minimizar el tamaño de nuestro código.
  • Tipos de objetos y el uso de instanceof y typeof.
  • Expresiones regulares.
  • Qué expresiones no deben utilizarse (bad patterns)
  • Y, finalmente, cómo combinar todo lo anterior para construir aplicaciones sólidas, limpias, rápidas, mantenibles y compatibles.
El punto final, es especialmente importante porque puede darse el caso de que conozcamos los anteriores en mayor o menor medida pero no la forma de integrarlos en un todo. Este aspecto es el que finalmente denominaríamos arquitectura Javascript y corresponde a los cimientos en los que tiene que asentarse todo proyecto serio. Es el camino opuesto a un código tipo spaghetti realizado sin ninguna planificación previa.

Conclusión
Como puede comprobarse a través de todos los aspectos mencionados anteriormente, Javascript es algo más que manejar eventos con nuestra biblioteca favorita o validar un formulario. Es cierto que podemos solventar muchos de estos puntos utilizando nuestro tradicional copy/paste; sin embargo, con esta técnica, podemos llegar muy fácilmente a un callejón sin salida donde continuar avanzando resulte simplemente imposible.

La mejor garantía para afrontar un proyecto de envergadura es, como con cualquier otro lenguaje, conocer el comportamiento de las herramientas que utilizamos lo mejor posible. De este modo, si no encontramos el snippet adecuado o tenemos que modificar el comportamiento de uno concreto, estaremos en disposición de hacerlo nosotros mismos sin mayores problemas.

2011/05/17

Mapeando "this" a "self"

Continuando con la serie de entradas dedicadas a programación orientada a objetos con javascript, me he encontrado con algo que ya había visto en varios sitios, pero que nadie se había molestado en explicar, la asignación a self de la instanciación de la clase (this).


El problema principal viene de que "this" puede variar a lo largo de la ejecución del javascript al usarse frameworks como jquey, de modo que de lugar a errores muy difíciles de encontrar ya que "debería funcionar", pero no lo hace debido al ámbito de this.


Un ejemplo de este comportamiento es el siguiente.

Bob.findFriend("Barry");

Person.prototype.findFriend = function(toFind) {
    // this = Bob
    $(this.friends).each(function() {
        // this = Bob.friends[i]
        if (this.name === toFind) {
            // this = Barry
            return this;
        }
    });
}

En el ejemplo, inicialmente this vale "Bob", pero en cuanto entra en el bucle de jQuery each, this es igual a cada uno de las iteraciones del bucle y ya la hemos armado buena...

Esta situación se puede dar my fácilmente así que la mejor solución es usar una variable local al contexto del objeto para guardar la instancia del objeto (y posiblemente mejorar el rendimiento) mediante

var self = this

Así, el ejemplo anterior quedaría así corregido de modo que haga lo que tiene que hacer y no perdamos el valor de "this" (ahora "self")

Bob.findFriend("Barry");

Person.prototype.findFriend = function(toFind) {
    // the only time "this" is used
    var self = this;
     
    $(self.friends).each(function(i,item) {
        if (item.name === toFind) {
            return item;
        }
    });    
}

2011/05/05

Optimización de javascript (XI). Ámbitos de ejecución

Para no hacer muy densa la entrada de ayer, la he dividido en dos partes.

Ésta sí que se refiere a optimización de javascript, ya que posiblemente, la creación de un ámbito de ejecución para nuestra aplicación sea la característica que más va a influir en la ejecución de nuestro javascript si éste es medianamente pesado. Además de las ventajas de tiempo de ejecución por reducción del ámbito, de regalo se tiene una aproximación a programación orientada a objetos con javascript que permite encapsular funcionalidades.

Un auténtico experto en el uso de ámbito para aplicaciones en tiempo real (demos gráficas) es el creador del sitio dhteumeuleu, que mediante una base de aplicación sencilla y un timer, hacer arte.

El esqueleto básico de nuestra aplicación 'App' debe ser el siguiente:
// Esqueleto de la clase App
var App = function() {
    /////////////////////////////////////////////////////////////
    // MIEMBROS PÚBLICOS DE LA CLASE
    /////////////////////////////////////////////////////////////
    // Objeto donde almacenaremos las propiedades públicas de la clase
    // Se accede a ellas mediante app.self.propx
    this.self = {
        prop1: valor,
        prop2: valor,
        // ...
    };

    // Otra manera de definir miembros públicos de la clase es mediante this
    this.prop = valor;
    this.funcionPublica = function (params) { /* código */ };

    /////////////////////////////////////////////////////////////
    // MIEMBROS PRIVADOS DE LA CLASE
    /////////////////////////////////////////////////////////////
    // ----- crossbrowsers addEvent ----- 
    function addEvent (o, e, f) { 
        if (window.addEventListener) o.addEventListener(e, f, false); 
        else if (window.attachEvent) r = o.attachEvent('on' + e, f); 
    } 

    // Variables privadas de la clase
    var _proppriv1 = valor;
    var _proppriv2 = valor;

    // Funciones privadas de la clase
    var func1 = function (params) { /* lo que sea */ };
    function func2(params) { /* lo que sea */ } // También se puede hacer así

    /////////////////////////////////////////////////////////////
    // CLASES
    /////////////////////////////////////////////////////////////
    // Además del resto de miembros privados y públicos, 
    // se pueden definir nuevas clases dentro de nuestra clase que se 
    // podrán usar dentro del objeto (y no fuera).
    // Evidentemente todos los miembros de nuestra clase son 
    // visibles para la clase "hija"
    var Coordenada = function (x, y) { 
        this.x = x; 
        this.y = y; 
    }; 

    /////////////////////////////////////////////////////////////
    // CONSTRUCTOR
    /////////////////////////////////////////////////////////////
    var init = function (params) {
        // Función interna del constructor
        // Llamada desde la función de instanciación
    };

    // Devuelve la función de instanciación de la clase
    // En este caso se vincula a un evento para que la ejecución no sea bloqueante
    return {
        init : function (params) { 
            addEvent(window, 'load', function () { 
                init(params); // Llama a la función de construcción
            }
        };
}();

/////////////////////////////////////////////////////////////
// FUNCIONES PÚBLICAS DE LA CLASE
// Se hace mediante el prototype por mayor velocidad de 
// ejecución a costa de algo más de memoria usada.
App.prototype.func1 = function( params ) { /* ... código ... */ };
App.prototype.func2 = function( params ) { /* ... código ... */ };
// ...
App.prototype.funcn = function( params ) { /* ... código ... */ };

Cuando se esté ejecutando código de una de las funciones de la clase, para acceder a la "instancia" (los valores de la instanciación del objeto) basta con hacer la llamada mediante
self.propx
ya que self está dentro del ámbito de la aplicación o mediante un simple
propy
en el caso de que se haya definido de la segunda manera, eso sí, no nos beneficiamos de la reducción del ámbito de búsqueda que nos da definirla dentro de "self".

Para crear la instancia de la aplicación se hace de la manera siguiente
<script>var miApp1 = app.init( { prop: valor, prop: valor, ... } );</script>
Si no quisiéramos usar una función "init" para instanciar la clase, se podría quitar toda la parte del constructor y se definiría la clase como
// Esqueleto de la clase app
var App2 = function( params ) {

    // ... 
    // El resto es igual, pero para acceder a los parámetros de instanciación
    // habría que hacer params.prop, con param en formato de objeto 
    // ...

    // ... Inicialización ...

    return self; // Dentro vamos a definir los miembros públicos además 
                 // de los definidos mediante this.xxx = yyy;
    return this; // Si no se quiere usar el objeto "self" siempre se 
                 // puede devolver this
}();

// Se llama mediante
var miApp2 = App2( { prop: valor, ... } );

En la llamada, se crea la instancia de la clase de la aplicación pasando como parámetro un objeto con los valores de la instanciación.

Si en algún momento ya no nos hacen falta propiedades que tenga definidas (ya sean variables o funciones incluso) se pueden borrar mediantedelete miApp.prop;
Os recuerdo que además, se pueden eleiminar elementos del DOM que ya no nos hagan falta para liberar memoria mediante
delete el.parentNode.removeChild(el);
Como extra, podemos aprovechar además para aplicar técnicas de "memoization" que no es más que cachear resultados de operaciones costosas para acelerar su ejecución
function miFunc(param) {
    if(!miFunc.cache) {
        miFunc.cache = {}; // Creamos un caché de resultados para la función
    }
    if(!miFunc.cache[param]) {
        var resultado = {};
        // ... realiza las operaciones y se calcula resultado ... 
        miFunc.cache[param] = result;
    }
    return miFunc.cache[param];
}
Obviamente, para tareas costosas que no necesiten ejecutarse en primer plano, tenemos los web workers de los que ya he hablado con antelación. Si no se dispone de web workers por la versión del navegador, se puede hacer ejecutar la función en segundo plano mediante setTimeout() como se vió en el capítulo VI de optimización de javascript.

2011/05/04

Optimización de javascript (X). OOP con javascript

Como no hay 2 sin 3, no pueden haber 9 sin 10... vamos a por la décima entrega de optimización de javascript.

Hoy, más que hablar de optimización, vamos a ver algo sobre orientación a objetos, que uno de los mejores inventos desde la cocacola.

El primer paso para la orientación a objetos, es saber cómo se crea un objeto de tipo Object en javascript.

El objeto más simple es el mapa (y no el de Dora la exploradora) o matriz asociativa dispersa que se define entre llaves (recuerdo que los corchetes es la notación simple para definir arrays) y que se indexa por cadenas.

Para definir propiedades del objeto, se puede hacer al definirlo (la clave es una cadena de texto pero no es obligatorio escribirla como cadena entre comillas) o a posteriori como si fuera un Array.
var obj      = {};               // Objeto vacío
var obj      = {'clave': valor}; // Objeto con propiedad
obj['clave'] = valor;            // Propiedad en tiempo de ejecución
El valor almacenado para una propiedad puede ser cualquier cosa, es decir, cualquier objeto de javascript, incluidos otros objetos.

Así se definen objetos, pero las funciones también son objetos y cuando se busca la orientación a objetos con javascript, son los que se usan para definir las clases.

Una clase sencilla definida con una función puede ser el siguiente
var Coordenada = function (x, y) { 
    this.x = x; 
    this.y = y; 
}; 
Los miembros de la clase se definen de dos maneras, o desde dentro de la definición de la clase, o desde fuera.

Si es desde dentro, se hace con la palabra reservada this
this.x = x;
Si es desde fuera, hay que hacer referencia al prototipo de la función clase
Coordenada.prototype.propiedad = valor;
Todas las variables/funciones definidas dentro dentro de nuestra función clase que no se definan mediante this, serán miembros privados de la clase mientras que todas las que se hagan con this (o accediento al prototype) serán miembros públicos.
var Coordenada = function (x, y) { 
    this.x = x;  // Miembro público
    this.y = y;  // Miembro público
    var c = 123; // Miembro privado
};
Para instanciar la clase, no hay más que usar la palabra reservada new
var c = new Coordenada(x, y);
Creo que es importante recordar algo de notación básica de objetos (heredada de Java) para diferenciar clases de objetos.
  • Las clases comienzan por mayúscula
  • Los objetos empiezan por minúscula
Aparte, de regalo, recomiendo usar una notación coherente luego para nombrar clases y objetos. Si te gusta usar '_' entre "palabras", usar siempre '_'. Si te gusta usar combinaciones mayúsculas/minúsculas, usarlas siempre.

2011/05/03

Optimización de Javascript (IX). Creando una cola de ejecución

Tal y como hemos visto en las 8 entradas anteriores, hay que optimizar todo lo referente a javascript y su carga ya que éste bloquea la ejecución durante la carga.

Lo hemos movido a otro sitio de la página, lo hemos minimizado, lo hemos cargado en diferido, pero aún no había tratado sobre el código javascript de tipo inline, es decir, el que ponemos en medio del códgido para hacer cualquier cosa. Bueno, pues ahora vamos a ver cómo optimizar éste código también para dar una mayor velocidad de carga de nuestra página.

1.- El secreto de todo está en crear un objeto para poner en una cola de ejecución todas nuestras llamadas de tipo inline. Así, lo primero que tendremos que hacer es
var miaplicacion = { queue: [] };
Con ésto creamos un contexto de ejecución para todas las llamadas además de crear un array para encolar las ejecuciones.

2.- Ahora tenemos que sustituir todas las llamadas inline como por ejemplo
<script>alert("Zas!");</script>
por
<script>
    miaplicacion.queue.push(function() { 
        alert("Zas!"); 
    }
</script>
3.- Ya hora la guinda. Una vez llegamos al final de la página, justo antes del </body> que es donde ya estamos seguros de que se ha cargado todo el DOM (así no es necesario enlazar con el evento de DOMReady con lo que arañamos aún más milésimas de tiempo de ejecución), ejecutamos la cola mediante el siguiente código:
<script>
    var l = miaplicacion.queue.length;
    for(var i = 0; i < l; i++) {
        miaplicacion.queue[i]();
    }
</script>

2011/04/22

Longpolling

Como os dije ayer, la técnica del longpolling merece una mención aparte principalmente porque es una técnica universal, es decir, si tu navegador soporta AJAX, también soporta longpolling y lo más importante, funciona y lo hace muy bien.

Solo tiene una pega, que mantiene una conexión permanentemente abierta con el servidor con lo que si tenemos muchas visitas simultáneas, podemos llegar a tener problemas a medida que nos vayamos acercando al límite de conexiones del servidor... pero vamos, que en la práctica no vamos a hacer nuestro Google particular donde hay que optimizar cada conexión y cada bit.

Si realmente el número de conexiones es un problema, habrá que recurrir a otras tecnologías como por ejemplo websockets, que es la que tiene más posibilidades (ver el artículo de ayer).

Bueno, pues vamos al lío. En esta técnica hay dos partes, el código del cliente y el del servidor.

En el servidor, vamos a tener un PHP que se encarga de la lógica de negocio cuya pieza clave es la posibilidad de hacer un fork poniendo el proceso en estado de sleep mientras no hayan cambios.
usleep(20000);    // sleep 20ms to unload the CPU
clearstatcache();
Al estar el proceso dormido, no estará bloqueando la ejecución y liberará la CPU. Ahora, la segunda clave, es esperar x segundos mientras no haya que enviar nada al cliente siendo x suficientemente largo como para no tener que estar reabriendo la petición por parte del cliente y suficientemente corto como para que la petición al PHP no de timeout del servidor. En la práctica, el tope son 30 segundos aproximadamente (depende de la configuración del servidor), así que 25 segundos es un buen número. El ejemplo que vamos a ver es el de un chat así que ahí va el código de al parte servidora.

<?php
$fileName = 'chatdata.txt'; //    File to save latest chat in
$maxTime = 25; // Maximum loop time in seconds before we end the script
// Note: The front end will wait 5 seconds, and then re-connect.

if( ! is_file( $fileName ) ) file_put_contents( $fileName, "Hello from the chat server!" ); // Controla que exista el fichero

header('Content-Type: application/javascript');
if( isset( $_REQUEST['msg'] ) ) {
    //    Only 1024 chars per chat
    $msg = substr( $_REQUEST['msg'], 0, 1024 );
    file_put_contents( $fileName, $msg );
    exit();
}

$serverTime = isset( $_REQUEST['ts'] )? $_REQUEST['ts']: 0;
$fileTime = filemtime( $fileName );

$started = time();
$timedOut = false;

// Loop till the file is modified or loop times out
while( ( $fileTime <= $serverTime ) && ( ! $timedOut ) ) {
    usleep(20000);    // sleep 20ms to unload the CPU
    clearstatcache();
    $fileTime = filemtime($fileName);
    $timedOut = ( ( time() - $started ) >= $maxTime );
}

// JSON response, only if we didn't time out
if( ! $timedOut ) {
    print $_REQUEST['callback'] . "(" . json_encode( array(
        "data" => file_get_contents( $fileName ),
        "ts" => $fileTime
    ) ) . ")";
}
else
    header("HTTP/1.0 204 No Content");

flush();
?>

Cómo funciona. Todo se basa en que el PHP del servidor está vigilando un archivo "compartido" que crea y que es usado por todos los clientes del chat. Cuando un cliente escribe un texto y lo manda al PHP, éste lo cobreescribe inmediatamente en el fichero. Como el resto de los clientes tienen sus propios procesos PHP a la escucha, en el momento que se detecta un cambio en la fecha del fichero con filemtime, automáticamente vuelcan el contenido del fichero a sus respectivos clientes mediante JSONP que están a la espera mediante el longpolling. De este modo en el fichero siempre está la última línea de texto escrita en el chat.

Ahora la parte cliente. Para su funcionamiento, se ha usado jQuery, y un plugin llamado jquery-longpoll-0.0.1.js que se encarga de la comunicación con el servidor y que a su vez necesita tener el plugin de jQuery de JSONP para funcionar.

/*
 jQuery Long Poll plugin 0.0.1 (02-05-2010)
 
 Note: requires jQuery-JSONP: http://code.google.com/p/jquery-jsonp/

 Copyright (c) 2010 JavaScript Guy

 This script is licensed as free software under the terms of the
 LGPL License: http://www.opensource.org/licenses/lgpl-license.php
 
 TODO: Implement a way to hide the spinner in FF and IE, possibly use an iFrame?
 

*/
(function($){
 
 $.longPoll = function( args ) {
  args = args || {};
  var self = $.extend( {
   url: '',    // URL To connect to
   tsParameter: 'ts',  // Parameter used for the timestamp
   reconnect: true,  // If we're automatically reconnecting
   errorTimeout: 60,  // Timeout for errors, eg: server timeout, execution time limit, etc.
   errorTimeoutDelay: 5, // Seconds to delay, if the connection failed in an error
   hasConnection: false, // If we have a connection
   timestamp: 0,
   error: false,
   request: null,
   reconnectFunc: function() {
    if( self.reconnect ) {
     self.disconnect();
     // Reconnect - with longer error timeout delay (in case we have an intermittent connection)
     if( self.error ) {
      setTimeout( function() { self.connect(); }, self.errorTimeoutDelay * 1000 );
     } else {
      setTimeout( function() { self.connect(); }, 100 );
     }
    }
   },
   connect: function() {
    self.reconnect = true;
    // self.request = $.ajax( {
    self.request = $.jsonp( {
     url: self.url + '?' + self.tsParameter + '=' + self.timestamp + '&callback=?',
           timeout: self.errorTimeout * 1000,  // Timeout and reconnect
           error: function(XHR, textStatus, errorThrown) {
      self.reconnectFunc();
      self.error = true;
           },
     dataType: "jsonp",
     success: function( data ) {
      self.timestamp = (data[self.tsParameter])? data[self.tsParameter]: self.timestamp;
      self.success( data );
      self.error = false;
     },
     complete: function( data ) {
      self.reconnectFunc();
     }
     
    } );
    self.hasConnection = true;
   },
   
   // Aborts request
   disconnect: function() {
    self.reconnect = false;
    if( self.request )self.request.abort();
    self.hasConnection = false;
   },
   
   // User defined
   success: function( data ) {}
   
  }, args );
  return self;
 };

})(jQuery);

La aplicación de chat en sí, tiene dos partes y está hecha como se deben hacer las aplicaciones en javascript mediante contextos, aunque generalmente somos demasiado vagos como para hacerlo. La primera parte encapsula la aplicación de chat que se encargará de realizar las peticiones JSONP mediante el plugin de longpolling.

// Chat app
var chatApp = function( url ) {
 
 var self = {
  connection: $.longPoll( {
   url: url,
   success: function( data ) {
    // Ignore server timeouts, the script will handle it.
    if( data['data'].indexOf( "Maximum execution time of" ) == -1 ) {
     jQuery('#content').append( '
' + data['data'] + '
' ); } } } ), // Sends a chat message sendChat: function(request) { jQuery.ajax( { url: url, type: "GET", dataType: "jsonp", data: { 'msg' : request } } ); }, // Toggle connection button toggleConnect: function( button ) { if( self.connection.hasConnection ) { self.connection.disconnect(); jQuery( button ).val("Connect"); } else { self.connection.connect(); jQuery( button ).val("Disconnect"); } } }; return self; };

Básicamente, lo que hace es implementar una clase que controla la conexión al chat, el envío de mensajes y la puesta en espera de nuevos mensajes del resto de los clientes. La segunda parte es el uso de la clase chatApp definida en la primera parte.

// Chat app initialisation
// var chatAppContext = chatApp( 'http://paxjs.com/labs/longpolljsonp/chat.php' );
var chatAppContext = chatApp( 'chat.php' );

var sendMessage = function() {
 chatAppContext.sendChat( jQuery('#message').val() );
 jQuery('#message').val('');
};

jQuery( '#connect' ).click( function() {
 chatAppContext.toggleConnect( jQuery( this ).get(0) );
} );

jQuery( '#sendMsg' ).click( sendMessage );
jQuery( '#message').keyup(function(e) {
 if(e.keyCode == 13)sendMessage();
});

Todo junto en jsfiddle y funcionando (pinchad en la pestaña de "Result" y pulsad el botón "Connect")


Las posibilidades de comunicación son infinitas ya que es realmente sorprendente la velocidad de respuesta ya que los cambios son instantáneos en todos los clientes.

Como conclusión, decir que la aproximación óptima debería ser mixta de modo que los clientes que soporten técnicas más modernas que no mantengan una conexión permanentemente abierta como websockets de HTML5 los usen y los clientes más viejos usen longpolling. Con la fusión de estas dos técnicas se soportaría el 100% de los navegadores del mercado y nos permitiría el desarrollo de aplicaciones de tiempo real con comunicación bidireccional cliente-servidor.

2011/04/16

Comunicaciones con HTML5

El post de hoy es muy interesante ya que nos va a permitir romper con las limitaciones tradicionales de las páginas web. No os lo perdáis...

Casi todas las páginas que hacemos, tienen la necesidad de comunicarse o desde o hacia el exterior. Normalmente usamos AJAX, pero no siempre es la solución óptima. Con HTML5, han aparecido varias tecnologías que nos ayudan a rellenar los huecos dejados por AJAX para realizar operaciones complejas de forma nativa.

Concretamente, vamos a ver 5 métodos de comunicación
  • XHR & XHR2 con CORS. XHR = XMLHttpRequest, es decir, AJAX, pero en este caso, con esteroides.
  • Web Messaging
  • Web Sockets
  • Server Sent Events
  • Web Workers
Todos ellos menos XHR están basados en eventos lo que hace que todo sea más dinámico y por supuesto, sin necesidad de recargar la página.

XHR & XHR2 con CORS
Parece un gazpacho de letras aleatorias... En cristiano es XMLHttpRequest y XMLHttpRequest2 con Cross Origin Resource Sharing. Básicamente de lo que se trata es que habilitar las peticiones AJAX entre distintos dominios al de la página actual, pues ya sabéis que por motivos de seguridad, ese tipo de peticiones están capadas (aunque algunos navegadores se lo pasen por el forro en el mejor de los casos... no te escondas IE, que se te ve el plumero).

El secreto para permitir peticiones entre distintos dominios reside en el servidor de destino, de modo que en la cabecera se especifica el dominio o dominios desde los que permiten realizar peticiones AJAX. También admite comodines. Un ejemplo en PHP es el siguiente:
<?php
header('Access-Control-Allow-Origin: *');
?>

Entonces, os preguntaréis de dónde viene el objeto XHR2. Básicamente, lo que ocurre es que el objeto XHR original no permite o no debe permitir abrir un URL de un dominio distinto al actual. Sin embargo el uso de un objeto u otro es transparente al programador, menos para IE8, de nuevo, que ha de usar XDomainRequest. Un ejemplo de llamada usando XHR2 con CORS a un dominio distinto al de la página actual es éste.
var client = new XMLHttpRequest();
client.onreadystatechange = function () {
  if (this.readyState == 4 && this.status == 200) {
    alert('The most awesome-est person to follow: ' + this.responseText);
  }
};
client.open('GET', 'http://remysharp.com/demo/cors.php');
client.send();
Web Messaging con postMessage
Esta tecnología es bastante antigua ya en cierta manera está soportada desde IE6.

El principal punto fuerte de esta tecnología es el permitir la comunicación entre páginas de distintos dominios y es una de las maneras más sencillas de saltarse la limitación de la política de mismo dominio. El principio de funcionamiento es el siguiente. En una página tenemos un contenido con un iframe que apunta a otra página distinta que es con la que nos queremos comunicar. La página principal dispone de un dódigo javascript que permite recibir eventos onmessage del tipo
window.onmessage = function (event) {
  if (event.origin == 'mytrustedsite.com') {
    alert('mi sitio de confianza ha dicho: ' + event.data);
  }
};
 El contenido del iframe además podrá ser originador de mensajes mediante el siguiente código.
// iframe es el nodo DOM del propio iframe
iframe.contentWindow.postMessage("hola", location.host);
La comunicación está limitada a cadenas de texto, pero siempre se pueden realizar conversiones de tipo JSON-string y string-JSON con las funciones JSON.stringify y JSON.parse.

Para aumentar la compatibilidad a IE6, hay un proyecto llamado EasyXDM que lo que hace es en función de la versión del navegador, si es necesario añadir un flash que permite el envío de messages. Para más información, en su sitio.

Web Sockets
Web sockets es la tecnología para mí con más futuro ya que permite no tener que recurrir a inventos artificiosos para comunicar una página web con cualquier cosa ya que usa sockets y eventos.

Los web sockets son equivalentes a los sockets de comunicación de toda la vida pero no son lo mismo. Para poder usar web sockets, se usa un protocolo especial ws:// de modo que es necesario que el servidor de destino soporte ese protocolo.

Hay muchas implementaciones de servidores que soportan el protocolo, pero desafortunadamente, los servidores normales no lo soportan de forma nativa. Esto es debido a que el modelo de funcionamiento basado en conexión como por ejemplo el de apache, no es compatible con el modelo de funcionamiento basado en eventos como pasa con los web sockets. Además, existe un paradigma denominado el problema de las 10000 conexiones C10k, que especifica que un servidor debe ser capaz de soportar 10000 conexiones simultáneas. El problema viene en que para que un servidor de tipo tradicional orientado a la conexión soporte websockets, ha de mantener la conexión abierta continuamente para mantener levantado el canal de comunicación entre la página y el servidor... esa conexión no se cierra y si hacemos un cálculo rápido de unas 50 conexiones por página, la frontera de las 10000 conexiones se nos puede hacer muy pequeña.

Aquí entran en juego los servidores orientados a eventos, que mediante una única conexión abierta por servicio nos permiten servir a un número ilimitado de peticiones. Es decir, que por cada tipo de recurso (llamémosla página) solo habrá que tener una conexión abierta. Así, un servidor ligero puede ser capaz de servir de forma muy rápida no solo a 10000 clientes simultáneos, sino a 100000, 200000 etc. Hay varias alternativas de servidores pero las que más me gustan son las siguientes:
  • Node. Mi favorito. Servidor ligero basado únicamente en javascript V8. Es sencillo de implementar, extremadamente rápido y muy muy ligero. Hay servidores que te permiten hospedar gratuitamente servidores node como es el caso de Joyent. Hay extensiones para node para acceso a bases de datos e integraciones con otros servicios para respuestas en tiempo real. Un ejemplo de servidor node es tan sencillo como el siguiente, que responde con una página web con la cadena Hello World a peticiones websocket hechas sobre el servidor:
var http = require('http');
http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Hello World\n');
}).listen(8124, "127.0.0.1");
console.log('Server running at http://127.0.0.1:8124/');
otro ejemplo de servidor TCP con node que hace un echo de lo que se le envíe
var net = require('net');
var server = net.createServer(function (socket) {
  socket.write("Echo server\r\n");
  socket.pipe(socket);
})
server.listen(8124, "127.0.0.1");
  • WaterSpout. otro que también funciona muy bien. hecho en PHP y su principal ventaja es que soporta tanto web sockets como longpolling (tecnología sobre la que hablaré otro día). Evidentemente no es un PHP para ser servido por apache por ejemplo, si no para ser ejecutado desde línea de comandos y mantenerse en segundo plano. Nota para los losties, probad el chat multiusuario de John Locke ;)
  • Apache. Aunque apache no es la plataforma ideal, si se tiene el humor de leerse un par decientos de páginas de documentación y hacer cientos de cambios de parámetros de configuración, existe un módulo basado en pyton para apache para soportar websockets llamado pywebsockets. Más información aquí.
  • Nginx. Detrás de una web horrorosa que da pocas ganas de probarlo se esconde este servidor cuya principal virtud quizás es que dispone de binarios apra windows y que coge un 46% de la cuota de mercado de servidores de websockers (el 54% lo más probable es que sean node).
Bueno, ya tenemos los cimientos del edificio, ahora a por las paredes. Quién soporta websockets? pues todos los navegadores de verdad menos los sospechosos habituales (Internet Explorer, al rincón!) bueno, realmente, solo chrome y safari. Firefox tampoco lo soporta nativamente... pero realmente hay maneras de que desde el IE6 se puedan usar websockets mediante una librería que incorpora un flash que suple esta carencia a costa de menor rendimiento en la carga de la página debido a la carga del flash. El proyecto en cuestión se llama web-socket-js y solo se puede decir que funciona y es transparente, qué más se puede pedir?!. El desarrollador no asegura que en IE < 8 no haya errores, pero que directamente pasa de dar soporte para esas versiones aunque debería funcionar teniendo flash > v.10.

El tejado de nuestro edificio es el código dentro de la página y una vez tenemos toda la infraestructura montada es realmente sencillo... tan sencillo que da miedo...

var ws = new WebSocket('ws://somesite.com/updates');

ws.onmessage = function (event) {
  alert(event.data);
};

ws.onopen = function () {
  ws.send('yay! we connected!');
};
Lo más importante, es cambiar el "chip" de programación ya que los web sockets tanto en su parte servidora como en la cliente está 100% orientada a eventos y como tan, necesita que el código sea así ya que es la única manera de conseguir conexiones no bloquentes. Es un imperativo hacerlo así.

Server-Sent Events
Una tecnología que viene desde el 2006 promocionada por Opera. Es de un único sentido desde el servidor hacia el cliente y se usa para realizar push de información hacia los clientes además de implementar la limitación de mismo dominio. Soportada únicamente por Opera, Safari y Chrome, pero bajo mi punto de vista, la limitación de plataformas soportadas y el sentido único hace que no sea especialmente atractiva.

Hace falta un servidor preparado como pasa con los web sockets. Un ejemplo de servidor basado en Node es éste y en la parte cliente es prácticamente idéntico a web sockets, solo cambiando el objeto referenciado y el protocolo, claro.

var es = new EventSource('/sse');
es.onopen = function () {
  console.log('opened stream');
};

es.onmessage = function (event) {
  console.log('new message: ' + event.data);
};
Web Workers
En pocas palabras, un web worker es un javascript de una página que se ejecuta en segundo plano. Proporciona un sistema de comunicación bidireccional  de cadenas de texto con su página principal basado en mensajes. Evidentemente, no implementa "per se" comunicación cliente-servidor... solo de una página con un proceso en segundo plano creado por ella.

Ejemplo de creación de web worker en la página principal
var worker = new Worker('bigjob.js');
worker.onmessage = function (event) {
  alert('Message from worker: ' + event.data); // event.data es string!
};

worker.postMessage('task=job1');
Código fuente del worker bigjob.js.
this.onmessage = function (event) {
  var job = event.data;
  if (job == 'task=job1') {
    job1();
  } else if (job == 'task=job2') {
    job2();
  }
};

// just a pseudo example
function job1() {
  // do some big task
  while (working) {
    // continue task
    this.postMessage('job1 ' + amountComplete + '% complete');
  }
  this.postMessage('job1 100% complete');
}
Internet Explorer no soporta web workers para variar. Sin embargo, funcionan en Chrome, Safari, Opera y Firefox.

2011/04/07

Clase MD5 en JavaScript

A mí me ha hecho falta más de una vez y por eso la comparto con todos vosotros.

El uso es muy sencillo. Se incluye el js de la clase y se llama con
var resultado = MD5("cadena de texto");

La podéis ver con un ejemplo aquí http://jsfiddle.net/jrdiaz/5mdt6/


Otro ejemplo de fire-and-forget de los que me gustan.

2011/04/04

Comprobando si el navegador soporta CSS3 con javascript

He encontrado este trozo de código para comprobar si el navegador con el que se está viendo la página soporta cierta propiedad de CSS3. Ojo, hay que pasar la propiedad en notación javascript, es decir, son guión y con la letra siguiente al guión en mayúscula (p.e. border-radius ==> borderRadius)
var supports = (function() {
   var div = document.createElement('div'),
      vendors = 'Khtml Ms O Moz Webkit'.split(' '),
      len = vendors.length;
   return function(prop) {
      if ( prop in div.style ) return true;
      prop = prop.replace(/^[a-z]/, function(val) {
         return val.toUpperCase();
      });
      while(len--) {
         if ( vendors[len] + prop in div.style ) {
            // browser supports box-shadow. Do what you need.
            // Or use a bang (!) to test if the browser doesn't.
            return true;
         }
      }
      return false;
   };
})();
Ejemplo de uso
if ( supports('textShadow') ) {
   document.documentElement.className += ' textShadow';
}

2011/04/01

Minimización de fuentes II. Minify.

Como os decía ayer, hay una alternativa a la compresión en tiempo de desarrollo de los fuentes javascript y CSS, y esa alternativa se llama Minify (http://code.google.com/p/minify/).

Lo que hace que funcione esta librería PHP es que incluye en motor de compresión de JSMin que vimos ayer portado a PHP para javascript y CSS (y también HTML) de modo que en tiempo real se minimizan los fuentes ya sean uno a uno o en grupos, lo que además de reducir el tamaño, permite reducir el número de peticiones HTTP.
Antes

Después

La instalación es sencilla... se descarga el paquete y se descomprime en un directorio accesible desde PHP del servidor. A partir de ahí, suponiendo por ejemplo  que se ha descomprimido en el document root, solo habrá sustituir las inclusiones javascript y CSS por
<script source="/min/b=directorio_base&amp;f=fichero1.js,fichero2.js,fichero3.js"></script>
<link rel="stylesheet" href="/min/b=directorio_base&amp;f=style1.css,style2.css" type="text/css">
El directorio base con el b=directorio es opcional y se puede incluir en el nombre de cada fichero.
Alguno dirá... "espera! y qué pasa con las directivas url() de CSS... si me cambian la localización del CSS, se me escogorciará todo!" y tiene razón... pero el propio minify es capaz de analizar el directorio del CSS y generar el código final con los url cambiados para que se mantenga el camino hasta el fichero indicado en el url.

La ventaja es que las minimizaciones se cachean de modo que cuando se vuelve a pedir el códido minimizado si los fuentes no han cambiado, se devuelve el caché incluso pre-comprimido por gzip haciendo que vaya muy rápido. En el momento que se cambia un fichero, se reminimiza todo y se devuelve el resultado.

Ahora llega el momento es que se está en medio del desarrollo y se quiere depurar el código porque algo está dando lata. Muy fácil... solo hay que añadir al final del URL la coletilla &debug=true (&amp;debug=true) para que no minimice el código y además añada como comentario el número de línea original antes de concatenar los ficheros unos con otros.

Este es el funcionamiento base, pero puede que os pase como a mí, que no me gusta dar "pistas" de la estructura interna de la página o tener mayor control sobre qué se incluyo en el minimizado o no. Pues ello se consigue definiendo grupos de minimización.

Los grupos se configuran en el fichero /min/groupsConfig.php dentro del paquete de minify y son un array asociativo con clave el nombre del grupo y con valor un array con la ruta al fichero a minimizar. Se puede usar en la ruta del fichero el pronombre '//' al principio para indicar el document_root del servidor web. Un ejemplo es:
return array(
    'js' => array('//js/file1.js', '//js/file2.js')
);
Para CSSs seria igual. Para incluir un grupo, solo hay que cambiar el /f (&amp;f) del url minimizado, por /g (&amp;g) indicando a continuación el nombre del grupo.

Como esto puede ser un tanto engorroso, minify proporciona una herramienta para generar el código a incluir en el groupsConfig.php simplemente abriendo la dirección /min de nuestro servidor sin parámetros, con lo que se muestra una interfaz para añadir ficheros y al final se genera el código PHP a pegar.

Minify, además tiene estructura modular y se le puede integrar el toolchain para el closure compiler de google, otros motores de minimización como por ejemplo el jsmin+ o el yui. Para ésto hay que mirar la documentación.

No hay que llegar a tantas virguerías ya que el caso más común que se puede dar es que haya código ya minimizado porque forme parte de otro paquete y que no se quiera minimizar ya que al reminimizar un código minimizado con otro motor de lugar a un código inválido y nos volvamos locos al no poder depurar. La solución es usar el módulo de trasformación de código que se usaría para cosas más complejas como he dicho antes pero forzando que la salida sea igual a la entarada. Un ejemplo con el jQuery:


//'//directorio/js/jquery-1.5.1.min.js' // Línea inicialnew Minify_Source(array('filepath' => '//directorio/js/jquery-1.5.1.min.js','minifier' => create_function('$a', 'return $a;'))) // Línea nueva
Realmente es más complicado explicarlo que usarlo. Quizás no de la mejor tasa de compresión de todas, pero funciona tan bien y es son tan fáciles de mantener la reglas que no da ni pereza. Yo lo que hago es generar un grupo para el CSS con todos los CSS aplicables y 2 ó 3 grupos de javascript con más o menos paquetes en función del uso más normal en las distintas páginas e incluyo el URL correspondiente en cada página.

2011/03/31

Minimización de fuentes I. Introducción.

A la hora de optimizar la carga de una página web, los dos puntos principales a optimizar son reducir la cantidad de bytes transferidos y reducir el número de peticiones hechas al servidor. Para el segundo punto ya hemos visto cómo hacerlo para las imágenes usando sprites, pero hay un modo de hacer ambas cosas pero aplicado a javascripts y CSSs (y también el HTML como hace por ejemplo Google), la minimización.

Aparte de estas dos ventajas, se tendrían otras secundarias como por ejemplo la ofuscación de código y aumento de la velocidad de parseo y ejecución de javascripts, pero todos los avances en un sentido tienen su contrapartida en otro, el mantenimiento.

Hay varias herramientas para la minimización, siendo las más importantes
Salvo uglify y la versión online del closure compiler de google tienen la pega que para sacarle el rendimiento real, hay que pelearse con unos ficheros de configuración bastante complejos que hacen que la pereza de ponerse a minimizar código sea mayor aún y al final por dejadez y vagancia nunca se hagan. Un ejemplo de ello es el XML de configuración del closure compiler para la generación del paquete minimizado de jQueyUI https://github.com/jquery/jquery-ui/blob/master/build/build.xml.

Una vez que se ha minimizado el código javascript y los CSSs, se hace imposible la modificación de los fuentes porque los javascripts se convierten en un galimatías incomprensible y los CSSs en un chorizo interminable de propiedades, lo que obliga a aplicar la minimización solamente al producto final y todos sabemos que todo desarrollo informático es como las obras de arte, que no se terminan, se "abandonan"... siempre queda algo por hacer o mejorar.

Bueno, pues hay una alternativa usando PHP que permite una tasa razonable de minimización y fácil mantenimiento, pero eso, en la segunda parte... mañana, más.

2011/03/29

Detección de clientes móviles mediante javascript, PHP y .htaccess

Recopilo las forma de detectar navegadores de dispositivos móviles mediante distintas tecnologías:

Javascript
var ua = navigator.userAgent.toLowerCase();
var isAndroid = ua.indexOf("android") > -1; // && ua.indexOf("mobile");
var isiPhone = ua.indexOf("iphone") > -1;
var isiPod = ua.indexOf("ipod") > -1;
var isiPad = ua.indexOf("ipad") > -1;
// For use within iPad developer UIWebView
if(!isiPad) isiPad = /iPad/i.test(ua) || /iPhone OS 3_1_2/i.test(ua) || /iPhone OS 3_2_2/i.test(ua);
var isWPx = ua.indexOf("windows phone") > -1;
PHP
$ua = strtolower($_SERVER['HTTP_USER_AGENT']);
$isAndroid = stripos($ua, 'android') !== false; // && stripos($ua,'mobile') !== false
$isiPhone = stripos($ua, 'iphone') !== false;
$isiPod = stripos($ua, 'ipod') !== false;
$isiPad = stripos($ua, 'ipad') !== false;
$isWPx = stripos($ua, 'windows phone') !== false;
.htaccess
RewriteCond %{HTTP_USER_AGENT} ^.*Android.*$ RewriteRule ^(.*)$ http://android.site.com [R=301] RewriteCond %{HTTP_USER_AGENT} ^.*iPhone.*$ RewriteRule ^(.*)$ http://iphone.site.com [R=301] RewriteCond %{HTTP_USER_AGENT} ^.*iPod.*$ RewriteRule ^(.*)$ http://ipod.site.com [R=301] RewriteCond %{HTTP_USER_AGENT} ^.*iPad.*$ RewriteRule ^(.*)$ http://ipad.site.com [R=301]RewriteCond %{HTTP_USER_AGENT} ^.*Windows Phone.*$RewriteRule ^(.*)$ http://wpx.site.com [R=301]
HTML

<!--[if IEMobile]>
    Explorer mobile en Windows Phone 7
<![endif]-->
<![if !IEMobile]>
    Otros
<![endif]>

2011/03/19

Bucles rápidos en javascript

Hola, hoy os voy a contar varias maneras de hacer lo mismo, pero mejor. Bucles en javascript.
Todos estamos acostumbrados a hacerlos así:
for(i = 0; i < Obj.length; i++){}
Pero esa no es la manera mejor. Se puede mejorar haciendo lo siguiente que es un 20% más rápido
for(i = 0, ii = Obj.length; i < ii; i++){}
Pero espera, que aún puede ser mejor. Un 50% más rápido
for(i = 0, ii = Obj.length; ii--;){}

Otras alternativas de cómo hacer bucles...
for(var i = -1; ++i < Obj.length; ){}
Una variación algo más rápida
for(var i = -1, ii = Obj.length; ++i < ii; ){}

La velocidad de cada uno se puede ver en la siguiente gráfica:

Y de premio, una manera mejor de recorrer un array de objetos
while (p = o[i++]) { ... }

Como muy bien ha señalado Sócrates en su comentario, no queda claro en la gráfica qué bucle es cuál, por lo que lo mejor es mirar el jsperf que compara todas las variantes con datos actuales:

http://jsperf.com/loops/146

2011/03/17

El que rompe, paga y se lleva los trozos, a.k.a. limpiando el código en tiempo de ejecución.

Lo que sobra se tira... así que se debería hacer limpieza de las siguientes cosas:

1.- Propiedades de objeto que ya no se usan
var myApp = {
    propiedad: <algoenorme>;
};
// ...
delete myApp.propiedad;
2.- Elementos DOM que no se necesitan. Ojo, remove no es lo mismo que delete!
var el = $('myDiv'); // jQuery, para los despistados
...
// El elemento se quita del padre pero sigue estando ahí
el.parentNode.removeChild(el);
// Quitamos el elemento y lo borramos de la memoria
delete el.parentNode.removeChild(el); 

2011/03/16

Acceso al DOM mediante javascript por punteros

Dando vueltas al acceso a la información de DOM, resulta que si almacenamos en una variable local el puntero a la función que se encarga de de realizar la consulta al DOM, se reduce mucho el tiempo en un orden de 4 veces más rápido!

Ejemplo:

function foo() {
    var get = document.getElementsBtTagName;
    for (var i = 0; i < 100000; i++) {
        get('head');
    }
}
foo(); // llamada a la función
Y ésta, para nota...
... cualquier cambio que se haga por ejemplo al innerHTML de un elemento está "prohibido" hacerlo dentro de un bucle... lo que hay que hacer es realizar las modificaciones sobre una variable local que contendrá la cadena de texto y una vez se termine el bucle, se asigna el resultado. Hay que recordar que hasta no se termina la ejecución de la llamada al javascript no se realizan cambios en la UI.