Módulo perteneciente al curso Programación avanzada con JavaScript y ECMAScript.
A medida que la cantidad de código JavaScript de una aplicación va creciendo, se empieza a hacer evidente que necesitamos organizar nuestro código y separarlo en distintos ficheros.
Cuando empezamos a separar nuestro código empiezan a surgir algunas cuestiones:
Muchos desarrolladores no buscan una respuesta real a esas dos preguntas: la primera la solucionan colocando los tags <script> en el orden correcto. Es una tarea que hay que hacer manualmente y es fácil equivocarse, pero es la que se sigue en muchos casos, por no conocer otro modo de hacerlo. El segundo problema (cómo comunicamos los distintos archivos) también tiene una respuesta sencilla: dado que el contexto global (window en los navegadores, global en NodeJS) se comparte, los distintos ficheros pueden declarar cualquier variable y colocarla en el contexto global para comunicarse. Esto funciona pero poluciona el contexto global.
De todos modos, muchos desarrolladores no prestan atención a este aspecto, pero si desarrollas una librería, pensada para ser usada por otros desarrolladores en sus propias aplicaciones, no puedes polucionar el contexto global ni depender de un orden manual, debes buscar una alternativa.
Los módulos son precisamente esta alternativa: ofrecen un mecanismo para comunicar los distintos ficheros JavaScript de una aplicación (o librería), sin necesidad de ensuciar el contexto global.
Debemos distinguir entre el concepto de simplemente “módulo” (que no deja de ser un patrón de desarrollo y que puede usarse con cualquier motor JavaScript, sin más), con el de “sistema de módulos”. Los sistemas de módulos (que veremos posteriormente) nacen para dar respuesta a la primera de las dos cuestiones: cómo aseguramos que los ficheros se cargan en el orden correcto, mientras que el patrón “módulo”, es simplemente un mecanismo (más o menos elegante, depende de cómo se implemente) de separar nuestro código en archivos JavaScript distintos y permitir comunicarlos entre ellos.
Para que un archivo JavaScript deje de ser simplemente un fichero con código y pase a ser considerado un “módulo” debe cumplir dos condiciones:
Vamos a ver primero cómo implementar un módulo, para posteriormente analizar los distintos sistemas de módulos que existen, para finalizar con el sistema de módulos que incorpora ECMAScript 2015.
Vamos a suponer que tenemos un archivo JavaScript con el siguiente código. Supongamos que el fichero se llama lib.js:
let _value = 0; const inc = function() { _value++; } const value = function() { return _value; }
Este código define una variable (_value) y dos funciones, una para incrementar la variable (inc) y otra para obtener el valor de la variable (value). La idea es que la variable _value sea “privada”, pero no lo es realmente, ya que no tenemos ámbito privado en JavaScript.
Para usar las funciones inc y value basta con incluir el fichero lib.js:
<script src='./lib.js'></script> <script> let x = value(); // x == 0 inc(); let y = value(); // y == 1 </script>
El problema está en que la variable _value no es privada, por lo que cualquiera puede acceder a ella. Eso rompe la encapsulación de lib.js. Muchos desarrolladores usan la convención de que las variables “privadas” empiecen por un subrayado, pero eso no deja de ser una convención: es fácil que por error alguien acceda a una variable (o función) a la que no debería acceder y termine generando algún error. Además las aplicaciones se vuelven más costosas en mantenimiento al no haber una encapsulación real. Necesitamos un mecanismo para ocultar la parte privada (la variable _value).
Una solución es usar el ámbito local. En JavaScript las variables locales, definidas dentro de una función, solo son visibles desde la propia función. Y además en JavaScript podemos declarar funciones dentro de funciones. Podemos crear un ámbito privado en lib.js añadiendo una función que envuelva el código:
(function() { let _value = 0; const inc = function() { _value++; } const value = function() { return _value; } })();
Observa como hemos creado una función anónima autoejecutable. Fíjate que en la última línea ejecutamos la función (por ello la llamamos autoejecutable). Vale, realmente ahora hemos pasado de un extremo a otro. Si incluyes lib.js y luego intentas llamar a _value verás que ya no es accesible (lo esperado). El problema es que ni inc() ni value() lo son tampoco. ¿Cómo podemos exponer las dos funciones “hacia afuera”, para que sean accesibles, a la vez que mantenemos _value privada?
Hay varias maneras de hacer esto, pero por regla general todas son variaciones de la misma técnica fundamental: se trata de colocar en el contexto global un objeto que contenga solo lo que se quiera hacer público. Veamos un ejemplo de cómo podría quedar nuestro fichero lib.js:
let l$ =(function() { let _value = 0; const inc = function() { _value++; } const value = function() { return _value; } return { inc, value }; })();
Observa las dos diferencias. Por un lado, ahora la función anónima autoejecutable devuelve un valor. Este valor es un objeto con todo lo que deseamos hacer público (en este caso las funciones inc() y value()). Y por otro lado, en el mismo fichero lib.js declaramos una variable global (l$) y le asignamos el valor de retorno de la función anónima autoejecutable. Así pues, el resultado de incluir el script lib.js en nuestro código será que se cree la variable l$, la cual contendrá los métodos (y variables) públicos que deseamos:
<script src='./lib.js'></script> <script> let x = value(); // Error: value() no existe en el contexto global let x = l$.value(); // x == 0 l$.inc(); let y = l$.value(); // y == 1 let z = l$._value; // Error: _value no existe en l$ </script>
A este patrón lo llamamos comúnmente “el patrón de módulo”. Dicho patrón nos permite que nuestros ficheros JavaScript tengan un estado privado y expongan hacia el exterior solo la parte pública que deseemos.
La idea fundamental es que los métodos privados se definen dentro de la función anónima autoejecutable y los métodos públicos se exponen a través de un objeto que se devuelve en dicha función anónima autoejecutable.
Existen varias variantes del patrón de módulo. La que hemos visto aquí es una de las más usadas y se conoce con el nombre de Revealing Module Pattern (RPM). Un RMP es un módulo que cumple las siguientes características adicionales:
El Revealing Module Pattern es quizá, el más usado de los patrones de módulo, pero no es el único. Cada uno tiene sus peculiaridades y se diferencian, básicamente, en cómo se exponen las funciones (y variables) públicas “hacia afuera”.
La idea de un módulo es tener un ámbito donde el módulo pueda tener sus variables y métodos privados y exponer solo lo que quiera.
function MathModule() { // Esta será la función privada. function _sumar(a, b) { return a + b; } // Queremos que esta sea la función pública function inc(a) { return _sumar(a, 1); } }
No podríamos usar tal cual este MathModule porque no devuelve nada ni podemos acceder a sus funciones.
Vamos a exportar la función inc que es la que nos interesa:
function MathModule() { // Esta será la función privada. function _sumar(a, b) { return a + b; } // Queremos que esta sea la función pública function inc(a) { return _sumar(a, 1); } return { inc } }
Seguimos sin poder usar la función inc directamente, pero sí lo podemos usar así:
var m$ = MathModule(); m$.inc(10); // -> 11
Lo normal es tener la función anterior como función anónima autoejecutable asignada a una variable global:
var m$ = (function () { // Esta será la función privada. function _sumar(a, b) { return a + b; } // Queremos que esta sea la función pública function inc(a) { return _sumar(a, 1); } return { inc } })();
Ahora podemos usarlo así:
m$.inc(10); // -> 11
Otra forma anotación que se suele encontrar es:
(function () { function MathModule() { // Esta será la función privada. function _sumar(a, b) { return a + b; } // Queremos que esta sea la función pública function inc(a) { return _sumar(a, 1); } return { inc } } window.m$ = MathModule(); })();
Y lo podríamos usar igual que antes. En un navegador, al declarar algo en window, es equivalente a declarar una variable de ámbito global.
Podríamos también escribirlo así:
(function (wnd) { function MathModule() { // Esta será la función privada. function _sumar(a, b) { return a + b; } // Queremos que esta sea la función pública function inc(a) { return _sumar(a, 1); } return { inc } } wnd.m$ = MathModule(); })(window || global);
El ámbito global en NodeJS se llama global, así que el código anterior valdría tanto para navegador como para NodeJS.
El principal problema que tiene el sistema de módulos visto anteriormente es que no se puede saber que un módulo depende de otro para su funcionamiento ni forzar que el motor de ejecución de JavaScript ejecute un módulo antes que otro.
Supongamos un fichero sumar.js con el siguiente contenido:
let s$ = (function () { function sumar(a, b) { return a + b; } return {sumar} })();
Y tenemos otro módulo en un fichero llamado math.js que usará la función sumar anterior:
(function (wnd) { function MathModule() { function inc(a) { // Aquí usamos la función 'sumar' del otro módulo return s$.sumar(a, 1); } return { inc } } wnd.m$ = MathModule(); })(window || global);
Existe ahora una dependencia entre los dos módulos. Si no se carga sumar.js, s$ no existirá y dará error al usar inc, será undefined.
Manualmente, donde carguemos los ficheros JavaScript, podemos establecer el orden necesario:
<!DOCTYPE html> <html> <head> <title>Demo RMP</title> <script src="sumar.js"></script> <script src="math.js"></script> </head> <body> <h1>Demo RMP</h1> </body> </html>
Este método no es mantenible para aplicaciones grandes. Un sistema de módulos real debe proporcionar soporte para:
ECMAScript 2015 da soporte a esas necesidades.
CommonJS es un sistema de módulos que hizo su aparición con NodeJS y es el sistema de módulos usado por éste. Recuerda que un sistema de módulos además de proporcionar una sintaxis para definir nuestros módulos, tiene también un sistema para que los módulos expresen sus dependencias. De esta manera el cargador de módulos sabe en qué orden debe cargar los módulos para que funcionen correctamente.
Un módulo de CommonJS tiene una sintaxis muy simple:
Nota: Lo explicado en esta lección se aplica explícitamente a NodeJS, aunque es posible usar CommonJS también en navegadores, como veremos enseguida.
Para exportar un módulo se coloca en module.exports el valor a exportar. Así, el módulo que vimos anteriormente, declarado en CommonJS quedaría de la siguiente manera (el siguiente código podría estar en un fichero llamado libcj.js):
let _value = 0; const inc = function() { _value++; } const value = function() { return _value; } module.exports = {inc, value};
Observa la simplicidad del código: no necesitamos ninguna función anónima autoejecutable. La variable _value ya es privada: todo es privado al módulo por defecto. Al final, en lugar de devolver algo lo que hacemos es colocar en module.exports el dato a exportar, es decir lo que es público. En este caso queremos hacer públicas las funciones inc() y value(), así que exportamos un objeto que contiene ambas funciones.
Lo que nos falta es ver cómo podemos requerir, o importar, un módulo CommonJS. En NodeJS no tenemos etiquetas <script>. En su lugar usamos la función require(). La función require() toma un argumento (el nombre del módulo CommonJS a cargar) y devuelve un valor, que es lo que el módulo haya exportado. Así para cargar y usar un módulo CommonJS simplemente haríamos:
let counter = require('./lib_cj'); let x = counter.value(); // x == 0 counter.inc(); let y = counter.value(); // y == 1
Observa cómo pasamos el nombre del módulo a cargar (el nombre del fichero, no es necesaria la extensión .js), y cómo guardamos el valor devuelto por require() en la variable counter. Posteriormente podemos usar counter desde nuestro código. Observa que counter contiene el valor exportado por el módulo (el objeto con las funciones inc() y value()).
Ten presente una cosa y es que los módulos se cargan una sola vez. En nuestro caso si tuviésemos dos módulos distintos que requiriesen el módulo lib_cj.js, el módulo se cargaría una única vez. Eso significa que habría una sola variable _value, por lo que llamar a inc() en cualquiera de los dos módulos incrementaría el valor de la misma variable real y afectaría por lo tanto a los dos módulos. En resumen, existe siempre “una sola instancia” de cada módulo CommonJS aunque este módulo se cargue varias veces por estar requerido por varios otros módulos.
Hay cierta confusión con el uso de module.exports y de exports. En efecto, a veces puedes ver módulos CommonJS que exportan sus datos públicos usando exports en lugar de module.exports:
let _value = 0; const inc = function() { _value++; } const value = function() { return _value; } exports.inc = inc; exports.value = value;
Este código es equivalente al código anterior. En general intenta recordar lo siguiente:
exports y module.exports apuntan al mismo objeto vacío.module.exports
Quizá te preguntes por qué existe tanto exports como module.exports. La razón de esa dualidad es por si necesitamos exportar algo que no sea un objeto. Recuerda que al inicio ambas variables apuntan al mismo contenido (un objeto vacío). Podemos agregar propiedades a dicho objeto y al final este objeto será el que se exportará. Ahora bien, si queremos exportar algo que no sea un objeto, como directamente una función, entonces debemos asignar module.exports a dicha función. Si se la asignamos a exports directamente, no funcionará. En resumen, lo más sencillo es que recuerdes siempre que module.exports es lo que se exporta siempre. Y que, inicialmente, module.exports referencia al mismo objeto que exports.
CommonJS es el sistema de módulos propiedad de NodeJS. No está soportado directamente en los navegadores.
Nuestro fichero math.js quedará así:
// Importamos el módulo de 'sumar.js' var sumar = require('./sumar.js'); function inc(a) { // La función 'sumar' es la que está definida en 'sumar.js' return sumar(a, 1); } module.exports = { inc: inc };
module es una palabra clave de NodeJS. Indicamos qué exportará el módulo. En nuestro caso, exportaremos un objeto con función inc.
Ahora vamos al fichero sumar.js, que es dependencia de math.js:
function sumar(a, b) { return a + b; } module.exports = sumar;
Crearemos un fichero main.js:
var math = require('./math.js'); console.log(math.inc(10));
Esto lo tendríamos que ejecutar con NodeJS:
node main # -> 11
De las dependencias se encarga el sistema de módulos que tiene NodeJS (CommonJS).
Cualquier cosa que declare en un módulo, pero no exportemos, será privado. Por ejemplo:
var sumar = require('./sumar.js'); var a = 42; function inc(a) { // La función 'sumar' es la que está definida en 'sumar.js' return sumar(a, 1); } module.exports = { inc: inc };
Si ahora hacemos lo siguiente en main.js:
var math = require('./math.js'); console.log(math.a);
Al ejecutarlo:
node main # -> 'undefined'
Como no hemos exportado la variable a, no podremos acceder a ella.
¿Hay alguna manera de usar el sistema de módulos CommonJS desde el navegador? Existen herramientas como browserify o webpack que nos dan esa funcionalidad. Aquí veremos browserify.
Para instalarlo es necesario contar con NodeJS:
npm install browserify
El comando anterior instalará browserify en el directorio actual.
Y lo usaríamos:
browserify main.js -o bundle.js
Escribirá en el fichero bundle.js que contendrá el polyfill de CommonJS y los módulos que utilizamos. Todo el código (y sus dependencias) junto en un único fichero.
Ahora modificaríamos el fichero HTML para indicar que use este bundle en lugar de los ficheros sumar.js y math.js:
<!DOCTYPE html> <html> <head> <title>Demo RMP</title> <!-- <script src="sumar.js"></script> --> <!-- <script src="math.js"></script> --> <script src="bundle.js"></script> </head> <body> <h1>Demo RMP</h1> </body> </html>
AMD (Asynchronous Module Definition) es un sistema de módulos pensado para que los módulos se carguen de forma asíncrona. Está pues diseñado pensando en los navegadores, donde es más lógico ir cargando los distintos módulos de forma asíncrona. Es cierto que en muchos casos verás que se usan bundles y es debido a que en HTTP/1.1 se intenta minimizar las conexiones al servidor (teóricamente es mejor cargar un bundle con todos los módulos de golpe, que realizar varias conexiones para ir cargando los módulos uno a uno). De todos modos es posible crear bundles a partir de módulos AMD y usar AMD te da la libertad de, en cualquier momento, decidir si quieres usar un bundle o bien ir cargándolos de forma asíncrona. Con total seguridad cuando HTTP/2 se use masivamente, este sistema se popularizará (aunque, lo más deseable sería que se popularizara el sistema de módulos de ES2015 que también soporta carga asíncrona). Esa es la principal diferencia con CommonJS, que solo soporta carga síncrona de módulos.
Declarar un módulo AMD es muy simple ya que usa una sintaxis parecida a la del patrón de módulo clásico de JavaScript. Toda definición de módulo empieza con una llamada a la función define que recibe dos parámetros:
Para exportar un elemento basta con devolverlo desde la función que contiene el código de nuestro módulo. Es decir, lo que devolvamos en la función que contiene el código del módulo (la que pasamos como segundo parámetro a define) es lo que exportamos. Al igual que en CommonJS solo podemos exportar una sola cosa (una función, un valor, un objeto, …).
El siguiente código define un módulo AMD:
define([], function() { let _value = 0; const inc = function() { _value++; } const value = function() { return _value; } return {inc, value}; });
Observa la llamada a define, con el array de dependencias (vacío, este módulo no depende de ningún otro) y la función con el código de nuestro módulo que exporta un objeto simplemente devolviéndolo.
El concepto de “importar” un módulo no existe directamente en AMD: simplemente hemos de declarar dicho módulo en el array de dependencias (y añadir opcionalmente el parámetro en la función que contiene el código de nuestro módulo):
define(['./lib_cj'], function(counter) { let x = counter.value(); // x == 0 counter.inc(); let y = counter.value(); // y == 1 });
Al contener el array de dependencias un valor (lib_cj) la función que define nuestro módulo acepta un parámetro (que es el valor exportado por el módulo libcj.js). El sistema de módulos se encarga de asegurar que el módulo lib_cj.js esté cargado antes de ejecutar nuestro módulo.
Si quieres usar módulos AMD desde un navegador debes usar un cargador de módulos que soporte AMD, ya que los navegadores no los soportan directamente. Un “cargador de módulos” no es nada más que una librería JavaScript que contiene la función define y toda la infraestructura necesaria para cargar los módulos según la especificación de AMD. Hay dos que suelen usarse: RequireJS y SystemJs. Básicamente la configuración mínima consiste en cargar la librería, configurar el cargador (p. ej. indicarle cuál es la ruta base de los módulos) y cargar explícitamente el módulo inicial. Los detalles de configuración no están definidos por AMD por lo que dependen de la librería usada, aunque son muy parecidos.
El siguiente código hace una configuración básica de SystemJS:
<script src="system.js"></script> <script> // Establece la ruta orígen de los módulos System.config({ baseURL: '/js' }); // carga js/main.js (el módulo inicial) System.import('main.js'); </script>
Consulta la página web del cargador que utilices para ver todas las opciones disponibles.
A diferencia de CommonJS, donde los módulos se cargan de forma síncrona, AMD está pensado para carga asíncrona.
Convertiremos los módulos que teníamos con CommonJS a la sintaxis AMD.
Fichero sumar.js:
define([], function() { function sumar(a, b) { return a + b; } // Lo que devolvamos, es lo que el módulo exporta return sumar; });
Vamos al fichero math.js:
define(['./sumar.js'], function(sumar) { function inc(a) { return sumar(a, 1); } return {inc: inc}; });
Vamos con main.js:
define(['./math.js'], function(math) { console.log(math.inc(10)); });
Vamos con la transformación de la página HTML. Como la carga de módulos se hará de forma asíncrona, necesitamos de un cargador de módulos. Aquí veremos System.JS. Lo instalamos mediante bower (gestor de bibliotecas de JavaScript):
npm install -g bower
Una vez lo tenemos instalado, lo usamos para instalar System.JS:
bower install system.js
Se habrá creado la carpeta bower_components con un directorio system.js. Nos interesa el directorio dist donde veremos los ficheros de system.js.
<!DOCTYPE html> <html> <head> <title>Demo RMP</title> <script src="bower_components/system.js/dist/system.js"></script> <script> System.import('./main.js'); </script> </head> <body> <h1>Demo RMP</h1> </body> </html>
Una de las grandes novedades de ECMAScript 2015 fué precisamente que definía un sistema de módulos estándar para JavaScript. Puede parecer superfluo e innecesario existiendo ya CommonJS o AMD, pero la idea es tener un único sistema de módulos que permita compartir éstos de forma sencilla, tanto en escenarios de servidor (Node.js) como de cliente (navegadores).
Cuando apareció ES2015 el sistema de módulos estaba perfectamente definido: se establecieron tanto su sintaxis como sus posibilidades. Pero eso es solo una parte de lo necesario para tener un “sistema de módulos” funcional: necesitamos disponer también de un cargador de módulos, es decir la parte del runtime que se encargará de cargar los distintos módulos desde los ficheros de disco.
Lamentablemente cuando salió ES2015, el cargador de módulos no estaba definido, ni tampoco la API que debía usarse para configurarlo. Esto significaba que no había en el mercado ningún motor de ECMAScript que soportase los módulos de ES2015 de forma nativa. Ningún navegador, ni Node.js en ninguna de sus versiones. Afortunadamente eso ha cambiado y en la actualidad tenemos una especificación completa del sistema de módulos, y todas las versiones modernas de navegadores evergreen ya podemos usarlo, tal y como nos indica caniuse:
La imagen anterior es para el soporte de módulos dinámicos, que son aquellos que se cargan usando el propio lenguaje ECMAScript, a través de la palabra clave import (de una forma similar a como usábamos require en módulos CommonJS). Pero el sistema de módulos de ES2015 va mucho más allá y se soporta también la carga de módulos usando etiquetas <script>. De nuevo caniuse nos muestra el soporte de esta otra característica:
Los módulos ES2015 constan siempre de un fichero de código fuente y esa relación es de 1 a 1 (un fichero define un módulo y un módulo está contenido íntegramente dentro de un fichero). No hay una sintaxis específica para declarar o definir un módulo, cualquier fichero con código fuente JavaScript puede ser cargado como un módulo si es necesario. Cuando un fichero se carga como un módulo:
this no es el contexto global, sino que es undefined."use strict".Insisto: no debes hacer nada para usar un fichero JavaScript como un módulo, basta que lo importes como tal.
Dado que todas las declaraciones son locales al módulo, necesitamos algún elemento para exportar elementos del módulo “hacia afuera”. Lo que exportemos es lo que los otros módulos podrán usar. Solo lo exportado es visible desde fuera del módulo. Existen dos tipos de exportaciones en los módulos de ECMAScript 2015. En este apartado nos centraremos en las exportaciones por defecto.
La sintaxis para exportar una declaración (variable, clase, función o constante) es usando las palabras clave export default. No tienes que hacer nada más, ni devolver nada (como en AMD) ni asignar nada a ningún objeto predefinido (como en CommonJS). Por ejemplo para exportar una clase te basta con:
export default class MyClass { hello() { console.log('hello world');} }
Este código exporta la clase MyClass para que pueda ser usada desde otros módulos. Vamos a asumir que este código está en un fichero llamado myClass.js.
Para importar un módulo usamos la palabra clave import. Dicha palabra espera el módulo a importar (el fichero que contiene el módulo) y carga dicho fichero como un módulo. Por supuesto si el módulo cargado importase otros módulos éstos se cargarían también en el orden apropiado: el sistema de módulos se encarga de gestionar las dependencias (al igual que hacen AMD y CommonJS).
Así, si el código anterior que hemos visto estuviese en myClass.js podríamos importar dicha clase mediante el siguiente código:
import MyClass from './myClass';
La palabra clave import espera un nombre local. Es decir, debemos indicar el nombre que tendrá, en el módulo actual, el elemento exportado por el módulo referenciado. Debes tener muy claro el significado exacto de import. Vamos a repetir el código de nuevo para que quede claro:
import MyClass from './myClass';
Este código no significa que se importe la clase MyClass del módulo myClass dentro del módulo actual. Este código lo que significa es que se importa el valor por defecto que exporte el módulo myClass dentro del módulo actual y que se le asigne el nombre MyClass.
Es decir, dado el mismo módulo myClass que exportaba la clase MyClass podríamos importarlo también de la siguiente manera:
import C from './myClass'; let c = new C(); c.hello();
En este caso damos al valor exportado por el módulo myClass el nombre de C. El nombre “real” de la clase (MyClass) no existe dentro de este segundo módulo.
ECMAScript 2015 también se conoce como ES6 o ECMAScript 6.
Como los módulos de ECMAScript 2015 no están disponibles en ningún motor de JavaScript, necesitaremos un conjunto de herramientas.
Utilizaremos Babel para convertir código de ECMAScript 2015 a ECMAScript 5 y además transformar los módulos de ECMAScript 2015 a módulos de Node.js.
Usaremos browserify para recoger esos módulos y generar un bundle que contenga todo el código y usarlo desde un navegador.
Empezaremos creando un package.json:
npm init
Tras dejar todas las opciones por defecto, tendremos el fichero package.json.
Instalamos browserify y guardamos información en package.json:
npm install browserify --save-dev
Nuestro package.json será similar a:
{ "name": "modulos_es6", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "devDependencies": { "browserify": "^13.0.1" } }
Gracias a este fichero, al compartir nuestro código, otro desarrollador solo tendrá que hacer:
npm install
Y Node.js se encargará de descargar todas las dependencias del proyecto, sin tener que obtener manualmente el contenido de node_modules.
Ahora instalaremos babelify, en lugar de Babel, que es un plugin de browserify que utiliza Babel para transformar el código. Esto lo hacemos por comodidad, para utilizar solo browserify.
npm install babelify babel-polyfill babel-preset-es2015 --save-dev
Contenido del package.json ahora:
{ "name": "modulos_es6", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "devDependencies": { "babel-polyfill": "^6.9.1", "babel-preset-es2015": "^6.9.0", "babelify": "^7.3.0", "browserify": "^13.0.1" } }
Vamos ahora a crear un módulo en un fichero m1.js:
let Answer = 32; export default Answer;
Ahora creamos un fichero c.js que utilice el anterior módulo:
import Answer from './m1'; console.log("Answer is " + Answer);
Ahora creamos un fichero .babelrc en la raíz de nuestro proyecto con la configuración de Babel para indicar qué transformación utilizar (en nuestro caso desde ECMAScript 2015):
{ "presets": ["es2015"] }
Ahora usaremos browserify sobre el módulo inicial (el que tiene todas las dependencias) para generar la transformación de código y el bundle:
./node_modules/.bin/browserify c.js -t babelify --outfile bundle.js
El fichero bundle.js tendrá el contenido de los módulos m1.js y c.js además de lo necesario para que todo funcione en entornos de ECMAScript 5.
Para probarlo, crearemos un fichero index.html con el contenido siguiente:
<!DOCTYPE html> <head> <title>Test</title> <script src="./bundle.js"></script> </head> <body> <h1>Test</h1> </body> </html>
Si no tenemos un servidor web, podemos usar ws:
ws -p 5000
Accederíamos entonces a http://localhost:5000.
Si queremos que browserify nos añada también un fichero source maps:
./node_modules/.bin/browserify c.js -t babelify --outfile bundle.js --debug
Si no queremos teclear tanto, podemos añadir el comando al package.json, en la sección de scripts:
{ // ... "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "bundle": "browserify c.js -t babelify --outfile bundle.js --debug" }, // ... }
Entonces lo usaríamos de la siguiente manera:
npm run bundle
Ahora veremos la conversión de los módulos sumar.js, math.js y main.js a módulos ECMAScript 2015 (ES6).
Empezaremos con sumar.js:
export default function sumar(a, b) { return a + b; } // Antes teníamos lo siguiente: /* define([], function() { function sumar(a, b) { return a + b; } return sumar; }) */
Otra manera de exportar el módulo, sería guardando la función en una constante:
const sumar = (a, b) => a + b; export default sumar;
Vamos con math.js:
import s from './sumar'; function inc(a) { return s(a, 1); } export default {inc: inc}; // Antes teníamos: /* define([], function(sumar) { function inc(a) { return sumar(a, 1); } return {inc: inc} }); */
El fichero principal, main.js:
import math from './math'; console.log(math.inc(10)); // Antes teníamos: /* define(['./math.js'], function(math) { console.log(math.inc(10)); }); */
Vamos a crear el bundle:
npm run bundle
Y ya podríamos usarlo en el fichero HTML.
Una de las limitaciones existentes tanto en CommonJS como en AMD es la imposibilidad de exportar más de un valor. En CommonJS se exporta siempre el valor de module.exports y en AMD se exporta lo que se devuelva en la función que contiene el módulo. Por supuesto puedes exportar un objeto que contenga tres funciones pero no puedes exportar las tres funciones por separado. El sistema de módulos de ECMAScript 2015 sí que lo permite.
Lo habitual es que cada módulo exporte un valor por defecto, ya hemos visto como: usando export default. Pero un módulo puede exportar otros valores además del valor por defecto o incluso no exportar valor por defecto y exportar otros valores. A los otros valores exportados por un módulo se les llama “exports nombrados”.
Quizá te puede parecer innecesario que un módulo pueda exportar varios elementos. Pero tiene mucha utilidad si adoptas un punto de vista de desarrollo más funcional. En este paradigma tus módulos pueden ser colecciones de funciones relacionadas, puedes exportarlas todas y luego cada módulo cliente puede elegir exactamente cuáles quiere importar.
Vamos a ver un ejemplo de un módulo que exporte dos funciones. Asumamos que el código está en math_functions.js:
export function add(...a) { let accum = 0; for (let v of a) {accum += v;} return accum; } export function substract(start, ...a) { let accum = start; for (let v of a) {accum -= v;} return accum; }
Observa cómo se usa export sin default para importar las dos funciones. A esos exports los llamamos “exports nombrados”.
Recuerda: un módulo puede exportar tan solo un valor por defecto y todos los nombrados que desee.
¿Cómo podemos importar un export nombrado? Pues seleccionamos todos aquellos que queremos exportar y aplicamos la desestructuración:
import {add} from './math_functions'; // importamos el export nombrado 'add' let r1 = add(1,2,3); let r2 = substract(10,1,2,3); // Error: substract no está definido
Observa ahora por un lado que la función substract no está definida dentro del segundo módulo ya que no la hemos importado. Y por otro lado, ahora la función add se ha importado dentro del namespace del propio módulo.
Si quisiéramos importar tanto add como substract podemos hacerlo en una sola sentencia import:
import {add, substract} from './math_functions';
Al importar un export nombrado el nombre se preserva, por lo que en el módulo que importa el elemento se llamará tal y como se llamaba en el módulo importado (en nuestro ejemplo add). ¿Hay alguna manera de cambiarlo? Pues la respuesta es que sí, y se debe usar esa sintaxis especial:
import {add as sum} from './math_functions'; // importamos el export nombrado 'add' let r1 = sum(1,2,3); let r2 = add(1,2,3); // Error: add no está definido
La clave es el as sum. Eso asigna el nombre sum al export nombrado add. Así en nuestro módulo ahora debemos usar siempre sum ya que a todos los efectos add no existe.
Usaremos el entorno que creamos antes para ejecutar módulos ES6.
Fichero math.js:
// Función que suma todos los parámetros export function sum(...a) { let accum = 0; for (let v of a) { accum += v; } return accum; } // Cálculo de la media de los valores export function avg(...a) { if (a.length == 0) { return 0; } return sum(...a) / a.length; }
Ya tenemos listos los módulos para importarlos. Creamos ahora el fichero main.js con:
import {sum, avg} from './math'; console.log(sum(1, 2, 3)); console.log(avg(1, 2, 3));
Creamos el bundle:
npm run bundle
Iniciamos un servidor web local (podemos usar local-web-server, de Node.js):
ws -p 5000
Y navegamos a http://localhost:5000. Nos fijamos en la pestaña Consola de las herramientas para desarrolladores.
Podríamos cambiar el nombre de lo que importamos de los módulos. Por ejemplo, cambiando el nombre de avg a average:
import {sum, avg as average} from './math'; console.log(sum(1, 2, 3)); console.log(avgerage(1, 2, 3));
Cuando un módulo de ECMAScript 2015 tiene varios “exports nombrados” podemos importarlos todos de golpe usando el comodín * (asterisco) en el import:
import * as mf from './math_functions'; let v1 = mf.add(1,2,3,4); let v2 = mf.substract(10,1,2,3);
Cuando se usa el asterisco debe indicarse forzosamente un espacio de nombres, mediante el uso de as. En este ejemplo todos los exports nombrados del módulo math_functions.js se incorporarán en el espacio de nombres mf. De ahí que usemos mf.add y mf.substract en lugar de add y substract directamente.
Un módulo ECMAScript puede tener un export por defecto y tantos exports nombrados como desee. Cuando sea el caso debemos tener presente que:
import m from './module' importará el export por defecto del modulo e2import m, {e1 as y1,e2 as y2} from './module' importará el export por defecto del modulo e2 con el nombre local y2import m, * as x from './module' importará el export por defecto del modulo module.js y le asignará el nombre local m y también importará todos los exports nombrados (uso de *) dentro del espacio de nombres x
Como se puede ver, ¡la sentencia import es realmente flexible y potente!
Tenemos el módulo math.js que exporta 2 elementos nombrados: sum y avg.
export function sum(...a) { let accum = 0; for (let v of a) { accum += v; } return accum; } export function avg(...a) { if (a.length == 0) { return 0; } return sum(...a) / a.length; }
En el fichero main.js podremos hacer lo siguiente para importar los dos:
import * as m from './math'; console.log(m.sum(1,2,3)); console.log(m.avg(1,2,3));
Creamos el bundle:
npm run bundle
Iniciamos un servidor web local (podemos usar local-web-server, de Node.js):
ws -p 5000
Y navegamos a http://localhost:5000. Nos fijamos en la pestaña Consola de las herramientas para desarrolladores.
Un módulo ECMASCript 2015 puede exportar (ya sea de forma nombrada o por defecto) cualquier tipo de declaración. Podemos exportar pues:
Los exports por defecto pueden ser anónimos. Eso significa que podemos exportar sin necesidad de colocar nombre alguno a lo que estamos exportando. Eso solo podemos hacerlo en los exports por defecto (ya que el módulo que importa siempre indica un nombre local para los exports por defecto). Los exports nombrados siempre deben tener nombre.
Por lo tanto el siguiente código es correcto (supongamos que está en el fichero export. Así en lugar de tener el siguiente código:
export function foo() {} export class Bar {}
Podemos tener el siguiente código:
function foo() {} // No hay export class Bar {} // No hay export // exportamos ambos exports nombrados de golpe export {foo, Bar};
En ambos casos el resultado es el mismo (dos exports nombrados (foo y Bar)). Usa la sintaxis que prefieras porque son totalmente equivalentes.
La segunda sintaxis usa un enfoque parecido al revealing module pattern: se declara todo lo necesario dentro del módulo y al final se exporta todo lo público. En el caso del revealing se usa una sentencia return que devolvía un objeto conteniendo todos los elementos exportados del módulo. En este caso usamos la sentencia export.
Acostúmbrate a usar el sistema de módulos de ECMAScript. Recuerda que hoy ya se soporta de serie en todos los navegadores modernos (e incluso en Node.js) y en el caso de que debas soportar otros motores puedes usar un transpilador. Así que ¡no hay excusa para no utilizar módulos en tus aplicaciones! aumenta la legibilidad y mantenibilidad de nuestro código al permitir una mayor organización. El sistema de módulos de ES6 es lo suficientemente flexible y potente como para ser usado tanto en pequeños desarrollos como en grandes proyectos. No dudes en experimentar con él y en acostumbrarte a sus posibilidades… ¡pronto descubrirás que no puedes vivir sin él!
Hasta ahora has visto como cargar módulos dinámicamente, es decir a través del propio lenguaje. Pero los módulos se pueden cargar de forma estática a partir de una etiqueta <script>. El uso más común de esto es utilizar dicha etiqueta para cargar el módulo inicial desde una página web. Dicho módulo inicial posteriormente cargará el resto de módulos usando import.
Si no existiese este soporte, el sistema de módulos se quedaría “cojo” para aplicaciones web, ya que no tendríamos ningún mecanismo para cargar dicho módulo inicial.
Para cargar un fichero como si se tratase de un módulo, basta con usar type="module" en la etiqueta <script>. Al usar este valor de type todas las reglas de módulos se aplican a dicho fichero (ya sabes: modo estricto por defecto, this vale undefined y las declaraciones son locales al módulo). Por supuesto, eso implica que dicho código puede usar import y export para cargar dinámicamente otros módulos y exponer resultados respectivamente.
No hay sintaxis definida para “importar” nada de lo que exporten los módulos cargados mediante <script>. Es decir, los valores que exporte un módulo cargado de esta manera, “se pierden”, ya que no pueden recogerse. Debes entender que la idea de cargar un módulo con <script> no es “importar” elementos de dicho módulo, sino que dicho módulo tenga código ejecutable que interaccione con el DOM de la página. Dado que estos módulos se están ejecutando en un navegador tienen acceso al contexto global de ese y por lo tanto a document.
Importante: Los módulos cargados vía el tag <script> con el atributo type="module" se comportan como si tuvieran el atributo ''defer'' aplicado, es decir no bloquean el parser de HTML mientras el script se carga, y no se ejecutan hasta que el documento esté cargado por completo.
Por lo tanto dado el siguiente código:
<script type="module" src="1.js"></script> <script src="2.js"></script> <script defer src="3.js"></script>
El orden de ejecución de los scripts es 2.js, 1.js y 3.js.
De igual modo que ahora disponemos de type="module" para indicar al navegador que cargue un determinado fichero ECMAScript como un módulo, también hay el atributo nomodule, que se usa para indicar que, si el navegador soporta módulos, entonces NO cargue dicho script. De este modo podemos tener dos versiones de nuestro código:
<script type="module"><script nomodule>. Los navegadores que soportan módulos, no cargarán esa versión.
Página index.html con un cuadro de texto:
<!DOCTYPE html> <head> <title>Demo cargar módulos</title> <!-- Como queremos añadir un script JS como módulo, usaremos el atributo "type" con el valor "module" para indicarlo --> <script type="module" src="./bootstrap.js"></script> </head> <body> <h1>Demo cargar modulos</h1> <input type="text" width="50" id="txtOne" value="0"/> <br /> <input type="button" width="50" id="btnInc" /> </body> </html>
Como vemos, añadimos un módulo inicial, bootstrap.js:
import {next} from './inc.js' const txt = document.getElementById('txtOne'); const btn = document.getElementById('btnInc'); btn.addEventListener('click', () => { const value = parseInt(txt.value, 10); txt.value = next(value); }, false);
Como vemos, en ese módulo no se exporta nada. Esto pasa con los módulos que se cargan a través de la etiqueta <script>.
El módulo inc.js:
import sum from './sum.js' const next = i => sum(i,1); export {sum, next};
El módulo sum.js:
export default (a,b) => a+b;
Como hemos visto, ya no es necesario Babel o ningún otro transpilador en las últimas versiones de los navegadores porque soportan los módulos de forma nativa.
Este código tiene varios años, es muy probable que ya no funcione igual o que las notas que acompañen esta sección estén desfasadas
Fichero index.mjs:
import {next} from './inc.mjs' const value = 42; console.log(next(value));
Node.js obliga que los ficheros de módulos tengan la extensión .mjs.
Fichero .inc.mjs:
import sum from './sum.mjs' const next = i => sum(i,1); export {sum, next};
Fichero sum.mjs:
export default (a,b) => a+b;
Ejecutamos con Node.js indicando la característica experimental:
node --experimental-modules index.mjs