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!

2 comentarios en “Módulos en JavaScript… AMD, CommonJS”

Deja un comentario

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