TechLead

Módulos ES

Sintaxis import/export, imports dinámicos, patrones de módulos y empaquetado

Módulos ES (ESM)

Los Módulos ES son el sistema de módulos oficial y estandarizado para JavaScript. Proporcionan una sintaxis limpia para organizar código en piezas reutilizables con imports y exports explícitos. ESM está soportado en todos los navegadores modernos y Node.js.

Características Clave

  • Análisis estático — Los imports/exports se determinan en tiempo de compilación
  • Modo estricto — Los módulos siempre están en modo estricto
  • Ejecución diferida — Los scripts de módulo son diferidos por defecto
  • Instancia única — Los módulos son singletons (cacheados después de la primera carga)

Exports Nombrados

// math.js — Exports nombrados
export const PI = 3.14159;

export function add(a, b) {
  return a + b;
}

export function multiply(a, b) {
  return a * b;
}

// También se puede exportar al final
const subtract = (a, b) => a - b;
const divide = (a, b) => a / b;

export { subtract, divide };

// Renombrar al exportar
export { subtract as sub, divide as div };
// main.js — Imports nombrados
import { add, multiply, PI } from "./math.js";

console.log(add(2, 3));      // 5
console.log(multiply(4, 5)); // 20
console.log(PI);             // 3.14159

// Renombrar al importar
import { add as suma, multiply as mult } from "./math.js";

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

Exports por Defecto

// logger.js — Export por defecto
export default function log(mensaje) {
  console.log(`[LOG] ${mensaje}`);
}

// O con clase
export default class Logger {
  log(msg) {
    console.log(msg);
  }
}

// O exportar al final
function log(mensaje) {
  console.log(mensaje);
}
export default log;
// main.js — Import por defecto (cualquier nombre funciona)
import log from "./logger.js";
import miLogger from "./logger.js"; // Lo mismo, diferente nombre

log("¡Hola!");

// Mezclar default y nombrados
import log, { formatMessage, LogLevel } from "./logger.js";

Re-exportación

// utils/index.js — Archivo barrel (re-exports)
export { add, subtract } from "./math.js";
export { formatDate } from "./date.js";
export { default as Logger } from "./logger.js";

// Re-exportar todo
export * from "./math.js";

// Re-exportar con renombre
export { add as suma } from "./math.js";
// main.js — Imports limpios desde barrel
import { add, formatDate, Logger } from "./utils/index.js";
// o
import { add, formatDate, Logger } from "./utils"; // Con bundler

Imports Dinámicos

// Import estático (arriba del archivo, siempre carga)
import { funcionPesada } from "./modulo-pesado.js";

// Import dinámico (carga bajo demanda, retorna Promise)
async function cargarModulo() {
  const modulo = await import("./modulo-pesado.js");
  modulo.funcionPesada();
}

// Carga condicional
if (necesitaFuncion) {
  const { funcion } = await import("./funcion.js");
  funcion();
}

// Cargar basado en acción del usuario
boton.addEventListener("click", async () => {
  const { mostrarModal } = await import("./modal.js");
  mostrarModal();
});

// Con manejo de errores
try {
  const modulo = await import(`./locales/${idioma}.js`);
  aplicarTraducciones(modulo.traducciones);
} catch (error) {
  console.error("Falló cargar locale:", error);
}

Usando Módulos en HTML

<!-- Script de módulo (diferido por defecto) -->
<script type="module" src="main.js"></script>

<!-- Módulo inline -->
<script type="module">
  import { saludar } from "./utils.js";
  saludar("Mundo");
</script>

<!-- Fallback para navegadores antiguos -->
<script nomodule src="legacy-bundle.js"></script>

<!-- Precargar módulos para rendimiento -->
<link rel="modulepreload" href="./utils.js">

CommonJS vs Módulos ES

Característica CommonJS (CJS) Módulos ES (ESM)
Sintaxis require() / module.exports import / export
Carga Síncrona Asíncrona
Análisis Dinámico (runtime) Estático (compile time)
Tree shaking Difícil Soporte integrado
Navegador Necesita bundler Soporte nativo

Patrones de Módulos

// Patrón singleton
let instancia = null;

export function obtenerInstancia() {
  if (!instancia) {
    instancia = crearInstancia();
  }
  return instancia;
}

// Factory con estado privado
const datosPrivados = new WeakMap();

export class Usuario {
  constructor(nombre) {
    datosPrivados.set(this, { nombre });
  }
  
  obtenerNombre() {
    return datosPrivados.get(this).nombre;
  }
}

// Módulo de configuración
export const config = Object.freeze({
  API_URL: "https://api.ejemplo.com",
  TIMEOUT: 5000,
  VERSION: "1.0.0"
});

// Import solo por efectos secundarios
// analytics.js
console.log("Analytics cargado");
window.analytics = { track: () => {} };

// main.js
import "./analytics.js"; // Solo ejecuta el módulo

ESM en Node.js

// package.json - Habilitar ESM para todo el proyecto
{
  "type": "module"
}

// O usar extensión .mjs para archivos ESM
// utils.mjs

// Importar JSON (requiere aserción)
import datos from "./datos.json" with { type: "json" };

// Importar desde node_modules
import express from "express";
import { useState } from "react";

// Built-ins de Node
import fs from "node:fs";
import path from "node:path";

// Equivalente de __dirname en ESM
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

// import.meta
console.log(import.meta.url); // file:///ruta/al/modulo.js

💡 Puntos Clave

  • • Usa exports nombrados para múltiples valores, default para export principal
  • • import() dinámico para code splitting y lazy loading
  • • Archivos barrel (index.js) simplifican imports
  • • ESM habilita tree shaking para bundles más pequeños
  • • Los módulos son singletons y siempre usan modo estricto
  • • Usa "type": "module" en package.json para ESM en Node.js

Práctica y tips de migración

  • • Convierte un archivo CommonJS a ESM y lista los cambios de ruta de import que necesitaste (extensión, default vs nombrado).
  • • Añade un import() dinámico para un modal o gráfico; mide tamaño de bundle y tiempo de carga inicial.
  • • Crea un archivo barrel (index.ts) para una carpeta y asegúrate de que tree-shaking aún elimina exports no usados.
  • • En Node, experimenta con "type": "module" vs extensiones .cjs/.mjs y nota las advertencias de interop.