Módulos en JavaScript… AMD, CommonJS

Con las aplicaciones web cada vez con mayor carga de cliente, y el uso cada vez mayor de sistemas de build en cliente como grunt o gulp, usar módulos para desarrollar en JavaScript es cada vez más habitual. Si todavía te descargas las librerías de sus páginas web y las incluyes una a una en tags <script/> es probable que este post te interese.

¿Qué es un módulo?

Llamamos módulo JavaScript a un código que de alguna manera es “auto contenido” y que expone una interfaz pública para ser usada. Esto no es realmente nuevo, el patrón de módulo ya hace bastantes años que se utiliza y no requiere más que algunos conocimientos de JavaScript para aplicarlo. El problema con los módulos en JavaScript no ha sido nunca el crearlos si no el de cargarlos. Puede parecer simple… de hecho, ¿no se trata solo de poner un tag <script />? Pues la realidad es que no, porque cargar un módulo implica que antes deben estar cargadas sus dependencias y por lo tanto debemos tener un mecanismo para definir esas dependencias y otro mecaniso para cargarlas al tiempo que cargamos el módulo deseado.

Es ahí donde entran en juego los distintos estándares de módulos que tenemos. Nos permiten crear módulos JavaScript, declarar las dependencias (es decir indicar de qué módulos depende nuestro módulo e incorporar la funcionalidad del módulo del cual dependemos) y cargar determinados módulos. Hay dos estándares usados hoy en día: CommonJS y AMD.

CommonJS

CommonJS es un sistema de módulos síncrono: es decir la carga de módulos es un proceso síncrono que empieza por un módulo inicial. Al cargarse este módulo se cargaran todas sus dependencias (y las dependencias de las dependencias, y las dependencias de las dependencias de las dependencias… y así hasta cualquier nivel de profundidad). Una vez finalicen todas esas cargas, el módulo inicial está cargado y empieza a ejecutarse. Definir un módulo en formato CommonJS es muy sencillo:

  1. var Complex = function (r, i) {
  2.     this.r = r instanceof Complex ? r.r : r;
  3.     this.i = r instanceof Complex ? r.i : (i || 0);
  4. }
  5. module.exports = Complex;

Supón que este código está en un fichero Complex.js. Este código define un módulo que exporta una función (constructora) llamada Complex. Observa el uso de module.exports para indicar que es lo que exporta el módulo. Todo lo que no pertenezca al exports son variables (y funciones) privadas del módulo. Ahora podríamos declarar otro módulo que dependiese de este módulo:

  1. var Complex = require('./complex');
  2. addComplex = function (ca, cb) {
  3.     return new Complex(ca.r + cb.r, ca.i + cb.i);
  4. }
  5. var math = {
  6.     add: function (a, b) {
  7.         if (a instanceof Complex || b instanceof Complex) {
  8.             return addComplex(new Complex(a), new Complex(b));
  9.         }
  10.         return a + b;
  11.     }
  12. }
  13. module.exports = math;

Este módulo (math.js) requiere el módulo complex.js (de ahí el uso de require), define un objeto math con un método y exporta dicho objeto. La función addComplex es privada al módulo.

Finalmente podemos crear un tercer módulo (main.js) que use esos módulos para sumar tanto números reales como complejos. Este va a ser nuestro módulo inicial:

  1. var Complex = require('./complex');
  2. var math = require('./math');
  3.  
  4. console.log(math.add(40, 2));
  5. var c1 = new Complex(40, 3);
  6. console.log(math.add(c1, 2));

Si ejecutamos el siguiente código mediante nodejs vemos como todo funciona correctamente:

image

Nodejs soporta módulos CommonJS de forma nativa, pero… ¿qué pasa con el navegador? Pues que necesitamos soporte de alguna herramienta externa. Una de las más conocidas es browserify que se instala como un paquete de node. Browserify es, a la vez, una herramienta de línea de comandos y un módulo CommonJS que podemos integrar con grunt o gulp. Si usamos la herrramienta de línea de comandos, se puede usar el comando browserify main.js > bundle.js para crear un fichero (bundle.js) que contenga el código de main.js y de todos sus módulos requeridos. Este fichero es el que usaría con un tag script.

Lo bueno de browserify es que solo debo indicarle el fichero inicial (en mi caso main.js). Él se encarga de ver los módulos necesarios y empaquetarlos todos juntos en un fichero. Usar browserify como herramienta de línea de comandos es posible (para ello basta con que lo tengas instalado como módulo global, es decir npm install –g browserify), pero no es lo más cómodo: lo suyo es tenerlo integrado dentro de nuestro script de build que tengamos con gulp o grunt. Por suerte browserify es también un módulo CommonJS por lo que podemos usarlo dentro de nuestro script de build. P. ej. el siguiente código muestra como usarlo mediante gulp:

  1. var gulp = require('gulp');
  2. var browserify = require('browserify');
  3. var source = require('vinyl-source-stream');
  4.  
  5. gulp.task('browserify', function () {
  6.     browserify('./main.js')
  7.         .bundle()
  8.         .pipe(source('bundle.js'))
  9.         .pipe(gulp.dest('./scripts'));
  10. });

Con esto basta con que ejecutes “gulp broswerify” para que browserify te deje en scripts un fichero bundle.js con el resultado de browserify. El único requisito es tener instalado (además de gulp y broswerify, obviamente) un módulo llamado vinyl-source-stream que se usa para pasar de streams basados en texto (los que usa browserify)  a streams de gulp.

Muchas de las librerías JavaScript (incluyendo incluso jQuery) tienen versión CommonJS lo que ayuda mucho a organizar tu código. Por supuesto se puede configurar browserify para que genere source maps o que aplique más transformaciones al código (p. ej. convertir código JSX de React en código JavaScript).

AMD

AMD es otra especificación de módulos JavaScript, cuya principal diferencia con CommonJS es que es asíncrona (AMD significa Asynchronous Module Definition). La implementación más conocida para navegadores de AMD es requirejs. Al ser asíncrona permite escenarios con carga de módulos bajo demanda (es decir cargar un módulo sólo si se va a usar), lo que puede ser interesante en según que aplicaciones.

Si usas requirejs no necesitas nada más: no es necesario que uses ninguna herramienta de línea de comandos o que crees tareas de grunt o gulp. Dado que requirejs implementa AMD va a ir cargando los módulos JavaScript de forma asíncrona. No tienes por qué crear un bundle con todos ellos.

La sintaxis para definir un módulo AMD es un poco más “liosa” que la sintaxis de CommonJS, pero tampoco mucho más. Empecemos por ver como sería el módulo AMD para definir el tipo Complex:

  1. define([], function () {
  2.     console.log('complex loaded…');
  3.     var Complex = function (r, i) {
  4.         this.r = r instanceof Complex ? r.r : r;
  5.         this.i = r instanceof Complex ? r.i : (i || 0);
  6.     }
  7.  
  8.     return Complex;
  9. });

Los módulos AMD empiezan con una llamada a define, que acepta básicamente dos parámetros: un array con las dependencias del módulo (el equivalente al require de CommonJS) y luego una función con el código del  módulo. Esa función devuelve lo que el módulo exporta (es decir, el return de la función equivale al module.exports de CommonJS). El módulo que define Complex no depende de nadie, así que el array está vacío. No ocurre lo mismo con el modulo math (fichero math_amd.js):

  1. define(['complex_amd'], function (Complex) {
  2.     addComplex = function (ca, cb) {
  3.         return new Complex(ca.r + cb.r, ca.i + cb.i);
  4.     }
  5.     var math = {
  6.         add: function (a, b) {
  7.             if (a instanceof Complex || b instanceof Complex) {
  8.                 return addComplex(new Complex(a), new Complex(b));
  9.             }
  10.             return a + b;
  11.         }
  12.     }
  13.     return math;
  14. });

Observa ahora como el módulo depende del módulo complex_amd. Eso significa que al cargarse este módulo, el módulo complex_amd (fichero complex_amd.js) debe estar cargado. Si no lo está requirejs lo cargará asincronamente, y cuando esta carga haya finalizado invocará la función que define el módulo. Observa ahora que la función tiene un parámetro. Este parámetro se corresponde con lo que exporta (devuelve) el módulo complex_amd del cual dependíamos. Básicamente, por cada elemento (dependencia) del array tendremos un parámetro en la función. Eso se ve todavía más claro en el modulo main_amd.js quie depende tanto de complex_amd como de math_amd:

  1. define(['complex_amd', 'math_amd'], function (Complex, math) {
  2.     console.log(math.add(40, 2));
  3.     var c1 = new Complex(40, 3);
  4.     console.log(math.add(c1, 2));
  5. });

Observa como hay dos parámetros en el array de dependencias y por lo tanto la función del módulo recibe dos parámetros. El array indica las dependencias y el parámetro de la función permite acceder a ellas.

Finalmente tan solo nos queda tener un html que cargue primero requirejs y una vez haya terminado, indique a requirejs que cargue el modulo main_amd.js y lo ejecute. Al cargar este módulo, requirejs cargará asíncronamente todas las dependencias. El código del fichero HTML es trivial:

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4.     <title></title>
  5. </head>
  6. <script data-main="main_amd" src="bower_components/requirejs/require.js"></script>
  7. <body>
  8. </body>
  9. </html>

El escenario “cargar requirejs y una vez haya terminado empieza a cargar el módulo indicado” es tan común que requirejs lo soporta a través del atributo data-main del tag script. Podemos ver en la pestaña network del navegador como realmente se cargan los tres módulos por separado:

image

¿Cuál usar?
La verdad es que AMD se acerca mucho más a la filosofía de la web que CommonJS. La carga asíncrona y on-demand es mucho más natural en la web que la carga síncrona que tiene CommonJS. Lo que ocurre es que actualmente solemos siempre crear un bundle de todos nuestros JavaScript, porque sabemos que es más rápido descargarse un solo fichero de 100Ks que 10 ficheros de 10Ks cada uno. Seguro que todos habéis oído que una de las normas básicas de optimizar una página web consiste en minimizar la descarga de ficheros. Los bundles de JavaScript, de CSS, los sprite-sheets y el uso de data-uris van todos por ese camino: cargar un fichero más grande antes que varios de pequeños. Si seguimos esa tónica perdemos la característica de carga on-demand y asíncrona de AMD (porque antes de ejecutar la aplicación hemos tenido que generar ese bundle). La verdad es que cargar los scripts on-demand no es algo que requieran la mayoría de aplicaciones (un escenario sería casos en que una aplicación quiere cargar scripts distintos en función de ciertos datos de ejecución, p. ej. scripts distintos por usuario).

Así parece que, actualmente, no haya una diferencia sustancial entre usar CommonJS y AMD si al final terminamos en un bundle. La cuestión puede reducirse a gustos personales o cantidad de módulos existentes en cada formato (a pesar de que es posible, con poco trabajo, usar módulos CommonJS bajo AMD) pero HTTP2 puede cambiar eso. HTTP2 convierte los bundles en no necesarios, ya que mejora el soporte para varias conexiones… bajo ese nueva prisma AMD parece ser una mejor opción que CommonJS. Pero HTTP2 es todavía muy nuevo, así que hasta todos los navegadores y servidores web lo soporten va a pasar algún tiempo… y cuando esté establecido, quizá y solo quizá, la duda de si usar CommonJS o AMD dejará de tenir sentido porque los módulos nativos de ES6 habrán tomado el relevo.

Saludos!

ASP.NET 5: Configuración

Una de las novedades de ASP.NET5 es su sistema de configuración. En versiones anteriores el sistema de configuración estaba muy atado al fichero web.config. En este fichero se guardaba tanto la configuración propia del programa (cadenas de conexión, appsettings o información adicional que suele estar en secciones de configuración propias) como información de configuración del propio runtime: tipo de seguridad, módulos a cargar, bindings de assemblies y un sinfin más de configuraciones.

En ASP.NET5 eso se ha simplificado mucho. Un tema importante es que ahora está claramente separada la configuración del framework que se realiza mayoritamente por código, la configuración del runtime que se delega en el fichero project.json y la configuración propia del programa que está en cualquier otro sitio. En este post nos centraremos solamente en la configuración propia del programa.

Configuración propia del programa (configuración de negocio)

La configuración propia toma la forma básica de un diccionario con claves que son cadenas y valores que son… cadenas. No tenemos una interfaz propia para cadenas de conexión o para las secciones propias de configuración. Para entendernos, sería como si todo fuesen appsettings. Es un mecanismo sencillo (si necesitas algo por encima de eso es fácil construirlo). El sistema por si mismo no nos ata a ningún formato de fichero (esa configuración puede estar en un json, un xml o lo que sea).

Aunque, es cierto, que el sistema de por si no nos ata a ningún esquema en concreto de nuestros datos, si queremos utilizar los métodos que son capaces de leer información automáticamente de un fichero, entonces si que debemos adaptarnos al esquema previsto, aunque es bastante ligero.

P. ej. esos tres ficheros contienen todos ellos la misma configuración:

  1.  
  2.   "smtp": {
  3.     "server": {
  4.       "user": "eiximenis",
  5.       "mail": "mail@domain.com"
  6.     },
  7.     "auth": {
  8.       "anonymous": "true"
  9.     }
  10.   }
  11. }

  1. <smtp>
  2.     <server>
  3.         <user>eiximenis</user>
  4.         <mail>mail@domain.com</mail>
  5.     </server>
  6.     <auth>
  7.         <anonymous>true</anonymous>
  8.     </auth>
  9. </smtp>

  1. [smtp:server]
  2. user=true
  3. mail=mail@domain.com
  4. [smtp:auth]
  5. anonymous=true

Los 3 ficheros definen las siguientes claves:

  • smtp:server:user –> Con valor “eiximenis”
  • stmp:server:mail –> Con valor “mail@domain.com
  • smtp:auth:anonymous –> Con valor “true”

En versiones anteriores esos 3 valores hubiesen ido, seguramente, en la sección<appSettings /> del web.config. La verdad es que usar el separador dos puntos (:) en las claves de appsettings es algo que ya era como un “estándard de facto”.

Para cargar esos ficheros usamos los métodos AddJsonFile(), AddXmlFile() o AddIniFile() del objeto ConfigurationBuilder, que se suele crear en el constructor de la clase Startup:

  1. public Startup(IHostingEnvironment env, IApplicationEnvironment appEnv)
  2. {
  3.     // Setup configuration sources.
  4.     var builder = new ConfigurationBuilder(appEnv.ApplicationBasePath)
  5.         //.AddJsonFile("config.json")
  6.         //.AddXmlFile("config.xml")
  7.         .AddIniFile("config.ini")
  8.         .AddEnvironmentVariables();
  9.     Configuration = builder.Build();
  10. }
  11.  
  12. public IConfiguration Configuration { get; set; }

El objeto Configuration básicamente expone un indexer para acceder a los valores de la configuración:

  1. var usermail = Configuration["smtp:server:user"];

Si quiero acceder a un valor de la configuración desde un controlador MVC puedo incorporar el objeto Configuration dentro del sistema de inyección de dependencias de asp.net 5:

  1. public void ConfigureServices(IServiceCollection services)
  2. {
  3.     services.AddInstance<IConfiguration>(Configuration);
  4. }

Y ahora puedo añadir un parámetro IConfiguration a cada controlador que lo requiera.

El método GetConfigurationSection me devuelve otro IConfiguration pero cuya raíz es la clave que yo indique. Es decir dado el objeto Configuration que tenía si hago:

  1. var usermail = Configuration["smtp:server:user"];
  2. var smtpcfg = Configuration.GetConfigurationSection("smtp");
  3. var usermail2 = smtpcfg["server:user"];
  4. var servercfg = Configuration.GetConfigurationSection("smtp:server");
  5. var usermail3 = servercfg["user"];

Los valores de usermail, usermail2 y usermail3 son el mismo.

Configuración tipada

Este esquema de configuración es funcional y sencillo, pero a veces queremos usar objetos POCO para guardar nuestra propia configuración. Supongamos que tenemos la siguiente clase para guardar los datos del servidor:

  1. public class ServerConfig
  2. {
  3.     public string User { get; set; }
  4.     public string Mail { get; set; }
  5. }

Podemos mapear los datos que están en “smtp:server” con el siguiente código:

  1. public void ConfigureServices(IServiceCollection services)
  2. {
  3.     services.Configure<ServerConfig>(Configuration.GetConfigurationSection("smtp:server"));
  4. }

Recuerda que GetConfigurationSection(“stmp:server”) me devuelve un IConfiguration que apunta directamente a esta clave y que por lo tanto tiene las dos claves “user” y “mail” que se corresponen con los nombres de las propiedades de la clase ServerConfig. Esta línea además incluye dentro del sistema de inyección de dependencias la clase ServerConfig así que ahora puedo inyectarla a cualquier controlador MVC que lo requiera:

  1. public HomeController(IOptions<ServerConfig> sc)
  2. {
  3.     var cfg = sc.Options;
  4.     var user = cfg.User;
  5. }

Observa, eso sí, que el parámetro no es un ServerConfig, si no un IOptions<ServerConfig> (eso es porque services.Configure incluye la interfaz IOptions<T> en el sistema de inyección de dependencias). Para acceder al objeto con la configuración usamos la propiedad Options.

Configuración anidada

Por supuesto el sistema soporta configuración anidada. Es decir, lo siguiente es correcto:

  1. public class SmtpConfig
  2.  {
  3.      public ServerConfig Server { get; set; }
  4.      public AuthConfig Auth { get; set; }
  5.  }
  6.  
  7.  public class ServerConfig
  8.  {
  9.      public string User { get; set; }
  10.      public string Mail { get; set; }
  11.  }
  12.  
  13.  public class AuthConfig
  14.  {
  15.      public string Anonymous { get; set; }
  16.  }

Y registrarlas de la siguiente manera:

  1. services.Configure<SmtpConfig>(Configuration.GetConfigurationSection("smtp"));

Y ahora podemos inyectar un  IOptions<SmtpConfig> en cualquier controlador que lo requiera:

  1. public HomeController(IOptions<SmtpConfig> sc)
  2. {
  3.     var cfg = sc.Options;
  4.     var user = cfg.Server.User;
  5. }

¡Sencillo y rápido!

Y hasta aquí este post sobre la configuración en ASP.NET5… espero que os haya resultado interesante!

Saludos!