CommonJS vs ES Modules

CJS, ESM y otros módulos para Javascript


Es probable, sobre todo si no llevas mucho tiempo en el ecosistema Javascript, que te hayas encontrado con conceptos como CommonJS (CJS), ES Modules (ESM), o incluso algunos ya menos frecuentes como AMD, System, UMD, IIFE o similares. Dichas siglas o nombres, suelen hacer referencia al sistema de módulos (importaciones desde otros archivos) utilizado en Javascript.

Un poco de historia

Antes de 2015, momento en el que nace ECMAScript 2015 (antes llamado ES6) con multitud de cambios y novedades, Javascript carecía de sistema de módulos oficial. Aunque pueda parecer extraño, tiene su sentido: Javascript nació como un lenguaje de programación para el navegador, que servía de apoyo a las páginas HTML+CSS para dotar de interactividad y de mayor dinamismo a su funcionamiento.

Con el tiempo, se comienza a utilizar más y más Javascript en los sitios web. Pero donde se produce un antes y un después, es cuando se hace posible utilizar Javascript fuera del navegador. Su implementación más popular es NodeJS, aunque actualmente existen otras como por ejemplo Deno. Todo esto, junto a la fuerte evolución de Javascript, vuelve muy necesario tener un sistema para incluir código desde ficheros externos y permitir organizar mejor código Javascript que comenzaba a ser muy extenso.

¿Qué es IIFE?

Las siglas IIFE significan Expresión de función invocada inmediatamente (Immediately-invoked function expression), y es una de las primeras formas que aparecen de conseguir encapsular contenido privado en Javascript, antes de 2015. Esto solía conocerse como patrón módulo revelador (Revealing Module Pattern):

var module = (function () {
  /* Data */
  /* Methods */

  // Revealing module
  return {
    /* Public data/methods */
  };
})();

module.data;
module.method();

Aunque no lo parezca, el funcionamiento era simple. Como no teníamos forma de declarar datos privados, la opción utilizada era crear una que en su interior contiene variables y/o funciones. Por último, se devolvía un con los datos/funciones que queríamos que fueran públicos. Los demás, eran privados porque no salían de la función, la cuál se encerraba entre paréntesis y se «autoejecutaba». De esta forma, lo que recibíamos en module era el «objeto revelado».

Como se puede ver, algo muy similar al concepto de clases que conocemos habitualmente. Sin embargo, seguíamos con el problema de no poder importarlo/exportarlo en ficheros a parte.

¿Qué es CommonJS (CJS)?

CommonJS surge cerca de 2009 como una serie de pautas para crear un sistema de módulos en el ecosistema Javascript. Algo más tarde, el equipo de NodeJS implementó parcialmente una versión síncrona de CommonJS, por lo que consigue popularizar un sistema de módulos no oficial como el que puedes ver a continuación:

// module-name.js
module.exports = {
  /* ... */
}

// index.js
const module = require("./module-name.js");
const package = require("package");

module.method();

De esta forma, haciendo un require() podemos importar módulos CommonJS que se exportan con un module.exports desde otros archivos. También es habitual encontrar importaciones de paquetes que habitualmente residen en la carpeta node_modules, obteniendo la carpeta principal del campo main del package.json. Este sistema se conocería más tarde como bare imports (importaciones desnudas), haciendo referencia a que no se indica una ruta de un archivo, sino un con el nombre del paquete.

Sin embargo, estos require() son creados por NodeJS y no son compatibles directamente en navegadores, salvo que se preprocese o transpile antes con alguna herramienta como podría ser un empaquetador o automatizador del estilo de Webpack, Parcel, Rollup, Babel o similar. Estas herramientas buscan los require() y los sustituyen por el código del fichero correspondiente, uniendo y empatando todos los archivos Javascript necesarios de nuestra aplicación web en un sólo archivo Javascript llamado bundle.

¿Qué es AMD?

AMD (Asynchronous Module Definition) nació del descontento de las limitaciones síncronas que tenía CommonJS, que no permitían cargar eficientemente módulos en el lado del cliente. No se llegó a hacer tan popular y extendido como CommonJS, además su sintaxis era algo más compleja de entender:

define(['dep1', 'dep2'], function (dep1, dep2) {
  /* ... */

  return {
    /* ... */
  };
});

AMD podría verse como una mezcla del patrón módulo revelador y una sintaxis donde se usa define(deps, module) para cargar módulos. El parámetro deps es un donde se definen los nombres de las dependencias que se necesitan para ejecutar la función module. Si están cargados, se ejecuta, si no, se carga asincronamente hasta que estén disponibles.

La implementación más popular de AMD fue require.js y era bastante prometedora, sin embargo, los ES Modules aterrizaron en 2015 y al ser nativos y estándar (y mucho más simples), todos estos sistemas de módulos pasarían a un segundo plano o un plano legacy.

Antes de popularizarse CommonJS con Node, nunca se llegó a decidir de forma unánime entre AMD y CommonJS, por lo que apareció también un patrón llamado UMD (Universal Module Definition), que básicamente era un fragmento de código que permitía cargar módulos independientemente de si eran AMD o CommonJS, ya que permitía cargar ambos. Eso sí, la sintaxis era considerablemente más fea y compleja. Poco más tarde, aparecería System.js ofreciendo lo mismo.

¿Qué es ES Modules (ESM)?

En 2015, aterriza ECMAScript 2015 (antiguamente ES6) y con ella multitud de novedades en Javascript. Una de ellas, el sistema de módulos nativos de Javascript. Los tienes detalladamente explicados en Módulos ECMAScript (ESM), pero básicamente, son una evolución de lo mejor de los anteriores, en versión simplificada:

// module.js
export const data = 42;
export const method = () => console.log("Hello");

// index.js
import { data, method } from "./module.js";

Este sistema de módulos nativo por fin nos permite cargar módulos externos con una sintaxis simple y de forma síncrona y asíncrona. Eso sí, con una pequeña pega que seguiremos sufriendo durante un tiempo: esperar que la industria vaya abandonando CommonJS a favor de ESM.

CommonJS vs ESM

Hoy en día, de todo lo anterior, lo más común suele ser utilizar CommonJS o ESM. En ecosistemas donde predomina la utilización de NodeJS, es más frecuente encontrarse usando CommonJS, mientras que en sistemas más modernos, de navegador o, por ejemplo, Deno, es más habitual utilizar el enfoque de ESM.

Al margen de su sintaxis, la cuál ya hemos visto en los ejemplos anteriores, ambos tienen sus diferencias pero las más populares son las siguientes:

CommonJS vs ES Modules

  • NodeJS soporta tradicionalmente la sintaxis require de CommonJS, y aunque cada vez soporta mejor ESM, aún el soporte no es completo y tiene una amplia comunidad con paquetes utilizando CommonJS a través de NPM.

  • CommonJS sólo permite cargar módulos de forma síncrona, mientras que ESM permite carga síncrona y asíncrona.

  • NodeJS permite hacer require() de bare imports utilizando npm mientras que ESM puede hacerlo mediante import maps, un fichero .json que incluye la URL de referencias a los nombres de los paquetes «desnudos».

  • Los require de CommonJS no son compatibles en el navegador de forma directa, mientras que los import de ESM si lo son si se indica el atributo <script type="module"> en los scripts que los utilicen.

  • CommonJS no permite cargar directamente desde una URL o CDN un módulo, mientras que con ESM puedes hacerlo sin problemas y funciona directamente desde un navegador.

  • Con ESM es posible hacer tree-shaking (eliminación de código no utilizado) de serie, mientras que en cambio con CommonJS no es posible, aunque se puede conseguir utilizando plugins de terceros de Webpack como webpack-common-shake.

  • CommonJS se utiliza en sistemas que generan bundles y utilizan técnicas de preprocesado o transpilado para generar builds. Por otro lado, ESM puede utilizarse tanto en entornos de procesados/transpilado o directamente desde el navegador, sin necesidad de transpilar. SkyPack.dev es un proyecto que pretende fomentar y popularizar el uso de paquetes de npm optimizados para utilizar sin necesidad de herramientas de preprocesado.

  • Deno utiliza ESM por defecto, y aunque puede soportar CommonJS, es altamente recomendable utilizar ESM si es posible.

¿Quién soy yo?

Soy Manz, vivo en Tenerife (España) y soy streamer partner en Twitch y profesor. Me apasiona el universo de la programación web, el diseño y desarrollo web y la tecnología en general. Aunque soy full-stack, mi pasión es el front-end, la terminal y crear cosas divertidas y locas.

Puedes encontrar más sobre mi en Manz.dev