jueves, 24 de mayo de 2012

Utilizando JSON para serializar y des-serializar objetos de javascript incluyendo sus métodos (Parte 2).


Tras ojear el libro "Professional Javascript for web developers" (tercera edición), en mi opinión bastante bueno, he visto que tenía bastantes cosas que corregir en la parte 1 de este tema.
Primero, tal y como se indica allí, si codificamos nuestros objetos con el "patrón siguiente":

     function Direccion(calle, codigoPostal) {
          this.tipo = "Direccion";
         this.calle = calle,
         this.codigoPostal = codigoPostal;
         this.getCodigoPostal = function() {
              return this.codigoPostal;
         }
     }

Al crear dos instancias del objeto "Dirección" se crearán también dos instancias distintas de la función (método, objeto) getCodigoPostal. Obviamente, esto es bastante malo si queremos aprovechar memoria. Aunque en el libro se comentan otros modos para codificar nuestras "clases" parece ser que lo más correcto es:

     function Direccion(calle, codigoPostal) {
          this.tipo = "Direccion";
         this.calle = calle,
         this.codigoPostal = codigoPostal;
     }

     Direccion.prototype = {
          constructor : Direccion,
         getCodigoPostal : function() {
              return this.codigoPostal;
         }
     };

Es decir, utilizar un constructor para los atributos y luego definir los métodos extendiendo el prototipo. De este modo se consigue que cada objeto tenga sus propios atributos y que todos los del mismo tipo compartan los mismos (objetos) métodos.

En ECMASCRIPT 6 aparecerá el concepto de clase.

Si, además, consideramos que estamos ejecutando ECMASCRIPT 5 entonces ya dispondremos de JSON (sin necesidad de incluir json2.js) y del método Array.isArray(value) para comprobar si "value" es un array o no.
Con estos cambios la página queda así:
Resumiendo, si incluimos el atributo “tipo” con el nombre de la clase de nuestros objetos en cada uno de ellos, con la función siguiente podremos des-serializar nuestros objetos almecenados con JSON, conservando su tipo y sus métodos.

     function recrearObjetoDesdeJSON(objetoDesdeJSON) {
          if (!objetoDesdeJSON.tipo)
              return objetoDesdeJSON;
         var nuevoObjeto = new this[objetoDesdeJSON.tipo];
         var valorAtributo;
         for(var atributo in objetoDesdeJSON) {
              valorAtributo = objetoDesdeJSON[atributo];
              if (typeof valorAtributo == "object") {
                   if (Array.isArray(valorAtributo)) {
                       var nuevoArray = new Array();
                      for (var i=0; i<valorAtributo.length; i++) {
                           nuevoArray[i] = recrearObjetoDesdeJSON(valorAtributo[i]);
                      }
                      nuevoObjeto[atributo] = nuevoArray;
                  }
                  else
                       nuevoObjeto[atributo] = recrearObjetoDesdeJSON(valorAtributo);
}
else
                  nuevoObjeto[atributo] = valorAtributo;
}
return nuevoObjeto;
}

Debemos seguir teniendo cuidado para no referenciar un atributo desde otro:
this.atr1 = ….
this.atr2 = new Array(this.atr1, …)
Ya que el atributo del array se creara como un objeto distinto al atributo de "la clase".

miércoles, 23 de mayo de 2012

Aplicar una transformación xsl desde javascript cogiendo el archivo xml del ordenador del cliente.


Aunque podría poner una xsl  general en este ejemplo voy a utilizar la siguiente (por_un_clavo.xsl):

<?xml version="1.0"?>
     <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
     <xsl:template match="/">
          <xsl:apply-templates/>
     </xsl:template>
     <xsl:template match="por_un_clavo">
          <ul>
       <xsl:apply-templates/>
  </ul>
     </xsl:template>
     <xsl:template match="linea">
          <li>
       <xsl:value-of select="."/>
  </li>
     </xsl:template>
</xsl:stylesheet>

Para procesar, con ella, el xml siguiente (por_un_clavo.xml):

<?xml version="1.0" encoding="UTF-8"?>
<!-- <?xml-stylesheet type="text/xsl" href="por_un_clavo.xsl"?> -->
<por_un_clavo>
     <linea>Por un clavo se perdió una herradura,</linea>
     <linea>por una herradura, se perdió un caballo,</linea>
     <linea>por un caballo, se perdió una batalla,</linea>
     <linea>por una batalla, se perdió el Reino.</linea>
</por_un_clavo>

Si descomentamos la línea comentada podemos abrir el archivo xml con Internet Explorer y éste realizará la transformación directamente. Esto nos servirá para validar rápidamente nuestra xsl.

Si no queremos tener problemas con los acentos, las ñ y demás deberemos poner el encoding dentro del xml con el que realmente hayamos guardado nuestro archivo (trabajar con utf-8 es la mejor elección, olvidaros del ISO-8859-1 por muy latín que sea). Una forma de hacerlo, desde Windows, es escribiendo el xml en Word, guardando como texto y allí eligiendo la codificación (en Windows, por defecto y para variar, la codificación es propietaria de Microsoft y no es utf-8):


Una vez seleccionado el archivo de disco y transformado con nuestra xsl el resultado, con Firefox, será:


La página, con el código completo, se puede ver en:

El body de la página es, simplemente:

<input id="ficheroxml" type="file" name="ficheroxml" onchange="procesarXml();"/>
<div id="resultado"></div>

En él tenemos un input para seleccionar el archivo y un div que albergará el resultado de la transformación. En el momento en el que se selecciona el archivo se producirá el evento “onchange” y se llamará a la función javascript procesarXml().

Dentro del javascript hemos incluido la xsl como una variable de texto con todo su contenido:

     var xsl = "<?xml version=\"1.0\" encoding=\"UTF-8\"?> …

y hemos creado una instancia de un lector de ficheros:

     var reader = new FileReader();

Cuando se llama a la función procesarXml se ejecuta el siguiente código:

     if (document.getElementById("ficheroxml").files.length == 0) return;
     var fichero = document.getElementById("ficheroxml").files[0];
     reader.readAsText(fichero);

donde validamos si se ha seleccionado algún archivo, cogemos el primero de ellos y le pedimos al lector que lo lea como un archivo de texto.
Si se produce algún error se lanzará el evento onerror en el lector que definimos del modo siguiente:

     reader.onerror = function(event) {
           alert("Error leyendo el archivo, code: " + reader.error.code);
     }

Si todo va bien se lanzará el evento onload sobre el reader:

      reader.onload = function(event) {
           xml = event.target.result;
   parsearXml();
      }

Aquí guardaremos el resultado de la lectura en la variable xml y llamaremos a la función que realiza el parseo e introduce el resultado en el div "resultado":

function parsearXml() {
      var xsltProcessor = new XSLTProcessor();
      var parser = new DOMParser();
      xsltProcessor.importStylesheet(parser.parseFromString(xsl, "text/xml"));
      var resultDocument = xsltProcessor.transformToFragment(parser.parseFromString(xml, "text/xml"),                  
                                       document);
      document.getElementById("resultado").appendChild(resultDocument);
}

Para que esto funcione en Chrome (18.0.1025.162 m) es necesario arrancar la instancia de  Chrome con el switch “--allow-file-access-from-files” ya que, si no, no se nos permitirá, por razones de seguridad, acceder al archivo del  disco.

martes, 22 de mayo de 2012

Hora de aventuras.

Hora de aventuras según el peque (7 añitos), con Finn, Jake, la princesa chicle, Lady Arcoiris, la princesa bultos y demás ...


Utilizando JSON para serializar y des-serializar objetos de javascript incluyendo sus métodos (Parte 1).

Ver Parte 2 de este tema.
En el desarrollo real del editor que estoy realizando me he visto en la necesidad de serializar mis objetos javascript a disco para lo que pensaba que seria suficiente con convertir mis objetos al formato JSON utilizando json2.js de https://github.com/douglascrockford/JSON-js) :

localStorage.setItem(nombredelapantalla, JSON.stringify(pantalla));

¿Cual ha sido mi sorpresa cuando he visto que este formato no admite que los objetos javascript tengan funciones? Es decir, si tenemos un objeto (función) del tipo:

function Persona(nombre, trabajo, nacimiento) {
     this.nombre = nombre;
     this.trabajo = trabajo;
     this.nacimiento = nacimiento;
     this.getNombre = function() {
          return this.nombre;
     }
}

Su JSON será:
{"nombre":"Pedro Picapriedra","trabajo":"Picapedrero","nacimiento":-1000}. 
Aquí no existe ninguna referencia a la clase, objeto o tipo “Persona”. Si des-serializamos este objeto tendremos un nuevo objeto con los atributos nombre, trabajo, y nacimiento pero sin el método getNombre() (Sabremos que su nombre es Pedro Picapiedra, que es Picapedrero pero no sabremos si es una Persona y no le podremos preguntar su nombre).
Tras varias vueltas por internet y revisar arriba y abajo JQuery sólo he visto el método toSource que convierte un objeto a un formato similar al de JSON con referencias al método y al tipo del objeto. Lástima que, una vez más, este método no esté disponible en todos los navegadores y sólo sea válido para Mozilla.
En definitiva, toca hacer en javascript algo que muchos otros lenguajes hacen de serie.

1ª Prueba: Introducimos un atributo para identificar el tipo del objeto en nuestra “clase” y lo utilizamos para regenerar el objeto copiando los valores de los atributos del objeto des-serializado por json2:

if (objetoSinMetodos.tipo == "persona") {
     var nuevoPedro = new Persona();
     // Copiamos los valores de los atributos
     for(var member in objetoSinMetodos)
          nuevoPedro[member] = objetoSinMetodos[member];
     alert(nuevoPedro.getNombre() + ", " + nuevoPedro.trabajo + "," + nuevoPedro.nacimiento);
}

El ejemplo en:

2ª Prueba: Introducimos el objeto "Direccion" con sus atributos y métodos como atributo del objeto Persona. Al des-serializar queremos poder llamar a objPersona.getDireccion().getCodigoPostal(). 
El código siguiente se explica por sí mismo, si un atributo es un objeto le metemos la propiedad “tipo” y así sabemos de qué tipo es (aunque en el código lo creemos a capón en esta versión):
var valorAtributo;

if (objetoSinMetodos.tipo == "persona") {
     var nuevoPedro = new Persona();
     // Copiamos los valores de los atributos
     for(var atributo in objetoSinMetodos) {
          valorAtributo = objetoSinMetodos[atributo];
    if (typeof valorAtributo == "object") {
         var nuevaDireccion = new Direccion();
         for (var atributo2 in valorAtributo)
              nuevaDireccion[atributo2] = valorAtributo[atributo2];
         nuevoPedro[atributo] = nuevaDireccion;
    } 
    else
         nuevoPedro[atributo] = valorAtributo;
     }
     alert(nuevoPedro.getDireccion().getCodigoPostal());
}

El ejemplo completo en:

3.- Quitando el “a capón”: Algo muy bueno que tiene Javascript es que a partir de una variable, un string, un nombre, podemos hacer casi todo. Podemos, en este caso, crear un nuevo objeto del tipo persona o del tipo dirección a partir de los valores, string, de los atributos tipo:

function recrearObjetoDesdeJSON(objetoDesdeJSON) {
     var nuevoObjeto = new this[objetoDesdeJSON.tipo];
     var valorAtributo;
     for(var atributo in objetoDesdeJSON) {
          valorAtributo = objetoDesdeJSON[atributo];
    if (typeof valorAtributo == "object")
         nuevoObjeto[atributo] = recrearObjetoDesdeJSON(valorAtributo);
    else
         nuevoObjeto[atributo] = valorAtributo;
     }
     return nuevoObjeto;
}

El ejemplo completo en:

4. Incluyendo arrays: Al ir a introducir el código anterior en mi estructura de objetos he visto que algunos de ellos tenían atributos que eran arrays por lo que la función dejaba de funcionar. Para resolver esto, debemos saber si un objeto de Javascript es un array o no. En 
nos explican la solución:

function isArray(a) {
     return Object.prototype.toString.apply(a) === '[object Array]';
}

Quedando, finalmente:

function recrearObjetoDesdeJSON(objetoDesdeJSON) {
  if (!objetoDesdeJSON.tipo)
return objetoDesdeJSON;
var nuevoObjeto = new this[objetoDesdeJSON.tipo];
var valorAtributo;
for(var atributo in objetoDesdeJSON) {
valorAtributo = objetoDesdeJSON[atributo];
if (typeof valorAtributo == "object") {
if (isArray(valorAtributo)) {
var nuevoArray = new Array();
for (var i=0; i<valorAtributo.length; i++) {
nuevoArray[i] = recrearObjetoDesdeJSON(valorAtributo[i]);
}
nuevoObjeto[atributo] = nuevoArray;
}
else
nuevoObjeto[atributo] = recrearObjetoDesdeJSON(valorAtributo);
}
else
nuevoObjeto[atributo] = valorAtributo;
}
return nuevoObjeto;
}

Recordar que para que esto funcione cada uno de nuestros objetos deberá disponer de un atributo “tipo” con el nombre de la clase.
Es muy probable que en el caso de un atributo que sea un array de arrays el código no funcione correctamente. No es mi caso ;D. 

¡CUIDADO! En mi caso tenia varios atributos que luego volvia a referencias desde otro que era un array:
this.atr1
this.atr2
this.atr3 = new Array(this.atr1, this.atr2);
Al des-serializarse el objeto con mi función los atributos del array no son los mismos objetos (son objetos del mismo tipo pero son instancias distintas).  En mi caso la solución ha sido poner una función:
this.getAtributos = function() {
return new Array(this.atr1, this.atr2);
}

Estoy leyendo un buen libro de javascript "Professional javascript for web developers", tercera edición, en el que comentan el problema de codificar los objetos javascript tal y como lo estoy haciendo yo aquí, definiendo las funciones dentro del objeto con "this.funcion". Parece ser que en este caso cada vez que se crea un objeto se crea tambien uno por cada una de las funciones que aparecen en ese objeto de modo tal que si creamos dos instancias de uno de mis objetos se crearan también instancias nuevas de cada una de las funciones. En definitiva debería escribir mis "clases" de otro modo y cuando lo haga deberé buscar el modo de serializar mis objetos de nuevo. En ese momento, espero que pronto, pondré aquí la parte 2.
Por cierto, con ECMASCRIPT 6 parece ser que Javascript resolverá muchos de sus problemas. Se prodrán definir clases, constantes, etc.

Ver Parte 2 de este tema.