Una introducción a aplicaciones full-JavaScript

por Alejandro Hernández

Traducción amateur del artículo An introduction to Full-Stack JavaScript publicado el 21/11/2013 en Smashing Magazine

Hoy en día, con cualquier web que desarrolles, tienes docenas de decisiones de arquitectura que tomar. Quisieras usar tecnologías que permitan desarrollo rápido, iteraciones continuas, máxima eficiencia, velocidad, robustez, etc. Quisieras desarrollar de acuerdo a prácticas Lean y con el método Agile. Quisieras usar tecnologías que te ayuden a triunfar en el corto y largo plazo. Y esas tecnologías no son siempre fáciles de escoger.

En mi experiencia, una aplicación full JavaScript alcanza todas esas metas. Probablemente ya lo estuviste viendo, quizás ya consideraste su utilidad e incluso lo discutiste con amigos.  Pero ¿probaste por vos mismo? En este posteo, voy a darte un pantallazo de por qué un desarrollo full JavaScript puede ser justo lo que necesitas y cómo es que produce su magia.

Para dar un rápido vistazo inicial:

esquema de una típica aplicación web full Javascript

Voy a explicarte cada uno de estos componentes. Pero primero, una nota breve sobre cómo llegamos a donde estamos hoy

Por qué uso JavaScript

Soy desarrollador web desde 1998. En aquel entonces usábamos Perl para la mayoría de nuestros desarrollos del lado del servidor. Pero incluso entonces teníamos JavaScript del lado del cliente. Las tecnologías de servidor cambiaron muchísimo desde entonces: oleada tras oleada de lenguajes y tecnologías, como PHP, ASP, JSP, .NET, Ruby, Python, sólo para nombrar algunas. Los desarrolladores empezaron a darse cuenta que usar dos lenguajes diferentes, respectivamente para los entornos cliente y servidor complicaba las cosas.

En la primera era de PHP y ASP, cuando los motores de plantillas (templates) eran sólo una idea, los desarrolladores incrustaban código de aplicación en el HTML. Ver código embebido como el de este ejemplo era cosa de todos los días:


<script>
      <?php
            if ($login == true){
      ?>
                  alert("Welcome");
      <?php
            }
      ?>
</script>

O, peor todavía:


<script>
      var users_deleted = [];
      <?php
            $arr_ids = array(1,2,3,4);
            foreach($arr_ids as $value){
      ?>
                  users_deleted.push("<php>");
      <?php
            }
      ?>
</script>

Para empezar, lo típico era confundirse con declaraciones diferentes de cada lenguaje, por ejemplo confundir for y foreach. Y para peor, escribir este tipo de código en el cliente y en el servidor para manejar la misma estructura de datos es muy incómodo, aún hoy. (A menos, claro, que tengas un equipo de desarrollo con ingenieros dedicados al front end e ingenieros dedicados al back end – pero entonces, incluso si comparten información, no van a poder colaborar en el código entre equipos)


<?php
      $arr = array("apples", "bananas", "oranges", "strawberries"),
      $obj = array();
      $i = 10;
      foreach($arr as $fruit){
            $obj[$fruit] = $i;
            $i += 10;
      }
      echo json_encode(obj);
?>

<script>
      $.ajax({
            url:"/json.php",
            success: function(data){
                  var x;
                  for(x in data){
                        alert("fruit:" + x + " points:" + data[x]);
                  }
            }
      });
</script>

Los primeros intentos de unificar bajo un sólo lenguaje fueron crear componentes para el cliente del lado del servidor y compilarlos a JavaScript. Esto no funcionó como se esperaba, y la mayoría de esos proyectos fallaron (por ejemplo, ASP MVC como reemplazo a las Web Forms de ASP.NET, y GWT que será posiblemente sustituído en un futuro próximo por Polymer). Pero la idea era buena, en esencia: un sólo lenguaje en el cliente y en el servidor, permitiendo reutilizar componentes y recursos. (y ésta es la palabra clave: recursos)

La respuesta fue simple: poner JavaScript en el servidor.

JavaScript había nacido en el servidor en Netscape Enterprise Server, pero no estaba maduro todavía en aquel entonces. Después de años y prueba y error, apareció finalmente Node.js, que no sólo pone JavaScript en el servidor, sino que soporta el concepto de programación anti-bloqueo (non-blocking programming), traído desde el mundo de nginx, (gracias al hecho de que el creador de Node viene de ese mundo), y manteniendo sabiamente la simplicidad, gracias a que Javascript tiene naturalmente bucles de eventos (event-loop)

(Para decirlo brevemente, la programación anti-bloqueo intenta poner las tareas que consumen mucho tiempo aparte, normalmente especificando qué habrá que hacer cuando estas tareas se completen, y permitiéndole al procesador seguir trabajando en otras tareas mientras tanto)

Node.js cambió la forma en que manejamos el acceso al I/O (input/output, entrada/salida) para siempre. Como desarrolladores web, estábamos acostumbrados a estas líneas de código para acceder a una base de datos (I/O):


      var resultset = db.query("SELECT * FROM 'table'");
      drawTable(resultset);

Esta línea básicamente bloquea el código, porque el programa se detiene hasta que el driver de la base de datos tiene un resultset para devolver. Entre tanto, la plataforma de tu infraestructura provee la capacidad de concurrencia, normalmente usando  hilos (threads) y bifurcaciones (forks)

Con Node.js y usando programación anti-bloqueo, tenemos más control sobre el flujo del programa. Ahora (incluso si todavía tienes ejecución paralela oculta en el driver de tu base de datos (I/O), podrás definir qué es lo que el programa debería hacer entretanto, y qué hará cuando reciba el resultset.


      db.query("SELECT * FROM 'table'", function(resultset){
            drawTable(resultset);
      });
      doSomeThingElse();

Con este trozo de código, definimos dos flujos de ejecución: el primero maneja qué se hará después de enviar la consulta a la base de datos, mientras el segundo define qué hacer cuando se reciba como respuesta el resultset, usando un simple callback (retrollamada). Esta es una forma elegante y potente de gestionar concurrencia. Como dice el dicho “todo corre en paralelo, excepto tu código“. Entonces, tu código debería ser fácil de escribir, de leer, de entender y de mantener, todo eso sin perder el control de cómo fluye el programa.

Estas ideas no eran nuevas. Entonces, ¿por qué fueron tan populares a partir de Node.js? Simple: la programación anti-bloqueo puede lograrse de muchas maneras. Quizás la más sencilla sea mediante el uso de callbacks y bucles de eventos (event-loop). En la mayoría de los lenguajes, eso no es sencillo: mientras que los callbacks son una característica común en algunos lenguajes, un bucle de evento no lo es, y muchas veces te vas a encontrar  lidiando con librerías externas (por ejemplo, la librería Tornado para Python)

Pero, en JavaScript, los callbacks están dentro del mismo lenguaje, así como los bucles de evento, y prácticamente cualquier programador que alguna vez incursionó en JavaScript está familiarizado con ellos (o al menos los ha usado, aunque no entienda qué cosa es un bucle de eventoevent-loop). De pronto, cualquier empresa por pequeña que sea puede reutilizar sus desarrolladores (es decir, sus recursos) tanto para desarrollo en el cliente como para desarrollo en el servidor, resolviendo así el problema del aviso “se necesita un gurú de Python“.

Bien, ya tenemos una plataforma increíblemente rápida (gracias a la programación anti-bloqueo), con un lenguaje de programación que es increíblemente sencillo de usar (gracias al JavaScript). ¿Alcanza? ¿Será duradero? Estoy seguro que JavaScript tiene futuro. Déjenme que les explique por qué.

Programación funcional

JavaScript fue el primer lenguaje que trajo el paradigma funcional a las masas (por supuesto, Lisp llegó primero, pero la mayoría de los desarrolladores jamás crearon una aplicación productiva con él) Lisp y Self, que son las principales influencias de JavaScript, están llenos de ideas innovadoras que nos permiten concentrarnos en explorar nuevas técnicas, patrones y paradigmas. Y todas esas innovaciones se trasladaron a JavaScript. Fíjense en la teoría de mónadas, los números de Church o incluso (para un ejemplo más práctico) en las colecciones de funciones que ofrece Underscore, que pueden ahorrarte innumerables líneas de código.

Objetos dinámicos y herencia por prototipos. (Prototypal Inheritance)

La programación orientada a objetos sin clases (y sin interminables jerarquías de clases) nos permiten desarrollar rápidamente. Simplemente creamos objetos, y les agregamos métodos para poder usarlos. Más importante, reduce el tiempo que dedicamos a refactorear cuando estamos haciendo tareas de mantenimiento, permitiéndole al desarrollador modificar instancias de objetos, en lugar de clases. Esta velocidad y flexibilidad allanan el camino del desarrollo rápido.

Javascript es Internet

JavaScript fue diseñado para Internet. Estuvo ahí desde el comienzo, y no va a irse. Todos los intentos de destruirlo fallaron: recordemos, por ejemplo, la caída de los Applets de Java, el reemplazo de VBScript por TypeScript (que compila a JavaScript), y la desaparición de Flash a manos del mercado móvil y HTML5Reemplazar JavaScript sin romper millones de páginas web es imposible, o sea que nuestro objetivo de aquí en más será mejorarlo. Y nadie es mejor para eso ni está más capacitado para ese trabajo que el Comité Técnico 39 de ECMA.

Es cierto, alternativas a JavaScript aparecen todos los días, como CoffeeScript, TypeScript y los millones de lenguajes que compilan a JavaScript. Estas alternativas pueden ser útiles para ciertas etapas de desarrollo (vía sourcemaps) pero no logran suplantar JavaScript a largo plazo por dos motivos: sus respectivas comunidades de soporte no van a crecer, y sus mejores adelantos van a ser adoptados por ECMAScript (es decir, JavaScript). JavaScript no es lenguaje ensamblador (Assembler). Es un lenguaje de programación de alto nivel cuyo código fuente se puede entender, por eso, es obligatorio entenderlo.

Javascript de punta a punta: Node.js y MongoDB

Cubrimos ya las razones para usar JavaScript. A continuación vamos a considerar JavaScript como un motivo para usar Node.js y MongoDB.

Node.js

Node.js es una plataforma para construir aplicaciones en red, rápidas y escalables; eso es básicamente lo que el website de Node.js dice. Pero Node.js es más que eso: es el mejor entorno en tiempo de ejecución (runtime environment) que hay disponible en estos momentos, usado por una tonelada de aplicaciones y librerías, inclusive hay librerías de navegadores que corren en Node.js. Más importante, esta rapidez de ejecución del lado del servidor les permite a los desarrolladores enfocarse en problemas más complejos, como por ejemplo Natural para procesar lenguaje natural. Inclusive si uno no está pensando en escribir la aplicación principal en el servidor con Node.js, se pueden usar herramientas construidas sobre Node.js para mejorar el proceso de desarrollo, por ejemplo: Bower, para administrar paquetes de front-end, Mocha para test unitarios, Grunt para automatizar tareas e inclusive Brackets como editor de texto para código.

Así que, si vas a escribir una aplicación JavaScript para el servidor o para el cliente, tendrías que familiarizarte con Node.js, porque vas a necesitarlo a diario. Existen algunas alternativas interesantes, pero ninguna tiene ni siquiera el 10% de la comunidad con la que cuenta Node.js.

MongoDB

MongoDB es una base de datos No-SQL basada en documentos, que usa JavaScript como lenguaje de consulta (pero no está escrita en JavaScript), y así completa nuestra plataforma JavaScript de punta a punta. Pero ni siquiera ésa es la razón principal para elegir esa base de datos.

MongoDB no tiene esquema, permitiendo persistir objetos en un modo flexible, y de esta manera se adapta rápidamente a los cambios en los requerimientos. Además es altamente escalable y está basada en map-reduce, lo que la hace apropiada para aplicaciones que consumen grandes volúmenes de datos. MondoDB es tan flexicble que se puede usar como base de datos sin esquema basada en documentos, como base de datos relacional  (aunque no tiene transacciones, sólo las emula), e incluso como repositorio del tipo “clave-valor” (key-value) para cachear resultados, como Memcached y Redis.

Componentes en el Servidor con Express

Generar componentes del lado del servidor nunca es fácil. Pero con Express (y Connect) llegó la idea del midleware o capa intermedia de la aplicación. En mi opinión, crear capas intermedias es la mejor forma de definir componentes en el servidor. Si se lo quiere comparar con un patrón de desarrollo conocido, es muy parecido al patrón pipes and filters (conductos y filtros)

La idea básica es que tu componente es parte de una cadena. La cadena procesa un requerimiento (el input) y genera una respuesta (el output), pero tu componente no es responsable de toda la respuesta. En vez de eso, modifica solamente lo que necesita modificar y delega al siguiente eslabón de la cadena. La respuesta se envía al cliente cuando la última parte de la cadena termina su proceso.

Nos referimos a estos eslabones de la cadena como componentes middleware. Claramente, podemos crear dos tipos de componentes de capas intermedias:

  • Componentes Intermedios
    Un intermedio procesa el requerimiento y devuelve una respuesta pero no es completamente responsable por la respuesta en sí, sólo la delega al siguiente elemento en la cadena.
  • Componentes Terminales
    Un componente terminal tiene completa responsabilidad sobre la respuesta final. Procesa y modifica el requerimiento y el resultado pero no necesita delegar al siguiente eslabón de la cadena.  En la práctica, si delega, va a permitir mayor flexibilidad en la arquitectura (por ejemplo, para agregar más capas intermedias más adelante, incluso si esas capas no existen (en cuyo caso la respuesta o resultado iría directamente al cliente)

Esquema del componente control de usuarios

Para un ejemplo concreto, consideremos un componente “control de usuarios” en el servidor. En términos de middleware, vamos a tener tanto terminales como intermedios. Para nuestros terminales, tenemos tareas como “crear usuario” o “listar usuarios”. Pero antes de poder ejecutar esas acciones, necesitamos que nuestros intermedios se encarguen de la autenticación (porque no queremos que alguien no registrado pueda crear usuarios). Una vez que creemos estos intermedios de autenticación, podemos simplemente insertarlos en cualquier punto de la cadena transformando una tarea no autenticada en una tarea autenticada.

Aplicaciones de Una Página (SPA)

Cuando estás trabajando con una aplicación full-JavaScript, muchas veces vas a enfocarte en crear una Aplicación de Una Página (SPA por sus siglas en inglés, Single-Page Application). Muchos desarrolladores web encuentran tentador probar su habilidad en una SPA. Yo desarrollé varias, (la mayoría propietarias) y creo que son el futuro de las aplicaciones Web. ¿Alguna vez comparaste una SPA versus una aplicación Web tradicional en una conexión móvil? La direrencia en la respuesta se mide en decenas de segundos.

(Nota: otros pueden estar en desacuerdo conmigo. Twitter, por ejemplo, volvió atrás de su intento de SPA. Mientras, sitios grandes como Zendesk están migrando hacia allí. Yo he visto evidencia suficiente de los beneficios de una SPA como para creer en ellas, pero las experiencias pueden variar.)

Si las SPAs son geniales, ¿por qué construir tu producto en un estilo anticuado? Un argumento frecuente que se escucha es que la preocupación acerca del SEO (Search Engine Optimization, Optimización de Motores de Búsqueda). Pero, si manejamos las cosas correctamente, esto no debería ser un problema: se pueden tomar diferentes enfoques, desde usar un navegador sin encabezado (como PhantomJS) para proveer HTML cuando se detecta un robot de rastreo web (web crawler) o proveer HTML generado en el servidor con la ayuda de los frameworks existentes.

MV* del lado del cliente con Backbone.js, Marionette y Bootstrap de Twitter.

Mucho se ha dicho acerca de los frameworks MV* (Model, View, *) para SPAs. Es una elección difícil, pero yo diría que los tres mejores son: Backbone.js, Ember y Angular.js.

Los tres tienen muy buenas referencias. Pero ¿cuál es el mejor para tu proyecto?

Desgraciadamente, tengo que admitir que mi experiencia con AngularJS es limitada, así que que lo voy a dejar fuera de la discusión. En cuanto a Ember y Backbone.js, representan dos diferentes maneras de atacar el mismo problema.

Backbone.js es mínimalista y ofrece sólo lo suficiente para que crees una SPA simple. Ember, del otro lado, es un framework completo y profesional para crear SPAs. Tiene más complejidad y parafernalia, pero también una curva de aprendizaje más empinada. (Se puede leer más sobre Ember.js aquí)

Dependiendo del tamaño de tu aplicación, la decisión puede ser tan sencilla como calcular la proporción entre las  “características a usar” contra las “características disponibles”, que seguramente te dará una gran pista.

El formato también es un desafío, pero nuevamente, podemos contar con frameworks que vengan a rescatarnos. Para CSS, Bootstrap de Twitter es una buena elección porque ofrece un set completo de estilos, que están listos para usar tal cual vienen empaquetados pero también son muy fáciles de personalizar.

Bootstrap fue creado en lenguage LESS, y es open source (código abierto), de manera que podemos modificarlo si hay necesidad de ello. Viene con un montón de controles que están bien documentados. Además, el modelo de personalización permite crear tu propio estilo. Es definitivamente la herramienta adecuada para este trabajo.

Mejores prácticas: Grunt, Mocha, Chai, RequireJS y CoverJS

Finalmente, deberíamos definir algunas buenas prácticas, así como mencionar cómo implementarlas y mantenerlas. Normalmente, mi solución se centra en varias herramientas, las cuales están basadas en Node.js

Mocha y Chai

Estas herramientas te permiten mejorar tu proceso de desarrollo aplicando Desarrollo guiado por Tests (TDD por sus siglas en inglés, Test Driven Development) o Desarrollo guiado por Comportamiento (BDD, Behavior-Driven Development), creando la infraestructura para organizar tus tests unitarios y un motor para ejecutarlos automáticamente.

Existen muchos frameworks de testing para JavaScript, ¿por qué usar Mocha? La respuesta corta es: porque es flexible y completo.

La respueta más larga es que tiene dos importantes características (interfaces y notificadores) y una ausencia significativa (assertions, afirmaciones). Permítanme explicar:

  • Interfaces
    Quizás ya estás acostumbrado a los conceptos “suites” y “unit tests” de TDD, o quizás prefieras las ideas de BDD sobre especificaciones de comportamiento con “describe” and “should” (debería). Mocha te permite usar ambos enfoques.
  • Notificadores (reporters)
    Correr un test va a generar reportes sobre los resultados, y se puede formatear estos resultados usando varios notificadores o reporters. Por ejemplo, si utilizas un servidor de integración continua, vas a encontrar un notificador para hacer precisamente eso.
  • Falta de una librería de aserciones (assertions)Lejos de ser un problema, Mocha fue diseñado para permitirte usar la librería de aserciones que prefieras, dándode aún más flexibilidad. Existen muchísimas opciones, y es ahí donde entra Chai en juego.

Chai es una librería de aserciones que te permite usar cualquiera de los tres estilos de afirmación

  • assert
    Este es el estilo clásico de aserción, de la vieja escuela de TDD. Por ejemplo
    assert.equal(variable, "value");
  • expect
    Este estilo de aserción encadenada es más comúnmente usado en BDD. Por ejemplo:

    expect(variable).to.equal("value");
  • should
    Este es también usado en BDD, pero yo prefiero expect porque should generalmente suena repetitivo (por ejemplo con la especificación de comportamiento “it(should do something…)” por ejemplo:

    variable.should.equal("value");

Chai combina perfectamente con Mocha. Usando sólo estas dos librerías, podrás escribir tus tests en TDD, BDD o en cualquier estilo imaginable.

Grunt

Grunt te permite automatizar tareas de buildeo, desde un simple copy & paste y concatenación de archivos,  precompilación de plantillas (templates), lenguajes de generación de estilos CSS (como SASS o LESS), test unitarios (con Mocha), linting y minificación (por ejemplo, con UglifyJS o Closure Compiler). Uno puede agregar sus propias tareas para automatizar a Grunt o buscar en el registro, donde hay disponibles cientos de plugins (de nuevo, usar una herramienta que tiene una gran comunidad detrás rinde frutos). Grunt además puede monitorear tus archivos y desencadenar acciones cuando haya alguna modificación.

RequireJS

RequireJs puede sonar como otra forma de cargar módulos usando la API AMD (API: Application Programming Interface; AMD: Asynchronous Module Definition o módulo de definición asíncrona) pero les aseguro que es mucho más que eso. Con RequireJS, se pueden definir dependencias y jerarquías en los módulos y permitir que RequireJS las cargue en tu lugar. También provee una forma sencilla de evitar contaminar el ámbito global de variables, definiendo todos tus módulos dentro de funciones. Esto hace que los módulos sean reusables, no como las soluciones con namespaces. Piensen esto: si uno define un módulo como Demoapp.helloWorldModule y luego lo quiere trasladar a Firstapp.helloWorldModule, entonces va a ser necesario cambiar cada una de las referencias a Demoapp para hacerlo portable.

RequireJS también ayuda a aplicar el patrón de inyección de dependencias (dependency injection). Supongamos que tenemos un componente que necesita una instancia del objeto de la aplicación principal, (un singleton). Por el sólo hecho de usar RequireJS, te das cuenta que no deberías usar una variable global para guardarlo, y que no podés definir una instancia como dependencia dentro de RequireJS. Entonces, necesitás solicitar esta dependencia en el constructor de tu módulo. Veamos un ejemplo:

En main.js

  define(
      ["App","module"],
      function(App, Module){
          var app = new App();

          var module = new Module({
              app: app
          })

          return app;
      }
  );

en module.js:

 define([],
      function(){
          var module = function(options){
              this.app = options.app;
          };
          module.prototype.useApp = function(){
              this.app.performAction();
          };
          return module
      }
  );

Nótese que no es posible definir el módulo con una dependencia a main.js sin crear una referencia circular.

CoverJS

La cobertura de código es una métrica para evaluar tus tests. Tal como el nombre sugiere, te dice qué tanto de tu código está cubierto por tu suite de tests en la actualidad. CoverJS mide la cobertura de tus tests instrumentando declaraciones (statements) en tu código (en lugar de líneas de código, como JSCoverage) y generando una versión instrumentadas de dicho código. Además puede generar informes para alimentar tu servidor de integración continua.

Conclusión

Una aplicación full-JavaScript no es la respuesta a todos los problemas. Pero esta tecnología –y la comunidad detrás de ella– pueden llevarte lejos. Con JavaScript, es posible crear aplicaciones escalables y mantenibles, unificadas bajo un solo lenguaje. No hay duda, es una fuerza a tener en cuenta.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *