Tema
Módulos ES
Cuando tu proyecto crece, un solo archivo app.js de 2,000 líneas se vuelve imposible de mantener. Los módulos te permiten dividir el código en archivos pequeños, cada uno con una responsabilidad, que se importan entre sí. Es la base de cómo se organiza cualquier proyecto profesional, desde una landing hasta una app en React o Node.
Concepto
Piensa en una cocina profesional: hay una estación de cortes, otra de salsas, otra de postres. Cada una hace lo suyo y entrega sus productos a las demás. Un módulo es una estación:
- Exporta (
export) lo que quiere compartir. - Importa (
import) lo que necesita de otros. - Todo lo demás es privado: las variables de un módulo no contaminan el ámbito global.
Antes de los módulos, todos los scripts compartían el mismo ámbito global: si dos archivos declaraban una variable total, una pisaba a la otra. Con módulos, cada archivo tiene su propio ámbito.
Exportar
Exportaciones nombradas
Un archivo puede exportar varias cosas, cada una con su nombre.
js
// utils/formato.js
export const IVA = 0.16;
export function formatearMoneda(cantidad) {
return new Intl.NumberFormat('es-MX', { style: 'currency', currency: 'MXN' }).format(cantidad);
}
export function capitalizar(texto) {
return texto.charAt(0).toUpperCase() + texto.slice(1);
}
// Función privada: no se exporta, nadie fuera de este archivo puede usarla
function redondear(n) {
return Math.round(n * 100) / 100;
}
export const conIva = (precio) => redondear(precio * (1 + IVA));También puedes exportar todo al final, lo que deja ver de un vistazo la "interfaz pública" del módulo:
js
const IVA = 0.16;
function formatearMoneda(cantidad) { /* ... */ }
function capitalizar(texto) { /* ... */ }
export { IVA, formatearMoneda, capitalizar };Exportación por defecto
Cada archivo puede tener una exportación default, normalmente lo "principal" del módulo (una clase, un componente):
js
// services/Carrito.js
export default class Carrito {
constructor() {
this.items = [];
}
agregar(producto) {
this.items.push(producto);
}
}Importar
js
// main.js
// Nombradas: entre llaves y con el MISMO nombre
import { formatearMoneda, conIva } from './utils/formato.js';
// Default: sin llaves y con el nombre que tú quieras
import Carrito from './services/Carrito.js';
// Ambas a la vez
import Carrito2, { IVA } from './services/Carrito.js';
// Renombrar (alias) para evitar choques de nombres
import { formatearMoneda as moneda } from './utils/formato.js';
// Todo el módulo como un objeto
import * as formato from './utils/formato.js';
formato.capitalizar('hola');
formato.IVA;
// Sólo ejecutar el módulo (efectos secundarios, como registrar algo)
import './analytics.js';Error común: la extensión .js
En el navegador debes escribir la ruta completa con extensión: './utils/formato.js'. Sin .js el navegador no encuentra el archivo. Herramientas como Vite permiten omitirla, pero en módulos nativos es obligatoria. Y las rutas relativas deben empezar con ./ o ../.
¿Nombradas o default?
| Nombradas | Default | |
|---|---|---|
| Cantidad por archivo | Muchas | Una |
| Nombre al importar | El mismo (o alias con as) | El que quieras |
| Autocompletado del editor | Excelente | Regular |
| Renombrados accidentales | Imposibles | Posibles |
Buena práctica
Muchos equipos prefieren sólo exportaciones nombradas: el editor autocompleta mejor, los refactors son más seguros y todos usan el mismo nombre. Usa default cuando una herramienta o framework lo pida (por ejemplo, componentes de Vue o páginas de Next.js).
Módulos en el navegador
Para que el navegador trate un archivo como módulo, usa type="module":
html
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Mi tienda</title>
<script type="module" src="./js/main.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>Lo que cambia con type="module":
- Se cargan con
deferautomáticamente (esperan a que el HTML esté listo). - Están en modo estricto siempre (
'use strict'). - Cada módulo tiene su propio ámbito: no crea variables globales.
- Se ejecutan una sola vez, aunque los importes desde diez archivos.
- Permiten
awaiten el nivel superior.
Error común: abrir el HTML con doble clic
Si abres index.html directamente (file:///C:/...) verás un error de CORS: los navegadores no cargan módulos desde el sistema de archivos. Necesitas un servidor local:
- Extensión Live Server de VS Code (clic derecho → Open with Live Server).
npx serveen la carpeta del proyecto.- O Vite (lo verás en npm y Vite).
Llamar funciones desde el HTML
Como los módulos no crean variables globales, esto no funciona:
html
<button onclick="agregarAlCarrito()">Agregar</button> <!-- ReferenceError -->La solución correcta es registrar los eventos desde el módulo con addEventListener (que es, de todos modos, la buena práctica):
js
document.querySelector('#agregar').addEventListener('click', agregarAlCarrito);Importación dinámica
import() como función carga un módulo bajo demanda y devuelve una promesa. Ideal para código pesado que no siempre se necesita (un editor, gráficas, un mapa):
js
const botonGrafica = document.querySelector('#ver-grafica');
botonGrafica.addEventListener('click', async () => {
// El archivo sólo se descarga cuando el usuario lo pide
const { dibujarGrafica } = await import('./graficas.js');
dibujarGrafica(datosVentas);
});
// Cargar según una condición
const idioma = navigator.language.startsWith('en') ? 'en' : 'es';
const textos = await import(`./i18n/${idioma}.js`);Esto se llama code splitting (división de código) y mejora mucho el tiempo de carga inicial.
Organización de un proyecto
Una estructura típica para un proyecto mediano en JavaScript puro:
txt
mi-tienda/
├── index.html
├── css/
│ └── estilos.css
└── js/
├── main.js ← punto de entrada: arranca la app
├── config.js ← constantes (URL de la API, IVA...)
├── api/
│ └── productos.js ← peticiones fetch
├── state/
│ └── carrito.js ← estado y lógica del carrito
├── ui/
│ ├── renderCatalogo.js
│ ├── renderCarrito.js
│ └── toast.js
└── utils/
├── formato.js
└── storage.jsjs
// js/config.js
export const API_URL = 'https://fakestoreapi.com';
export const IVA = 0.16;
// js/api/productos.js
import { API_URL } from '../config.js';
export async function obtenerProductos() {
const r = await fetch(`${API_URL}/products`);
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json();
}
// js/state/carrito.js
import { storage } from '../utils/storage.js';
let items = storage.get('carrito', []);
const suscriptores = new Set();
export function obtenerItems() {
return items;
}
export function agregar(producto) {
const existente = items.find((i) => i.id === producto.id);
items = existente
? items.map((i) => (i.id === producto.id ? { ...i, cantidad: i.cantidad + 1 } : i))
: [...items, { ...producto, cantidad: 1 }];
guardarYNotificar();
}
export function suscribir(fn) {
suscriptores.add(fn);
return () => suscriptores.delete(fn); // función para desuscribirse
}
function guardarYNotificar() {
storage.set('carrito', items);
suscriptores.forEach((fn) => fn(items));
}
// js/main.js
import { obtenerProductos } from './api/productos.js';
import { agregar, suscribir, obtenerItems } from './state/carrito.js';
import { renderCatalogo } from './ui/renderCatalogo.js';
import { renderCarrito } from './ui/renderCarrito.js';
const productos = await obtenerProductos();
renderCatalogo(productos, { alAgregar: agregar });
suscribir(renderCarrito);
renderCarrito(obtenerItems());Archivos "barril" (index.js)
Un archivo index.js que reexporta todo lo de una carpeta simplifica los imports:
js
// js/utils/index.js
export * from './formato.js';
export * from './storage.js';
export { default as debounce } from './debounce.js';
// En otro archivo, un solo import:
import { formatearMoneda, storage, debounce } from './utils/index.js';Buena práctica
- Un módulo, una responsabilidad. Si no puedes describir un archivo en una frase, divídelo.
- Evita dependencias circulares (A importa de B y B importa de A): suelen indicar que hay que extraer un tercer módulo.
- Nombra los archivos por lo que hacen:
formato.js,storage.js, nocosas.jsnihelpers2.js.
Módulos ES vs. CommonJS
En Node.js (y en código antiguo) verás otra sintaxis llamada CommonJS:
js
// CommonJS (Node clásico)
const fs = require('fs');
module.exports = { sumar };
// Módulos ES (estándar moderno: navegador y Node actual)
import fs from 'node:fs';
export { sumar };Hoy los módulos ES son el estándar en ambos lados. En Node se activan con "type": "module" en package.json o usando la extensión .mjs.
Ejercicios
- Crea un módulo
matematicas.jsque exportesumar,restary una constantePI_APROX = 3.14, e impórtalos enmain.js. - Crea un módulo
Contador.jscon una clase exportada por defecto que tengaincrementar()yvalor. Impórtala con el nombreMiContador. - Tienes dos módulos que exportan una función
formatear. Impórtalas en el mismo archivo sin conflicto. - Carga un módulo
confeti.jssólo cuando el usuario haga clic en "Celebrar". - Crea un módulo
toast.jscon una variable privadacontenedory una función exportadamostrarToast(mensaje)que cree el contenedor la primera vez.
Ver solución
js
// 1 — matematicas.js
export const PI_APROX = 3.14;
export const sumar = (a, b) => a + b;
export const restar = (a, b) => a - b;
// main.js
import { sumar, restar, PI_APROX } from './matematicas.js';
// 2 — Contador.js
export default class Contador {
#valor = 0;
incrementar() { this.#valor++; }
get valor() { return this.#valor; }
}
// main.js
import MiContador from './Contador.js';
const c = new MiContador();
// 3
import { formatear as formatearFecha } from './fechas.js';
import { formatear as formatearMoneda } from './moneda.js';
// 4
document.querySelector('#celebrar').addEventListener('click', async () => {
const { lanzarConfeti } = await import('./confeti.js');
lanzarConfeti();
});
// 5 — toast.js
let contenedor; // privada al módulo
export function mostrarToast(mensaje, ms = 3000) {
if (!contenedor) {
contenedor = document.createElement('div');
contenedor.className = 'toasts';
document.body.append(contenedor);
}
const toast = document.createElement('div');
toast.className = 'toast';
toast.textContent = mensaje;
contenedor.append(toast);
setTimeout(() => toast.remove(), ms);
}Resumen
- Un módulo es un archivo con su propio ámbito que comparte sólo lo que exporta.
exportnombrado (muchos por archivo, se importan con llaves) yexport default(uno, nombre libre).- En el navegador:
<script type="module">, rutas con./y extensión.js, y un servidor local. import()dinámico carga código bajo demanda (code splitting).- Organiza por responsabilidad:
api/,state/,ui/,utils/, con unmain.jsde entrada.
Siguiente: POO y clases