Módulos de JavaScript: una guía para principiantes

Si es un recién llegado a JavaScript, jerga como "paquetes de módulos frente a cargadores de módulos", "Webpack frente a Browserify" y "AMD frente a CommonJS" puede volverse rápidamente abrumadora.

El sistema de módulos JavaScript puede resultar intimidante, pero entenderlo es vital para los desarrolladores web.

En esta publicación, analizaré estas palabras de moda en inglés sencillo (y algunas muestras de código). ¡Espero que le sea útil!

Nota: en aras de la simplicidad, esto se dividirá en dos secciones: la Parte 1 explicará qué son los módulos y por qué los usamos. La Parte 2 (publicada la próxima semana) explicará lo que significa agrupar módulos y las diferentes formas de hacerlo.

Parte 1: ¿Puede alguien explicar qué son los módulos de nuevo?

Los buenos autores dividen sus libros en capítulos y secciones; los buenos programadores dividen sus programas en módulos.

Como un capítulo de un libro, los módulos son simplemente grupos de palabras (o código, según sea el caso).

Los buenos módulos, sin embargo, son altamente autónomos con una funcionalidad distinta, lo que les permite barajarlos, eliminarlos o agregarlos según sea necesario, sin interrumpir el sistema en su conjunto.

¿Por qué utilizar módulos?

Hay muchos beneficios al usar módulos a favor de una base de código interdependiente y en expansión. Los más importantes, en mi opinión, son:

1) Mantenibilidad: Por definición, un módulo es autónomo. Un módulo bien diseñado tiene como objetivo disminuir las dependencias en partes de la base de código tanto como sea posible, para que pueda crecer y mejorar de forma independiente. Actualizar un solo módulo es mucho más fácil cuando el módulo está desacoplado de otras piezas de código.

Volviendo a nuestro ejemplo de libro, si quisiera actualizar un capítulo de su libro, sería una pesadilla si un pequeño cambio en un capítulo requiriera que modificara todos los demás capítulos también. En cambio, querrá escribir cada capítulo de tal manera que se puedan realizar mejoras sin afectar a otros capítulos.

2) Espacio de nombres: en JavaScript, las variables fuera del alcance de una función de nivel superior son globales (es decir, todos pueden acceder a ellas). Debido a esto, es común tener "contaminación del espacio de nombres", donde el código completamente no relacionado comparte variables globales.

Compartir variables globales entre código no relacionado es un gran no-no en el desarrollo.

Como veremos más adelante en esta publicación, los módulos nos permiten evitar la contaminación del espacio de nombres al crear un espacio privado para nuestras variables.

3) Reutilización: seamos honestos aquí: todos hemos copiado el código que escribimos anteriormente en nuevos proyectos en un momento u otro. Por ejemplo, imaginemos que copió algunos métodos de utilidad que escribió de un proyecto anterior a su proyecto actual.

Eso está muy bien, pero si encuentra una mejor manera de escribir alguna parte de ese código, tendrá que volver atrás y recordar actualizarlo en todos los demás lugares donde lo escribió.

Obviamente, esto es una gran pérdida de tiempo. ¿No sería mucho más fácil si hubiera, espere, un módulo que podamos reutilizar una y otra vez?

¿Cómo se pueden incorporar módulos?

Hay muchas formas de incorporar módulos a sus programas. Repasemos algunos de ellos:

Patrón de módulo

El patrón del módulo se usa para imitar el concepto de clases (ya que JavaScript no admite clases de forma nativa) para que podamos almacenar métodos y variables públicos y privados dentro de un solo objeto, similar a cómo se usan las clases en otros lenguajes de programación como Java o Python. Eso nos permite crear una API de cara al público para los métodos que queremos exponer al mundo, sin dejar de encapsular variables y métodos privados en un ámbito de cierre.

Hay varias formas de realizar el patrón del módulo. En este primer ejemplo, usaré un cierre anónimo. Eso nos ayudará a lograr nuestro objetivo al poner todo nuestro código en una función anónima. (Recuerde: en JavaScript, las funciones son la única forma de crear un nuevo alcance).

Ejemplo 1: cierre anónimo

(function () { // We keep these variables private inside this closure scope var myGrades = [93, 95, 88, 0, 55, 91]; var average = function() { var total = myGrades.reduce(function(accumulator, item) { return accumulator + item}, 0); return 'Your average grade is ' + total / myGrades.length + '.'; } var failing = function(){ var failingGrades = myGrades.filter(function(item) { return item < 70;}); return 'You failed ' + failingGrades.length + ' times.'; } console.log(failing()); }()); // ‘You failed 2 times.’

Con este constructo, nuestra función anónima tiene su propio entorno de evaluación o "cierre", y luego lo evaluamos inmediatamente. Esto nos permite ocultar variables del espacio de nombres principal (global).

Lo bueno de este enfoque es que puede usar variables locales dentro de esta función sin sobrescribir accidentalmente las variables globales existentes, y aún así acceder a las variables globales, así:

var global = 'Hello, I am a global variable :)'; (function () { // We keep these variables private inside this closure scope var myGrades = [93, 95, 88, 0, 55, 91]; var average = function() { var total = myGrades.reduce(function(accumulator, item) { return accumulator + item}, 0); return 'Your average grade is ' + total / myGrades.length + '.'; } var failing = function(){ var failingGrades = myGrades.filter(function(item) { return item < 70;}); return 'You failed ' + failingGrades.length + ' times.'; } console.log(failing()); console.log(global); }()); // 'You failed 2 times.' // 'Hello, I am a global variable :)'

Tenga en cuenta que los paréntesis alrededor de la función anónima son obligatorios, porque las declaraciones que comienzan con la palabra clave función siempre se consideran declaraciones de función (recuerde, no puede tener declaraciones de función sin nombre en JavaScript). En consecuencia, los paréntesis que rodean crean una expresión de función en lugar. Si tiene curiosidad, puede leer más aquí.

Ejemplo 2: importación global

Otro enfoque popular utilizado por bibliotecas como jQuery es la importación global. Es similar al cierre anónimo que acabamos de ver, excepto que ahora pasamos globales como parámetros:

(function (globalVariable) { // Keep this variables private inside this closure scope var privateFunction = function() { console.log('Shhhh, this is private!'); } // Expose the below methods via the globalVariable interface while // hiding the implementation of the method within the // function() block globalVariable.each = function(collection, iterator) { if (Array.isArray(collection)) { for (var i = 0; i < collection.length; i++) { iterator(collection[i], i, collection); } } else { for (var key in collection) { iterator(collection[key], key, collection); } } }; globalVariable.filter = function(collection, test) { var filtered = []; globalVariable.each(collection, function(item) { if (test(item)) { filtered.push(item); } }); return filtered; }; globalVariable.map = function(collection, iterator) { var mapped = []; globalUtils.each(collection, function(value, key, collection) { mapped.push(iterator(value)); }); return mapped; }; globalVariable.reduce = function(collection, iterator, accumulator) { var startingValueMissing = accumulator === undefined; globalVariable.each(collection, function(item) { if(startingValueMissing) { accumulator = item; startingValueMissing = false; } else { accumulator = iterator(accumulator, item); } }); return accumulator; }; }(globalVariable)); 

En este ejemplo, globalVariable es la única variable que es global. El beneficio de este enfoque sobre los cierres anónimos es que declara las variables globales por adelantado, lo que lo deja muy claro para las personas que leen su código.

Ejemplo 3: interfaz de objeto

Otro enfoque más es crear módulos utilizando una interfaz de objeto autónoma, así:

var myGradesCalculate = (function () { // Keep this variable private inside this closure scope var myGrades = [93, 95, 88, 0, 55, 91]; // Expose these functions via an interface while hiding // the implementation of the module within the function() block return { average: function() { var total = myGrades.reduce(function(accumulator, item) { return accumulator + item; }, 0); return'Your average grade is ' + total / myGrades.length + '.'; }, failing: function() { var failingGrades = myGrades.filter(function(item) { return item < 70; }); return 'You failed ' + failingGrades.length + ' times.'; } } })(); myGradesCalculate.failing(); // 'You failed 2 times.' myGradesCalculate.average(); // 'Your average grade is 70.33333333333333.'

Como puede ver, este enfoque nos permite decidir qué variables / métodos queremos mantener privados (por ejemplo, myGrades ) y qué variables / métodos queremos exponer poniéndolos en la declaración de retorno (por ejemplo, promedio y fallando ).

Ejemplo 4: patrón de módulo revelador

Esto es muy similar al enfoque anterior, excepto que garantiza que todos los métodos y variables se mantengan privados hasta que se expongan explícitamente:

var myGradesCalculate = (function () { // Keep this variable private inside this closure scope var myGrades = [93, 95, 88, 0, 55, 91]; var average = function() { var total = myGrades.reduce(function(accumulator, item) { return accumulator + item; }, 0); return'Your average grade is ' + total / myGrades.length + '.'; }; var failing = function() { var failingGrades = myGrades.filter(function(item) { return item < 70; }); return 'You failed ' + failingGrades.length + ' times.'; }; // Explicitly reveal public pointers to the private functions // that we want to reveal publicly return { average: average, failing: failing } })(); myGradesCalculate.failing(); // 'You failed 2 times.' myGradesCalculate.average(); // 'Your average grade is 70.33333333333333.'

Eso puede parecer mucho para asimilar, pero es solo la punta del iceberg cuando se trata de patrones de módulos. Estos son algunos de los recursos que encontré útiles en mis propias exploraciones:

  • Aprendiendo patrones de diseño de JavaScript por Addy Osmani: un tesoro de detalles en una lectura impresionantemente concisa
  • Adecuadamente bueno por Ben Cherry: una descripción general útil con ejemplos de uso avanzado del patrón de módulo
  • Blog de Carl Danley: descripción general de los patrones del módulo y recursos para otros patrones de JavaScript.

CommonJS y AMD

Los enfoques anteriores tienen una cosa en común: el uso de una sola variable global para envolver su código en una función, creando así un espacio de nombres privado para sí mismo usando un alcance de cierre.

While each approach is effective in its own way, they have their downsides.

For one, as a developer, you need to know the right dependency order to load your files in. For instance, let’s say you’re using Backbone in your project, so you include the script tag for Backbone’s source code in your file.

However, since Backbone has a hard dependency on Underscore.js, the script tag for the Backbone file can’t be placed before the Underscore.js file.

As a developer, managing dependencies and getting these things right can sometimes be a headache.

Another downside is that they can still lead to namespace collisions. For example, what if two of your modules have the same name? Or what if you have two versions of a module, and you need both?

So you’re probably wondering: can we design a way to ask for a module’s interface without going through the global scope?

Fortunately, the answer is yes.

There are two popular and well-implemented approaches: CommonJS and AMD.

CommonJS

CommonJS is a volunteer working group that designs and implements JavaScript APIs for declaring modules.

A CommonJS module is essentially a reusable piece of JavaScript which exports specific objects, making them available for other modules to require in their programs. If you’ve programmed in Node.js, you’ll be very familiar with this format.

With CommonJS, each JavaScript file stores modules in its own unique module context (just like wrapping it in a closure). In this scope, we use the module.exports object to expose modules, and require to import them.

When you’re defining a CommonJS module, it might look something like this:

function myModule() { this.hello = function() { return 'hello!'; } this.goodbye = function() { return 'goodbye!'; } } module.exports = myModule;

We use the special object module and place a reference of our function into module.exports. This lets the CommonJS module system know what we want to expose so that other files can consume it.

Then when someone wants to use myModule, they can require it in their file, like so:

var myModule = require('myModule'); var myModuleInstance = new myModule(); myModuleInstance.hello(); // 'hello!' myModuleInstance.goodbye(); // 'goodbye!'

There are two obvious benefits to this approach over the module patterns we discussed before:

1. Avoiding global namespace pollution

2. Making our dependencies explicit

Moreover, the syntax is very compact, which I personally love.

Another thing to note is that CommonJS takes a server-first approach and synchronously loads modules. This matters because if we have three other modules we need to require, it’ll load them one by one.

Now, that works great on the server but, unfortunately, makes it harder to use when writing JavaScript for the browser. Suffice it to say that reading a module from the web takes a lot longer than reading from disk. For as long as the script to load a module is running, it blocks the browser from running anything else until it finishes loading. It behaves this way because the JavaScript thread stops until the code has been loaded. (I’ll cover how we can work around this issue in Part 2 when we discuss module bundling. For now, that’s all we need to know).

AMD

CommonJS is all well and good, but what if we want to load modules asynchronously? The answer is called Asynchronous Module Definition, or AMD for short.

Loading modules using AMD looks something like this:

define(['myModule', 'myOtherModule'], function(myModule, myOtherModule) { console.log(myModule.hello()); });

What’s happening here is that the define function takes as its first argument an array of each of the module’s dependencies. These dependencies are loaded in the background (in a non-blocking manner), and once loaded define calls the callback function it was given.

Next, the callback function takes, as arguments, the dependencies that were loaded — in our case, myModule and myOtherModule — allowing the function to use these dependencies. Finally, the dependencies themselves must also be defined using the define keyword.

For example, myModule might look like this:

define([], function() { return { hello: function() { console.log('hello'); }, goodbye: function() { console.log('goodbye'); } }; });

So again, unlike CommonJS, AMD takes a browser-first approach alongside asynchronous behavior to get the job done. (Note, there are a lot of people who strongly believe that dynamically loading files piecemeal as you start to run code isn’t favorable, which we’ll explore more when in the next section on module-building).

Aside from asynchronicity, another benefit of AMD is that your modules can be objects, functions, constructors, strings, JSON and many other types, while CommonJS only supports objects as modules.

That being said, AMD isn’t compatible with io, filesystem, and other server-oriented features available via CommonJS, and the function wrapping syntax is a bit more verbose compared to a simple require statement.

UMD

For projects that require you to support both AMD and CommonJS features, there’s yet another format: Universal Module Definition (UMD).

UMD essentially creates a way to use either of the two, while also supporting the global variable definition. As a result, UMD modules are capable of working on both client and server.

Here’s a quick taste of how UMD goes about its business:

(function (root, factory) { if (typeof define === 'function' && define.amd) { // AMD define(['myModule', 'myOtherModule'], factory); } else if (typeof exports === 'object') { // CommonJS module.exports = factory(require('myModule'), require('myOtherModule')); } else { // Browser globals (Note: root is window) root.returnExports = factory(root.myModule, root.myOtherModule); } }(this, function (myModule, myOtherModule) { // Methods function notHelloOrGoodbye(){}; // A private method function hello(){}; // A public method because it's returned (see below) function goodbye(){}; // A public method because it's returned (see below) // Exposed public methods return { hello: hello, goodbye: goodbye } }));

For more examples of UMD formats, check out this enlightening repo on GitHub.

Native JS

Phew! Are you still around? I haven’t lost you in the woods here? Good! Because we have *one more* type of module to define before we’re done.

As you probably noticed, none of the modules above were native to JavaScript. Instead, we’ve created ways to emulate a modules system by using either the module pattern, CommonJS or AMD.

Fortunately, the smart folks at TC39 (the standards body that defines the syntax and semantics of ECMAScript) have introduced built-in modules with ECMAScript 6 (ES6).

ES6 offers up a variety of possibilities for importing and exporting modules which others have done a great job explaining — here are a few of those resources:

  • jsmodules.io
  • exploringjs.com

What’s great about ES6 modules relative to CommonJS or AMD is how it manages to offer the best of both worlds: compact and declarative syntax and asynchronous loading, plus added benefits like better support for cyclic dependencies.

Probably my favorite feature of ES6 modules is that imports are live read-only views of the exports. (Compare this to CommonJS, where imports are copies of exports and consequently not alive).

Here’s an example of how that works:

// lib/counter.js var counter = 1; function increment() { counter++; } function decrement() { counter--; } module.exports = { counter: counter, increment: increment, decrement: decrement }; // src/main.js var counter = require('../../lib/counter'); counter.increment(); console.log(counter.counter); // 1

In this example, we basically make two copies of the module: one when we export it, and one when we require it.

Moreover, the copy in main.js is now disconnected from the original module. That’s why even when we increment our counter it still returns 1 — because the counter variable that we imported is a disconnected copy of the counter variable from the module.

So, incrementing the counter will increment it in the module, but won’t increment your copied version. The only way to modify the copied version of the counter variable is to do so manually:

counter.counter++; console.log(counter.counter); // 2

On the other hand, ES6 creates a live read-only view of the modules we import:

// lib/counter.js export let counter = 1; export function increment() { counter++; } export function decrement() { counter--; } // src/main.js import * as counter from '../../counter'; console.log(counter.counter); // 1 counter.increment(); console.log(counter.counter); // 2

Cool stuff, huh? What I find really compelling about live read-only views is how they allow you to split your modules into smaller pieces without losing functionality.

Then you can turn around and merge them again, no problem. It just “works.”

Looking forward: bundling modules

Wow! Where does the time go? That was a wild ride, but I sincerely hope it gave you a better understanding of modules in JavaScript.

In the next section I’ll walk through module bundling, covering core topics including:

  • Why we bundle modules
  • Different approaches to bundling
  • ECMAScript’s module loader API
  • …and more. :)

NOTE: To keep things simple, I skipped over some of the nitty-gritty details (think: cyclic dependencies) in this post. If I left out anything important and/or fascinating, please let me know in the comments!