Optimizaciones de JavaScript utilizadas en el proyecto de Prometheus

Introducción

Para los que no lo sepáis, próximamente se estrena la película Prometheus en Estados Unidos y desde Plain Concepts hemos desarrollado el training center del sitio web. Se puede acceder desde este enlace: http://www.projectprometheus.com/trainingcenter/. El proyecto ha estado financiado por Microsoft, más concretamente por el equipo de Internet Explorer, así que como página web que es, se ha desarrollado utilizando las últimas tecnologías web: HTML5 + CSS3.

Training center

El centro de entrenamiento es un sitio web donde los candidatos al proyecto Prometheus, de la empresa Weyland, puede probar su valía. El entrenamiento cuenta con 5 pruebas (juegos) que el recluta tiene que completar en un tiempo determinado. Una vez superadas las cinco pruebas el recluta puede formar parte de Weyland Industries. Los cinco juegos han sido desarrollados por gente de Plain Concepts:

Cada uno de los cuales ha desarrollado uno de los juegos del centro de entrenamiento. En mi caso he desarrollado el cubo de rubick en 2 y 3 dimensiones.

clip_image001

Aquí se puede ver una captura del juego en Internet Explorer llamado Prefrontal Cortex.

HTML5 / Javascript

Todos los juegos han sido desarrollados en HTML5 utilizando JavaScript para la parte de programación, en mi caso he utilizado Canvas para dibujar los cubos. Eso significa que todos los juegos funcionan perfectamente en todos los navegadores modernos, incluyendo Google Chrome, Firefox, Safari, Opera y Internet Explorer 9 y 10.

Para el desarrollo de los juegos se creo un motor en JavaScript que nos permitiera dibujar en un Canvas la geometría de los modelos de los cubos. Este motor no utiliza WebGL para renderizar los cubos, porque Internet Explorer no tiene soporte (además de en el resto es experimental), por lo que se opto por hacer un motor grafico completo desde cero. Es decir, todo el pipeline de grafica se tiene que hacer en JavaScript, esto significa, entre otras cosas, que tenemos que emular por software como funciona una tarjeta gráfica y eso normalmente es más lento que el propio hardware. Así que el desafío de implementar un pipeline gráfico por software es mayor ya que tiene que tener un rendimiento aceptable.

JavaScript 101

JavaScript es un lenguaje en el que existen varios tipos de datos básicos con los que podemos trabajar.

  • Object
  • Array
  • Number
  • String
  • Boolean

Los objetos son la forma más común a la hora de trabajar en JavaScript y se pueden utilizar de muchas maneras.

La forma más sencilla de crear un objeto es:

   1: var myObject = {}

A partir de ahí se pueden ir agregando propiedades al objeto sin ningún tipo de restricción. No son propiedades como las que se puede estar acostumbrado en C#, sino que el tipo object se comporta como una especie de diccionario de pares nombre valor.

Se pueden crear propiedades de la siguiente manera a un objeto previamente definido.

   1: myObject.name = ‘Luis’;

   2: myObject.number = 42;

Así el objeto pasará a tener dos propiedades, una llamada ‘name’ con el valor ‘Luis’ y otra llamada ‘number’ con el valor ‘42’.

También se puede acceder a esas propiedades como si de un diccionario se tratase. Las dos formas son igual de válidas y correctas.

   1: var name = myObject[‘name’];

Después de la ejecución de esta línea de código lo que se establece en la variable name es el valor de ‘Luis’, previamente establecido.

Con esta nueva forma de acceder a las propiedades no solo se pueden leer valores almacenados en un objeto sino que también se pueden guardar.

myObject[‘currentDate’] = new Date();

Vector3

Como ejemplo de objeto se va a definir Vector3; un vector de 3 dimensiones.

var vector3 = {x: 1, y: 1, z: 1}

¿Cuál es el problema con este Vector3?

El rendimiento. Como se ha dicho antes todos los objetos en JavaScript se comportan como un diccionario de pares nombre / valor, así que en cada una de las operaciones en las que se tenga que leer o escribir el valor de x, y o z, el runtime de JavaScript tiene que comprobar que la propiedad existe o no y luego leerla o almacenarla. Todo esto lleva tiempo. Es como si se programase todo el acceso a propiedades y campos en .NET utilizando únicamente la API de Reflexion (System.Reflection).

En el caso del motor de 3D en JavaScript Vector2, Vector3, Vector4, Color y Matrix son tipos que se están usando constantemente para dibujar la geometría de los cubos, así que esos tipos fueron los primero en ser optimizados.

La solución por la que se opto fue eliminar la definición de los tipos, es decir, que por ejemplo Vector2, Vector3, Vector4 y Color pasaron a ser un array de 2, 3, 4 y 4 posiciones respectivamente. Así que por convención lo que se hizo fue que la posición dentro del array representaba una coordenada de las dimensiones del vector.

  • X: array[0]
  • Y: array[1]
  • Z: array[2]
  • W: arrat[3]

En el caso de Matrix que se tenía M11, M12…M21,M22..M31..M44 pasaron a ser también las posiciones de un array.

Veamos como se ha cambiado la multiplicación de matrices, uno de los cuellos de botella, en cuanto a rendimiento se refiere.

Antes

   1: function Multiply(matrix1, matrix2) {

   2:     var matrix = new Matrix();

   3:     matrix.M11 = (((matrix1.M11 * matrix2.M11) + (matrix1.M12 * matrix2.M21)) + (matrix1.M13 * matrix2.M31)) + (matrix1.M14 * matrix2.M41);

   4:     matrix.M12 = (((matrix1.M11 * matrix2.M12) + (matrix1.M12 * matrix2.M22)) + (matrix1.M13 * matrix2.M32)) + (matrix1.M14 * matrix2.M42);

   5:     matrix.M13 = (((matrix1.M11 * matrix2.M13) + (matrix1.M12 * matrix2.M23)) + (matrix1.M13 * matrix2.M33)) + (matrix1.M14 * matrix2.M43);

   6:     matrix.M14 = (((matrix1.M11 * matrix2.M14) + (matrix1.M12 * matrix2.M24)) + (matrix1.M13 * matrix2.M34)) + (matrix1.M14 * matrix2.M44);

   7:     matrix.M21 = (((matrix1.M21 * matrix2.M11) + (matrix1.M22 * matrix2.M21)) + (matrix1.M23 * matrix2.M31)) + (matrix1.M24 * matrix2.M41);

   8:     matrix.M22 = (((matrix1.M21 * matrix2.M12) + (matrix1.M22 * matrix2.M22)) + (matrix1.M23 * matrix2.M32)) + (matrix1.M24 * matrix2.M42);

   9:     matrix.M23 = (((matrix1.M21 * matrix2.M13) + (matrix1.M22 * matrix2.M23)) + (matrix1.M23 * matrix2.M33)) + (matrix1.M24 * matrix2.M43);

  10:     matrix.M24 = (((matrix1.M21 * matrix2.M14) + (matrix1.M22 * matrix2.M24)) + (matrix1.M23 * matrix2.M34)) + (matrix1.M24 * matrix2.M44);

  11:     matrix.M31 = (((matrix1.M31 * matrix2.M11) + (matrix1.M32 * matrix2.M21)) + (matrix1.M33 * matrix2.M31)) + (matrix1.M34 * matrix2.M41);

  12:     matrix.M32 = (((matrix1.M31 * matrix2.M12) + (matrix1.M32 * matrix2.M22)) + (matrix1.M33 * matrix2.M32)) + (matrix1.M34 * matrix2.M42);

  13:     matrix.M33 = (((matrix1.M31 * matrix2.M13) + (matrix1.M32 * matrix2.M23)) + (matrix1.M33 * matrix2.M33)) + (matrix1.M34 * matrix2.M43);

  14:     matrix.M34 = (((matrix1.M31 * matrix2.M14) + (matrix1.M32 * matrix2.M24)) + (matrix1.M33 * matrix2.M34)) + (matrix1.M34 * matrix2.M44);

  15:     matrix.M41 = (((matrix1.M41 * matrix2.M11) + (matrix1.M42 * matrix2.M21)) + (matrix1.M43 * matrix2.M31)) + (matrix1.M44 * matrix2.M41);

  16:     matrix.M42 = (((matrix1.M41 * matrix2.M12) + (matrix1.M42 * matrix2.M22)) + (matrix1.M43 * matrix2.M32)) + (matrix1.M44 * matrix2.M42);

  17:     matrix.M43 = (((matrix1.M41 * matrix2.M13) + (matrix1.M42 * matrix2.M23)) + (matrix1.M43 * matrix2.M33)) + (matrix1.M44 * matrix2.M43);

  18:     matrix.M44 = (((matrix1.M41 * matrix2.M14) + (matrix1.M42 * matrix2.M24)) + (matrix1.M43 * matrix2.M34)) + (matrix1.M44 * matrix2.M44);

  19:     return matrix;

  20: }

La multiplicación simplemente accedía a cada uno de los índices de la matriz, los multiplicaba y luego los asignada de vuelta a la matriz de resultado. Como hemos dicho antes, esto implica leer una gran cantidad de propiedades durante el dibujado de un frame de la escena.

Ahora

   1: function Multiply(matrix1, matrix2) {

   2:     var matrix = new Matrix();

   3:     var position = matrix.position;

   4:     var position1 = matrix1.position;

   5:     var position2 = matrix2.position;

   6:     position[0] = (((position1[0] * position2[0]) + (position1[1] * position2[4])) + (position1[2] * position2[8])) + (position1[3] * position2[12]);

   7:     position[1] = (((position1[0] * position2[1]) + (position1[1] * position2[5])) + (position1[2] * position2[9])) + (position1[3] * position2[13]);

   8:     position[2] = (((position1[0] * position2[2]) + (position1[1] * position2[6])) + (position1[2] * position2[10])) + (position1[3] * position2[14]);

   9:     position[3] = (((position1[0] * position2[3]) + (position1[1] * position2[7])) + (position1[2] * position2[11])) + (position1[3] * position2[15]);

  10:     position[4] = (((position1[4] * position2[0]) + (position1[5] * position2[4])) + (position1[6] * position2[8])) + (position1[7] * position2[12]);

  11:     position[5] = (((position1[4] * position2[1]) + (position1[5] * position2[5])) + (position1[6] * position2[9])) + (position1[7] * position2[13]);

  12:     position[6] = (((position1[4] * position2[2]) + (position1[5] * position2[6])) + (position1[6] * position2[10])) + (position1[7] * position2[14]);

  13:     position[7] = (((position1[4] * position2[3]) + (position1[5] * position2[7])) + (position1[6] * position2[11])) + (position1[7] * position2[15]);

  14:     position[8] = (((position1[8] * position2[0]) + (position1[9] * position2[4])) + (position1[10] * position2[8])) + (position1[11] * position2[12]);

  15:     position[9] = (((position1[8] * position2[1]) + (position1[9] * position2[5])) + (position1[10] * position2[9])) + (position1[11] * position2[13]);

  16:     position[10] = (((position1[8] * position2[2]) + (position1[9] * position2[6])) + (position1[10] * position2[10])) + (position1[11] * position2[14]);

  17:     position[11] = (((position1[8] * position2[3]) + (position1[9] * position2[7])) + (position1[10] * position2[11])) + (position1[11] * position2[15]);

  18:     position[12] = (((position1[12] * position2[0]) + (position1[13] * position2[4])) + (position1[14] * position2[8])) + (position1[15] * position2[12]);

  19:     position[13] = (((position1[12] * position2[1]) + (position1[13] * position2[5])) + (position1[14] * position2[9])) + (position1[15] * position2[13]);

  20:     position[14] = (((position1[12] * position2[2]) + (position1[13] * position2[6])) + (position1[14] * position2[10])) + (position1[15] * position2[14]);

  21:     position[15] = (((position1[12] * position2[3]) + (position1[13] * position2[7])) + (position1[14] * position2[11])) + (position1[15] * position2[15]);

  22:     return matrix;

  23: }

Lo primero que se observa es que el código pasa a ser más críptico que el anterior, es decir, que ahora únicamente se tiene son los diferentes índices de position dentro de tres arrays, que representan las tres matrices con las que se esta trabajando en este momento.

Así que se ha pasado de,

matrix.M11 = (((matrix1.M11 * matrix2.M11) + (matrix1.M12 * matrix2.M21)) + 

(matrix1.M13 * matrix2.M31)) + (matrix1.M14 * matrix2.M41);

a esto:

position[0] = (((position1[0] * position2[0]) + (position1[1] * position2[4])) + 

(position1[2] * position2[8])) + (position1[3] * position2[12]);

Ya que ahora todas las posiciones de la matriz están almacenadas en un array de 16 posiciones lo que se tiene que hacer si se quiere acceder al valor M11 es acceder a la posición 0 de array, en el caso del valor M31 a la posición 8 de array y así sucesivamente.

Otras optimizaciones

Tamaño de los arrays

Si se tiene un array que tiene una propiedad length por la que se quiere iterar para realizar una acción por cada uno de los elementos del array, es recomendable no poner directamente el valor de myArray.length para comprobar si se ha llegado al final de array, sino guardar el tamaño del array en una variable y usar esta variable.

   1: var myArray = new Array();

   2:  

   3: for (var i = 0; i < myArray.length; i++) {

   4:     myArray[i] = i;

   5: }

   6:  

   7: var length = myArray.length;

   8: for (var i = 0; i < length; i++) {

   9:     myArray[i] = i;

  10: }

Cachear variables

Si durante la ejecución de un método se tiene variables que vamos a usar y estas variables son propiedades de un objeto, es mejor definirlas como variables en el ámbito del método que no referenciarlas desde el objeto original.

   1: var myObject =

   2:     {

   3:     name: 'luis',

   4:     company: {

   5:         name: 'PlainConcepts',

   6:         location: 'address'

   7:         }

   8:     };

   9:  

  10: var companyAddress = myObject.company.location;

  11: var company = myObject.company;

  12: companyAddress = company.location;

Espero que estas notas sobre optimización de JavaScript os sean útiles.

Luis Guerrero.

[Windows 8] Navegando en una aplicación HTML/JavaScript

Con la aparición de Windows 8 Consumer Preview Microsoft ha presentado Visual Studio 2012 para desarrollar aplicaciones Metro. En este artículo hablaremos de cómo se produce la navegación de contenido en una aplicación de Metro hecha en HTML / JavaScript.

Navegando en HTML

Cuando creamos una aplicación en HTML tradicional, el método de navegación es incluir un enlace <a> para poder empezar a navegar. Lo malo que tiene este método es que durante un breve periodo de tiempo la ventana del navegador permanecerá en blanco a la espera del contenido del nuevo HTML al que se está navegando. Esto puede resultar muy molesto en algunas ocasiones y aunque se tenga una conexión a Internet muy rápida puede parecer que la página parpadea por un instante.

Para este tipo de problemas se invento AJAX, que permite modificar selectivamente una sección de una página sin que se refresque la página entera. Esta funcionalidad es ideal para bajar datos de internet y luego “conectar” esos datos con el árbol de objetos en HTML.

Aplicaciones Metro con HTML

Sabiendo estas dos cosas tenemos que hacer que nuestras aplicaciones Metro no naveguen hasta un HTML nuevo, sino que tengamos un mecanismo para poder cargar y descargar contenido del DOM para simular la navegación. Gracias a las nuevas API’s que Microsoft ha incluido en WinJS (la parte JavaScript de WinRT) podemos hacer esto de manera muy sencilla.

Empezaremos echando un vistazo a la plantilla de nuevo proyecto de aplicación Metro para HMTL / JavaScript.

Como podemos ver en la captura del menú de nuevo proyecto de Visual Studio 11, tenemos varias plantillas de diferentes tipos de proyecto. Ahora mismo nos centraremos en el tipo de proyecto “Navigation Application”. Llamaremos a nuestra aplicación NavigationDemo.

Como se puede ver en esta captura de pantalla, estos son los ficheros que se crean por defecto en esta plantilla. Ahora vamos a proceder a ver como se produce la navegación.

Primera aplicación Metro

La aplicación que vamos a desarrollar es muy sencilla. Simplemente navega de una pieza de contenido a otra, pero utilizando las APIs que tenemos en WinRT para hacer la navegación.

La primera página Default.html

La primera página que se ejecuta en una aplicación Metro es default.html, así que ese es nuestro punto de entrada, para poder empezar a entender la navegación en Metro.

Si nos fijamos en el código fuente de default.html, tenemos lo siguiente:

   1: <!DOCTYPE html>

   2: <html>

   3: <head>

   4:     <meta charset="utf-8">

   5:     <title>NavigationDemo</title>

   6:  

   7:     <!-- WinJS references -->

   8:     <link href="//Microsoft.WinJS.0.6/css/ui-dark.css" rel="stylesheet">

   9:     <script src="//Microsoft.WinJS.0.6/js/base.js"></script>
   1:  

   2:     <script src="//Microsoft.WinJS.0.6/js/ui.js">

   1: </script>

   2:  

   3:     <!-- NavigationDemo references -->

   4:     <link href="/css/default.css" rel="stylesheet">

   5:     <script src="/js/default.js">

   1: </script>

   2:     <script src="/js/navigator.js">

</script>

  10: </head>

  11: <body>

  12:     <div id="contenthost" 

  13:          data-win-control="NavigationDemo.PageControlNavigator" 

  14:          data-win-options="{home: '/html/homePage.html'}"></div>

  15:     <!-- <div id="appbar" data-win-control="WinJS.UI.AppBar">

  16:         <button data-win-control="WinJS.UI.AppBarCommand" data-win-options="{id:'cmd', label:'Command', icon:'placeholder'}"></button>

  17:     </div> -->

  18: </body>

  19: </html>

Vemos que es HTML normal, de toda la vida, solamente que en la cabecera del documento tenemos unas referencias de unos ficheros JavaScript y hojas de estilos un poco especiales. Me refiero a esto:

   1: <script src="//Microsoft.WinJS.0.6/js/base.js"></script>

Si nos fijamos, con esta acción estamos referenciando la API de WinJS, que es la librería de JavaScript para el desarrollo de aplicaciones Metro.

Un poco más abajo tenemos dos ficheros JS y un fichero CSS referenciados. Estos ficheros forman parte de la lógica de la aplicación.

La cosa cambia cuando nos vamos al cuerpo de nuestra página html, donde vemos que tenemos únicamente un div, pero con unos atributos que no habíamos visto antes:

   1: <div id="contenthost"

   2:     data-win-control="NavigationDemo.PageControlNavigator"

   3:     data-win-options="{home: '/html/homePage.html'}"></div>

Tenemos por un lado el identificador del control contenthost que según el nombre podemos adivinar que será el hueco donde después pondremos el contenido por el que estemos navegando.

También vemos que el siguiente atributo, data-win-control, tiene como valor algo que parece una clase. Si recordáis el nombre del proyecto (NavigationDemo) vemos el valor del atributo es NavigationDemo.PageControlNavigator, lo que me indica que la clase se llama PageControlNavigator. La primera reacción a este atributo es pensar que, al estar en JS no tenemos clases, pero podemos saber por XAML que esto se parece mucho a nuestra MainPage.

El último atributo que nos queda por investigar es, data-win-options. Si nos fijamos en el contenido del mismo, vemos que es un objeto de JS que tiene una propiedad llamada home con un valor que es una ruta html relativa del proyecto. Todo parece indicar que es la primera página que se va a cargar, como efectivamente podemos comprobar si abrimos el fichero homePage.html, que mostrará justamente la primera captura de pantalla que hemos visto antes.

Pero, ¿Cómo se ha realizado esta navegación? Es el momento de entrar a hablar sobre JavaScript.

JavaScript

El primer fichero que vamos a abrir es el js/default.js, que es el punto de entrada de la aplicación.

Un detalle a destacar sobre los ficheros de JavaScript es que todos están envueltos en una función sin nombre, que se ejecuta tan pronto como se define.

   1: (function () {

   2: })();

El motivo de proceder de esta forma es aislar el ámbito global de JavaScript y así evitar problemas de colisión de nombres de objetos y funciones. No definir todas las variables en el espacio de nombre global es una buena práctica.

Lo siguiente que nos sorprende es que tenemos una sentencia que no habíamos visto antes (“use strict”), que indica al compilador y al runtime que hagan una verificación estricta de tipos, como si se tratase de un lenguaje fuertemente tipado. Esto, por supuesto, se realiza cuando se ejecuta la aplicación, pues no hay proceso de compilación como tal.

Después de esto el fichero default.js simplemente establece dos callbacks para el evento “onactivated” y “oncheckpoint” y luego llama a WinJS.Application.start();

Navegación

La verdadera magia del código no reside en este fichero default.js, sino en el fichero navigator.js. Veamos cómo está definido.

Lo primero que nos encontramos en la definición del fichero es la función sin nombre que envuelve todo el código. A partir de ahora la obviaremos para no ser redundantes.

En el cuerpo de la definición de la función vemos como se definen una serie de variables que son accesos directos a propiedades del código, como por ejemplo:

  • Windows.UI.ViewManagement.ApplicationView
  • Windows.Graphics.Display.DisplayProperties
  • WinJS.Navigation
  • WinJS.UI
  • WinJS.Utilities

Todo lo que empiece por WinJS, está definido en el fichero base.js, que forma parte de la referencia de “Microsoft Windows Library for JavaScript SDK” que acompaña al proyecto. El fichero base.js está perfectamente formateado y documentado, así que podremos consultar como está hecho pero no podremos modificarlo desde esta localización.

Las variables que empiezan por Windows hacen referencia a la API de WinRT, a la que podemos acceder desde JavaScript. De este modo, cuando nos referimos a Windows.Graphics.Display.DisplayProperties estamos hablando de una clase que podremos utilizar con C# y C++ (los otros dos lenguajes permitidos en WinRT).

A continuación, nos encontramos con dos de las funcionalidades que mejorarán la calidad del código que generemos en JavaScript: espacios de nombres y Classes. Veamos de qué se trata antes de continuar.

WinJS.Namespace

Este objeto, como su nombre indica, nos permite definir espacios de nombres para su uso dentro de una aplicación Metro. ¿A qué nos referimos con espacios de nombre?¿A un espacio de nombres tradicional? Los programadores de C# podemos pensar que estamos definiendo un espacio de nombre de la misma manera que lo definimos en este lenguaje, pero desde luego no estamos en un entorno de .NET. Lo que realmente estamos haciendo es definir una serie de objetos que después vamos a poder utilizar con una nomenclatura de espacio de nombres. Es decir, que vamos a poder hacer definiciones de objetos de forma parecida a como hace Microsoft con, por ejemplo, WinJS.Navigation, que también forma parte de espacio de nombres de WinJS.

A la hora de definir un espacio de nombres en JavaScript, utilizando WinJS emplearemos una de las dos funciones que vienen en WinJS: define o defineWithParent.

Define

Si queremos definir un segmento del espacio de nombres de nuestra aplicación, en nuestro caso NavigationDemo.PageControlNavigator, tendremos que declarar la base del espacio de nombres como todos los segmentos menos el último, en este caso, NavigationDemo, pero podríamos tener espacios de nombres más grandes.

Una vez definido el string del espacio de nombres base como primer parámetro, el segundo parámetro de la función ha de ser un objeto que contenga el último segmento del espacio de nombres correspondiente a la clase en cuestión.

El ejemplo quedaría así:

   1: WinJS.Namespace.define("DemoNamespace", {

   2:     Class1: {},

   3:     Class2: {}

   4: });

De esta manera podemos tener organizado nuestro código dentro de nuestra aplicación por espacio de nombres. Realmente no son espacios de nombres en el sentido tradicional del lenguaje C# (al que más estamos acostumbrados), sino que son objetos definidos de esa manera para dar sensación de jerarquía cuando se utilizan.

Otra función que podemos utilizar es defineWithParent que nos permite extender un espacio de nombres ya existente.

Una vez comprendido esto, el siguiente paso es definir clases para empezar a escribir la funcionalidad de nuestro código.

WinJS.Class

La definición de clases es otro aspecto importante de la programación en JavaScript. Como bien es sabido, no hay clases como tales en JavaScript, pero en WinJS podemos hacer que una función tenga el aspecto de una clase.

Según Microsoft las clases en WinJS tienen tres características:

  • Constructor: es una función que nos permite inicializar la clase en cuestión, nosotros no somos responsables de devolver this en la definición porque WinJS lo hace automáticamente por nosotros.
  • Métodos de instancia: es un objeto que contiene los métodos de instancia que vamos a utilizar en la definición de la clase. No hay descriptores de visibilidad en JavaScript así que todos lo métodos son públicos.
  • Métodos estáticos: son métodos que se pueden utilizar sin necesidad de crear una instancia de la clase directamente escribiendo el nombre de la clase.

Así es como quedaría la definición de una clase con WinJS.Class.

   1: WinJS.Class.define(

   2:     function (argum1) { },

   3:     {},

   4:     {}

   5: );

Herencia y mixing

A la hora de definir clases también es posible definir herencia de clases. Como hasta ahora esto no es herencia tradicional como la entendemos en C#, sino que simplemente se define como una mezcla de los métodos que han definido en las clase base más los métodos de la clase hija. Para ello utilizaremos el método WinJS.Class.derive.

La última de las opciones es una mezcla (mix), que consiste en coger dos objetos que no tienen ninguna relación y hacer una unión de los dos en una nueva definición de clase.

Navigator.js

Ahora ha llegado el momento de hablar sobre la clase más importante de todo el proyecto, navigator.js. Como vimos anteriormente, esta clase se utilizaba en el fichero default.html para hacer la navegación de esa página hasta homePage.html. Veamos ahora cómo se realiza esa navegación.

Definición de los objetos más usados

Al principio del fichero podemos ver que se definen una serie de propiedades con objetos que vamos a utilizar durante el desarrollo.

   1: var appView = Windows.UI.ViewManagement.ApplicationView;

   2: var displayProps = Windows.Graphics.Display.DisplayProperties;

   3: var nav = WinJS.Navigation;

   4: var ui = WinJS.UI;

   5: var utils = WinJS.Utilities;

Una vez definidos estos objetos lo siguiente que nos encontramos es directamente la definición de clase.

   1: (function () {

   2:     "use strict";

   3:  

   4:     var appView = Windows.UI.ViewManagement.ApplicationView;

   5:     var displayProps = Windows.Graphics.Display.DisplayProperties;

   6:     var nav = WinJS.Navigation;

   7:     var ui = WinJS.UI;

   8:     var utils = WinJS.Utilities;

   9:     

  10:     WinJS.Namespace.define("NavigationDemo", {

  11:         PageControlNavigator: WinJS.Class.define(

  12:         // Define the constructor function for the PageControlNavigator.

  13:             function (element, options) {

  14:                 this.element = element || document.createElement("div");

  15:                 this.element.appendChild(this._createPageElement());

  16:  

  17:                 this.home = options.home;

  18:  

  19:                 nav.onnavigated = this._navigated.bind(this);

  20:                 appView.getForCurrentView().onviewstatechanged = this._viewstatechanged.bind(this);

  21:  

  22:                 document.body.onkeyup = this._keyupHandler.bind(this);

  23:                 document.body.onkeypress = this._keypressHandler.bind(this);

  24:                 nav.navigate(this.home);

  25:             }, {

  26:                 // This function creates a new container for each page.

  27:                 _createPageElement: function () {

  28:                     var element = document.createElement("div");

  29:                     element.style.width = "100%";

  30:                     element.style.height = "100%";

  31:                     return element;

  32:                 },

  33:  

  34:                 // This function responds to keypresses to only navigate when

  35:                 // the backspace key is not used elsewhere.

  36:                 _keypressHandler: function (eventObject) {

  37:                     if (eventObject.key === "Backspace")

  38:                         nav.back();

  39:                 },

  40:  

  41:                 // This function responds to keyup to enable keyboard navigation.

  42:                 _keyupHandler: function (eventObject) {

  43:                     if ((eventObject.key === "Left" && eventObject.altKey) || (eventObject.key === "BrowserBack")) {

  44:                         nav.back();

  45:                     } else if ((eventObject.key === "Right" && eventObject.altKey) || (eventObject.key === "BrowserForward")) {

  46:                         nav.forward();

  47:                     }

  48:                 },

  49:  

  50:                 // This function responds to navigation by adding new pages

  51:                 // to the DOM.

  52:                 _navigated: function (eventObject) {

  53:                     var newElement = this._createPageElement();

  54:                     var parentedComplete;

  55:                     var parented = new WinJS.Promise(function (c) { parentedComplete = c; });

  56:  

  57:                     var that = this;

  58:                     WinJS.UI.Pages.render(eventObject.detail.location, newElement, eventObject.detail.state, parented).

  59:                         then(function (control) {

  60:                             that.element.appendChild(newElement);

  61:                             that.element.removeChild(that.pageElement);

  62:                             parentedComplete();

  63:                             document.body.focus();

  64:                             that.navigated();

  65:                         });

  66:                 },

  67:  

  68:                 // This function is called by _viewstatechanged in order to

  69:                 // pass events to the page.

  70:                 _updateLayout: {

  71:                     get: function () { return (this.pageControl && this.pageControl.updateLayout) || function () { }; }

  72:                 },

  73:  

  74:                 _viewstatechanged: function (eventObject) {

  75:                     (this._updateLayout.bind(this.pageControl))(this.pageElement, eventObject.viewState);

  76:                 },

  77:  

  78:                 // This function updates application controls once a navigation

  79:                 // has completed.

  80:                 navigated: function () {

  81:                     // Do application specific on-navigated work here

  82:                     var backButton = this.pageElement.querySelector("header[role=banner] .win-backbutton");

  83:                     if (backButton) {

  84:                         backButton.onclick = function () { nav.back(); };

  85:  

  86:                         if (nav.canGoBack) {

  87:                             backButton.removeAttribute("disabled");

  88:                         }

  89:                         else {

  90:                             backButton.setAttribute("disabled", "disabled");

  91:                         }

  92:                     }

  93:                 },

  94:  

  95:                 // This is the PageControlNavigator object.

  96:                 pageControl: {

  97:                     get: function () { return this.pageElement && this.pageElement.winControl; }

  98:                 },

  99:  

 100:                 // This is the root element of the current page.

 101:                 pageElement: {

 102:                     get: function () { return this.element.firstElementChild; }

 103:                 }

 104:             }

 105:         ),

 106:  

 107:         // This function navigates to the home page which is defined when the

 108:         // control is created.

 109:         navigateHome: function () {

 110:             var home = document.querySelector("#contenthost").winControl.home;

 111:             var loc = nav.location;

 112:             if (loc !== "" && loc !== home) {

 113:                 nav.navigate(home);

 114:             }

 115:         },

 116:     });

 117: })();

Constructor

En el constructor de la clase se realizan varias tareas para definir la navegación.

   1: function (element, options) {

   2:     this.element = element || document.createElement("div");

   3:     this.element.appendChild(this._createPageElement());

   4:  

   5:     this.home = options.home;

   6:  

   7:     nav.onnavigated = this._navigated.bind(this);

   8:     appView.getForCurrentView().onviewstatechanged = this._viewstatechanged.bind(this);

   9:  

  10:     document.body.onkeyup = this._keyupHandler.bind(this);

  11:     document.body.onkeypress = this._keypressHandler.bind(this);

  12:     nav.navigate(this.home);

  13: }

Vemos que la función tiene dos parámetros, element y options, que son justamente el elemento host de la navegación, en este caso un div, y un objeto con las opciones, respectivamente. Si recordamos la definición del html, había un atributo que se llamaba data-win-options, cuyo valor es “{home: ‘/html/homePage.html’}” que justamente es el objeto en el que, por convención, se especifica la página de inicio.

Una vez que se tiene la referencia del elemento host, en caso de que element sea undefined, se crea un div nuevo. A continuación guardaremos el valor de options.home en una propiedad llamada home en nuestra clase. El siguiente paso es suscribirse al evento onnavigated del objeto nav que, si recordamos de la definición de variables del principio, es WinJS.Navigation.

No obstante, si nos fijamos en esa línea de código, “nav.onnavigated = this._navigated.bind(this);” veremos que no se asigna directamente el valor de “this._navigated” a “nav.onnavigated”, sino que se obtiene la referencia de la función this._navigated y se llama al método bind pasándole como parámetro this. Esto se hace así porque tenemos que recordar que en JavaScript el contexto de this no se guarda en la llamada así que cuando se ejecute el método _navigated como resultado de la navegación, this en ese momento no será el mismo this que estamos usando en el constructor del objeto. Por tanto, con esta línea de código lo que pretendemos es guardar el contexto de this y luego utilizarlo cuando se llame a la función _navigated. Este comportamiento (bind) está definido en WinJS.

Un poco más abajo en la definición del constructor podemos ver como se llama al método nav.navigate(this.home), que ejecuta la navegación en sí.

Método de instancia

Ahora viene la parte donde se hace el trabajo de la navegación en sí: obtener el html de la página de destino y adjuntarlo al DOM de la página principal. Todo ello se hace en el método _navigated.

   1: _navigated: function (eventObject) {

   2:     var newElement = this._createPageElement();

   3:     var parentedComplete;

   4:     var parented = new WinJS.Promise(function (c) { parentedComplete = c; });

   5:  

   6:     var that = this;

   7:     WinJS.UI.Pages.render(eventObject.detail.location, newElement, eventObject.detail.state, parented).

   8:         then(function (control) {

   9:             that.element.appendChild(newElement);

  10:             that.element.removeChild(that.pageElement);

  11:             parentedComplete();

  12:             document.body.focus();

  13:             that.navigated();

  14:         });

  15: }

Aquí se llama al método WinJS.UI.Pages.render que se encarga de capturar el html definido en la página de destino y cargar los ficheros que se han definido en la cabecera, JavaScript y CSS, quedándose únicamente con el contenido de la etiqueta <body>, normalmente un div. Una vez que se ha completado el proceso y usando otra funcionalidad principal de WinJS, las promesas (promises), se ejecutará el método que hay definido después del then. A grosso modo. Las promesas se utilizan en WinJS para enlazar métodos asíncronos para su ejecución, aunque su definición es mucho más extensa y queda fuera del ámbito de este artículo.

Cuando el código html ya está limpio y listo para ser procesado se llama a ese método definido en el then. Sin embargo, antes de esto debemos observar que en la línea anterior se hace algo un poco raro a primera vista: var that = this, que es definir una variable que se llama that asignándole el contenido de this, lo que está relacionado con lo que hemos comentado antes de que en JavaScript los contextos no se guardan entre llamadas. De esta forma, como en la función anónima definida en el then vamos a usar esta referencia tenemos que guardarla antes.

Ya tenemos todo lo que necesitamos para poder añadir al DOM el nuevo control y quitar el anterior, hacer foco en el body recién creado y llamar al método that.navigated() que comprueba si se tiene que mostrar el botón de atrás en la interfaz de usuario de la aplicación.

Navegación

Como hemos visto, la navegación de la aplicación se define íntegramente en este método _navigated, es que el responsable de añadir el control (que WinJS formatea por nosotros) y añadirlo al DOM, quitando previamente el anterior que pudiera existir. WinJS es el framework en el que definimos las clasesy los espacios de nombres, así como el encargado de cargar las páginas html, los scripts de JavaScript y las hojas de estilos.

¿Cómo se genera una página en WinJS?

Otro aspecto importante del desarrollo de aplicaciones de Windows 8 son las propias páginas en sí. Hasta ahora hemos visto solamente cómo se realiza la navegación en una aplicación JavaScript de Windows 8, pero no hemos visto como se hacen las páginas a las cuales se navega. Para eso Visual Studio tiene una plantilla en el menú de nuevo elemento:

Ese elemento es Page Control, que nos genera una página HTML con las referencias de WinJS y un fichero JavaScript y CSS.

HTML

El HTML que genera la plantilla no tiene nada especial, teniendo en cuenta que hemos visto ya como referenciar WinJS en HTML.

   1: <!DOCTYPE html>

   2: <html>

   3: <head>

   4:     <meta charset="utf-8">

   5:     <title>pagecontrol</title>

   6:  

   7:     <!-- WinJS references -->

   8:     <link href="//Microsoft.WinJS.0.6/css/ui-dark.css" rel="stylesheet">

   9:     <script src="//Microsoft.WinJS.0.6/js/base.js"></script>
   1:  

   2:     <script src="//Microsoft.WinJS.0.6/js/ui.js">

   1: </script>

   2:     

   3:     <link href="pagecontrol.css" rel="stylesheet">

   4:     <script src="pagecontrol.js">

</script>

  10: </head>

  11: <body>

  12:     <div class="pagecontrol fragment">

  13:         <header aria-label="Header content" role="banner">

  14:             <button class="win-backbutton" aria-label="Back" disabled></button>

  15:             <h1 class="titlearea win-type-ellipsis">

  16:                 <span class="pagetitle">Welcome to Second Page!</span>

  17:             </h1>

  18:         </header>

  19:         <section aria-label="Main content" role="main">

  20:             <p>Content goes here.</p>

  21:         </section>

  22:     </div>

  23: </body>

  24: </html>

El contenido del Body es un div con la clase pagecontrol que identifica que ese es el contenido de la página a navegar.

JavaScript

   1: (function () {

   2:     "use strict";

   3:  

   4:     // This function is called whenever a user navigates to this page. It

   5:     // populates the page elements with the app's data.

   6:     function ready(element, options) {

   7:         // TODO: Initialize the fragment here.

   8:         var div = element;

   9:     }

  10:  

  11:     function updateLayout(element, viewState) {

  12:         // TODO: Respond to changes in viewState.

  13:     }

  14:  

  15:     WinJS.UI.Pages.define("/SecondPage/pagecontrol.html", {

  16:         ready: ready,

  17:         updateLayout: updateLayout

  18:     });

  19: })();

El fichero JavaScript sigue el estándar de envolver todo el código en una función anónima para así aislar el contexto global, definiendo a continuación la página internamente. Para ello se llama a la función WinJS.UI.Pages.define, a la que pasamos como primer parámetro un string con la ruta de la página actual y luego un objeto con los eventos a los que nos queremos suscribir, en el caso que nos ocupa ready y updateLayout. La función ready tiene los mismos parámetros que tenía la clase que definimos en navigator.js, siendo element el elemento div del contenido que queremos mostrar y options las opciones de la navegación, que es el segundo parámetro de WinJS.Navigator.navigate.

Conclusiones

Los desarrolladores de Silverlight estamos acostumbrados a una navegación basada en Páginas XAML, que el NavigationService se encarga de cargar y descargar conforme vamos interactuando con nuestra aplicación. En HTML tenemos el mismo concepto de navegación de página, pero vimos al principio que el inconveniente que tiene este modo de trabajar es que, en algún momento, la página se dejará de renderizar, haciendo que nuestra aplicación no tenga el aspecto de una aplicación tradicional de escritorio.

Para solucionar este problema se ha optado por una navegación basada en añadir y quitar contenido al DOM de la aplicación. El único problema que tiene esta metodología, es que, por ejemplo, los ficheros CSS que se carguen en memoria se podrán descargar, con lo que podemos encontrarnos con comportamientos no deseados. Por ejemplo, si definimos una clase de CSS con el mismo nombre en dos ficheros, al cargar el primero todo se verá correctamente. Si luego navegamos a una página, y en esa otra página cargamos otro CSS que sobreescribe la clase de la que estamos hablando, todo se seguirá mostrando correctamente. Pero si ahora volvemos hacia atrás el elemento que originalmente utilizaba esta misma clase de CSS, ahora se ve de manera diferente por este segundo fichero CSS. Por tanto, es muy importante hacer nombres de clases únicos para CSS, y si se repiten tener claro el porqué.

El desarrollo de HTML tiene un hándicap muy grande que es la falta de controles como en XAML. Esto hace que casi todo el contenido se tenga que repetir y no se pueda encapsular aspecto y funcionalidad de manera cómoda para el desarrollador. De todos modos Microsoft ha hecho un esfuerzo muy grande con WinJS para que el desarrollo de nuestras aplicaciones Metro con HTML sea lo más cómodo del mundo.

Él código de ejemplo lo puedes descargar de aquí.

Material de la charla de HTML5 en el CIIN

El lunes 3 de septiembre estuve con la gente del CIIN dando una charla sobre desarrollo de aplicaciones en HTML5 y cuáles son las novedades en este sentido.

Aquí os dejo la presentación, las demos y algunos enlaces que comenté durante la charla.

image

Agradecer a Alejandro Hidalgo (MVP de Internet Explorer 9) por la presentación en HTML5.

Enlaces de interés:

Decir que en PlainConcepts utilizamos Sproutcore para nuestros desarrollos y además formamos parte del soporte oficial de SproutCore para Microsoft y Strobe.

Saludos. Luis.