En esta página

Modulos ES

10 min lectura TextoCap. 5 — JavaScript moderno

Qué son los módulos ES?

Los módulos ES (ESM) son el sistema de módulos nativo de JavaScript, estandarizado en ES2015. Permiten dividir el código en archivos independientes con sus propias dependencias y exportaciones.

Antes de los módulos ES, JavaScript no tenia un sistema de módulos nativo. Se usaban patrones como IIFE, CommonJS (require) y AMD. Los módulos ES los reemplazan a todos.

Caracteristicas principales

  • Cada archivo es un módulo con su propio scope (no contamina el global)
  • Las dependencias se declaran con import/export de forma explícita
  • Se evaluan en modo estricto automaticamente
  • Los imports son enlaces vivos (reflejan cambios del módulo exportador)

Exports

Named exports

Un módulo puede tener multiples named exports:

// utils.js
export function formatear(fecha) { /* ... */ }
export function validar(email) { /* ... */ }
export const VERSION = '1.0.0';

Default export

Cada módulo puede tener un solo default export. Es útil cuando el módulo exporta una cosa principal:

// Logger.js
export default class Logger {
  log(msg) { console.log(msg); }
}

Cuando usar cada uno

Tipo Cuando usarlo
Named Multiples utilidades, constantes, funciones
Default Clases, componentes, una función principal

Imports

// Named - nombres exactos, con llaves
import { sumar, restar } from './math.js';

// Default - nombre libre, sin llaves
import MiClase from './MiClase.js';

// Ambos
import MiClase, { utilidad } from './módulo.js';

// Renombrar
import { sumar as add } from './math.js';

// Todo como namespace
import * as Utils from './utils.js';

Re-exports (barrels)

Un archivo index.js puede re-exportar desde multiples módulos, creando un punto de entrada único:

// features/index.js
export { UserList } from './user-list.js';
export { UserDetail } from './user-detail.js';
export { UserForm } from './user-form.js';

Esto permite importar todo desde una sola ruta:

import { UserList, UserDetail, UserForm } from './features/index.js';

Dynamic imports

import() como función retorna una promesa y permite cargar módulos bajo demanda. Es clave para:

  • Lazy loading — Cargar código solo cuando se necesita
  • Code splitting — Dividir el bundle en chunks más pequenios
  • Carga condicional — Cargar módulos segun la plataforma o configuración
const boton = document.querySelector('#abrir-editor');
boton.addEventListener('click', async () => {
  const { Editor } = await import('./editor.js');
  const editor = new Editor();
  editor.montar('#contenedor');
});

Modulos en el navegador

Para usar módulos directamente en HTML:

<script type="module" src="app.js"></script>

Los scripts de tipo module son diferidos automaticamente y se ejecutan en modo estricto. En la práctica, los bundlers (Vite, Webpack, esbuild) procesan los módulos para producción.


Práctica

  1. Crea un modulo con named exports: Crea un archivo utils.js que exporte dos funciones (capitalizar y truncar) y una constante MAX_LONGITUD. Importalas en otro archivo y usalas.
  2. Implementa un barrel export: Crea tres modulos pequenios y un archivo index.js que re-exporte todo. Importa desde el barrel y verifica que todas las exportaciones estan disponibles.
  3. Usa dynamic import: Escribe un boton que al hacer click cargue un modulo de forma dinamica con import() y ejecute una funcion del modulo cargado. Observa en la consola de red que el modulo se carga bajo demanda.

En la siguiente leccion exploraremos patrones modernos de JavaScript.

Un archivo, una responsabilidad
Cada módulo debe tener una única responsabilidad. Exporta solo lo necesario y mantiene su implementación interna privada. Esto facilita el testing y la reutilizacion.
Barrel exports con cuidado
Los archivos index.js que re-exportan todo son comodos pero pueden afectar el tree-shaking. En proyectos grandes, importa directamente desde el archivo fuente si el bundle size importa.
javascript
// === math.js ===
// Named exports
export function sumar(a, b) {
  return a + b;
}

export function restar(a, b) {
  return a - b;
}

export const PI = 3.14159;

// === user.js ===
// Default export (uno por archivo)
export default class User {
  constructor(nombre, email) {
    this.nombre = nombre;
    this.email = email;
  }

  saludar() {
    return `Hola, soy ${this.nombre}`;
  }
}

// === constants.js ===
export const CONFIG = Object.freeze({
  API_URL: 'https://api.ejemplo.com',
  VERSION: '2.0.0',
  MAX_RETRIES: 3,
});

export const ROLES = Object.freeze({
  ADMIN: 'admin',
  EDITOR: 'editor',
  LECTOR: 'lector',
});
javascript
// === app.js ===

// Named imports
import { sumar, restar, PI } from './math.js';
console.log(sumar(2, 3));  // 5
console.log(PI);           // 3.14159

// Default import (nombre libre)
import User from './user.js';
const carlos = new User('Carlos', '[email protected]');

// Renombrar imports
import { sumar as add, restar as subtract } from './math.js';

// Importar todo como namespace
import * as MathUtils from './math.js';
console.log(MathUtils.sumar(1, 2));
console.log(MathUtils.PI);

// Re-exportar (barrels)
// === index.js ===
export { sumar, restar } from './math.js';
export { default as User } from './user.js';
export { CONFIG, ROLES } from './constants.js';

// Dynamic import (lazy loading)
async function cargarEditor() {
  const módulo = await import('./editor.js');
  const editor = new módulo.default();
  editor.iniciar();
}

// Condicional
if (necesitaGraficos) {
  const { renderChart } = await import('./charts.js');
  renderChart(datos);
}