Hasta ahora tu código cabía en un fichero. Pero un proyecto de verdad tiene cientos: datos por
un lado, utilidades por otro, cada pantalla en el suyo. Meter todo en un index.js gigante es
inmanejable —no encuentras nada— y abrir veinte <script> distintos es peor: todos comparten
el mismo espacio global y se pisan las variables. La solución son los módulos: cada fichero
es un módulo con su propio espacio, que ofrece lo que quiere compartir (export) y toma
lo que necesita de otros (import).
De <script> normal a <script type="module">#
Durante todo el curso has cargado JavaScript así:
<script src="index.js"></script>Ese script “normal” vive en el ámbito global: cualquier const o function que declares
dentro de él queda disponible en toda la página. Con varios scripts, las variables se pisan entre
sí sin que te avisen. Además, en modo no-estricto, ciertos errores silenciosos pasan desapercibidos.
Los módulos cambian las tres cosas. Para activarlos, basta con añadir type="module" al script:
<!-- type="module" activa los módulos: import/export y modo estricto automático. -->
<script type="module" src="index.js"></script>Con eso en marcha:
- Ámbito propio: lo que declaras en un módulo solo existe en ese fichero. Otro módulo no lo ve a menos que lo exportes. No más variables globales que se pisan.
- Modo estricto automático: los módulos siempre corren en modo estricto (
"use strict"), lo que convierte algunos errores silenciosos en errores reales. import/exporthabilitados: la sintaxis de módulos solo funciona en scripts de tipomodule. Si intentas escribirimporten un<script>sintype="module", el navegador lanza un error.
Esto es la base sobre la que se monta todo React. Lo aprendes aquí, en JavaScript a secas.
export: lo que un módulo ofrece#
Por defecto, todo lo que declaras en un módulo es privado: solo existe dentro de ese
fichero. Para que otro módulo pueda usarlo, lo marcas con export. La forma más común es el
export nombrado: export delante de la declaración.
// datos.js
// Cada export nombrado hace pública esa variable o función, con SU nombre.
export const heroes = [
{ nombre: 'Tracer', rol: 'Daño', partidas: 120, victorias: 78 },
];
export const equipo = { nombre: "King's Row", region: 'Europa' };
// Una función también se exporta igual.
export function winratePct(heroe) {
return ((heroe.victorias / heroe.partidas) * 100).toFixed(1) + '%';
}
// Lo que NO lleva export (una variable interna, un helper privado) no sale del módulo.Un módulo puede tener tantos exports nombrados como quiera: aquí hay tres.
import: lo que un módulo toma#
En otro fichero, import { ... } from './ruta.js' trae esos exports. Entre llaves van los
nombres exactos que se exportaron, y la ruta es relativa y con extensión (./datos.js,
no datos).
// index.js
// Las llaves importan exports NOMBRADOS; los nombres deben coincidir.
import { heroes, winratePct } from './datos.js';
// 1
console.log(heroes.length);
// 65.0%
console.log(winratePct(heroes[0]));Si un nombre no te conviene (choca con otra variable, o es poco claro), lo renombras al importar
con as:
// Trae heroes y winratePct, pero winratePct úsalo localmente como calcularWinrate.
import { heroes, winratePct as calcularWinrate } from './datos.js';
// 65.0%
console.log(calcularWinrate(heroes[0]));export default: la pieza principal del módulo#
Además de los nombrados, un módulo puede tener un export default: representa “lo principal”
que ofrece ese fichero. Solo puede haber uno por módulo (o ninguno).
// formato.js
function winratePct(heroe) {
return ((heroe.victorias / heroe.partidas) * 100).toFixed(1) + '%';
}
// La pieza central de este módulo: una función que arma la ficha de un héroe.
export default function fichaHeroe(heroe) {
return heroe.nombre + ' (' + heroe.rol + ') — ' + winratePct(heroe);
}Se importa sin llaves, y —clave— el nombre lo eliges tú al importar, porque el default no tiene un nombre fijo que respetar:
// index.js
// Sin llaves = export default. 'ficha' es un nombre que elijo yo.
import ficha from './formato.js';
// Tracer (Daño) — 65.0%
console.log(ficha(heroes[0]));Puedes traer el default y nombrados del mismo módulo en una línea: import ficha, { winratePct } from './formato.js'.
Nombrado o default: ¿cuál uso?#
| Export nombrado | Export default | |
|---|---|---|
| Cuántos por módulo | muchos | como mucho uno |
| Al importar | con llaves, nombre exacto | sin llaves, nombre libre |
| Encaja para | un conjunto de utilidades, constantes, varias funciones | la pieza central de un módulo |
En la práctica: para un módulo de utilidades (varias funciones sueltas), nombrados. Para un módulo que gira en torno a una sola cosa —un componente de React, una clase principal—, default. No hay dogma; la mayoría de proyectos modernos tiran sobre todo de nombrados porque renombrar es explícito y el editor autocompleta mejor.
Traer todo un módulo: import * as#
Si quieres todos los exports nombrados de un módulo bajo un único nombre, usas el import de namespace:
// Mete heroes, equipo y winratePct dentro de un objeto llamado Datos.
import * as Datos from './datos.js';
// 1
console.log(Datos.heroes.length);
// King's Row
console.log(Datos.equipo.nombre);Es cómodo para agrupar, aunque normalmente se importa solo lo que se usa: así el editor y las herramientas saben qué depende de qué.
Existe además un import con paréntesis —import('./datos.js')— que carga un módulo solo
cuando hace falta, en vez de al arrancar la página. La sintaxis con paréntesis no te tiene que
preocupar ahora: devuelve una promesa, así que lo retomamos en Más promesas y cancelación,
cuando ese terreno te sea familiar.
El barrel: un punto de entrada único#
Cuando un proyecto tiene muchos módulos, repetir rutas largas cansa. Un barrel (fichero
barril) es un index.js que re-exporta lo público de varios módulos, para que quien los use
importe de un solo sitio:
// modulos/index.js — el barril: no tiene lógica, solo reúne exports.
// re-exporta nombrados
export { heroes, equipo } from './datos.js';
// el default, ahora con nombre
export { default as fichaHeroe } from './formato.js';// Quien consume importa de un único punto, no de cada fichero:
import { heroes, fichaHeroe } from './modulos/index.js';Es un patrón de organización, no una obligación: úsalo cuando de verdad simplifica, no por sistema.
./datos.js con extensión:
es como funcionan los módulos nativos del navegador. Más adelante, con npm y un bundler, verás
imports “pelados” como import { useState } from 'react' sin ruta ni extensión: eso lo resuelve
la herramienta de build, no el navegador. Lo veremos a su tiempo; por ahora, ruta relativa y con
.js.
Pruébalo tú#
Tres módulos conectados, renderizando en la página. Edita el código: por ejemplo, cambia el
import ficha (default) por import { ficha } (con llaves) y observa qué error aparece.
Comprueba lo que sabes#
Pregunta 1 de 5
¿Qué hace export delante de const heroes = [...] en datos.js?
Tu turno#
Tienes un index.js que lo hace todo. Repártelo en módulos con un trabajo claro cada uno.
Edita los ficheros y mira cómo, sin cambiar lo que se ve, el código gana orden. Cuando lo tengas
(o si te atascas), despliega las soluciones y fíjate en el salto de un nivel al siguiente.
Ejercicio · en esta página
Reparte el index.js en módulos
Partes de un index.js que lo hace todo. Sin cambiar lo que se ve, reparte la lógica en módulos con una responsabilidad clara cada uno: mueve el cálculo y el formato a formato.js, impórtalos en index.js y, en niveles altos, saca también el render a su módulo.
Paso 1: Que funcione
- formato.js exporta winrateDe y formatearWinrate.
- index.js los importa desde "./formato.js".
- La página sigue pintando los héroes igual que antes.
Paso 2: Que esté pulido
- Un módulo por responsabilidad: datos, formato, render, orquestación.
- index.js solo importa y conecta.
- Cada módulo importa explícitamente lo que usa.
Paso 3: Que sea excelente
- Cada módulo expone solo lo que se usa fuera; el resto queda interno.
- export default para la pieza principal de un módulo.
- Entiendes el patrón barrel y cuándo aplicarlo.
Ver soluciones
// ════════════════════════════════════════════════════════════════════════════
// NIVEL OK — "que funcione"
//
// Saca el cálculo y el formato fuera de index.js a su propio módulo (formato.js)
// con exports nombrados, y los importa de vuelta. Ya hay una separación: los
// datos en datos.js, el formato en formato.js, la orquestación en index.js.
//
// Funciona y reparte la responsabilidad principal. Sus límites (los pule Mejor):
// - El render sigue dentro de index.js, mezclado con la orquestación.
// - index.js todavía conoce los detalles del HTML.
//
// Esto es una VISTA COMBINADA de los ficheros, separados por cabeceras. En tu
// proyecto cada bloque es un fichero distinto.
// ════════════════════════════════════════════════════════════════════════════
// ───────────────────────────── datos.js ────────────────────────────────────
// Módulo de datos: su único trabajo es tener los héroes y exportarlos.
export const heroes = [
{ nombre: "Tracer", rol: "Daño", partidas: 120, victorias: 78 },
{ nombre: "Reinhardt", rol: "Tanque", partidas: 90, victorias: 51 },
{ nombre: "Mercy", rol: "Apoyo", partidas: 200, victorias: 130 },
{ nombre: "Genji", rol: "Daño", partidas: 150, victorias: 72 },
{ nombre: "Ana", rol: "Apoyo", partidas: 110, victorias: 66 },
{ nombre: "Winston", rol: "Tanque", partidas: 80, victorias: 38 },
];
// ──────────────────────────── formato.js ───────────────────────────────────
// Módulo de formato: cálculo del winrate y su presentación como %.
// export delante de cada función la hace pública para otros módulos.
export function winrateDe(heroe) {
return heroe.partidas === 0 ? 0 : heroe.victorias / heroe.partidas;
}
export function formatearWinrate(winrate) {
return (winrate * 100).toFixed(1) + "%";
}
// ───────────────────────────── index.js ────────────────────────────────────
// Punto de entrada: importa de los otros módulos y conecta las piezas.
// Las llaves { } importan exports NOMBRADOS; los nombres deben coincidir.
import { heroes } from "./datos.js";
import { winrateDe, formatearWinrate } from "./formato.js";
function render(lista) {
const app = document.getElementById("app");
if (!app) return;
const items = lista
.map((h) => `<li>${h.nombre} (${h.rol}) — ${formatearWinrate(winrateDe(h))}</li>`)
.join("");
app.innerHTML = `<h2>Héroes</h2><ul>${items}</ul>`;
}
render(heroes); Por qué este nivel
- Saca el cálculo y el formato a formato.js con exports nombrados y los importa de vuelta.
- Ya hay separación: datos en datos.js, formato en formato.js, orquestación en index.js.
- Su límite: el render sigue dentro de index.js, mezclado con la orquestación.
// ════════════════════════════════════════════════════════════════════════════
// NIVEL MEJOR — "que esté pulido"
//
// Por qué mejora a OK:
// - El render sale a su propio módulo (render.js): ahora cada fichero tiene UN
// trabajo: datos.js (datos), formato.js (cálculo/formato), render.js (DOM),
// index.js (solo orquesta).
// - index.js queda mínimo: importa y conecta. Se lee como un índice.
// - render.js declara qué necesita importando formatearWinrate y winrateDe de
// formato.js: las dependencias entre módulos quedan explícitas.
//
// Qué deja para Excelente: un punto de entrada único por módulo (un "barrel")
// y exportar SOLO lo que se usa, para que la API pública de cada módulo sea
// deliberada.
//
// Vista combinada de los ficheros.
// ════════════════════════════════════════════════════════════════════════════
// ───────────────────────────── datos.js ────────────────────────────────────
export const heroes = [
{ nombre: "Tracer", rol: "Daño", partidas: 120, victorias: 78 },
{ nombre: "Reinhardt", rol: "Tanque", partidas: 90, victorias: 51 },
{ nombre: "Mercy", rol: "Apoyo", partidas: 200, victorias: 130 },
{ nombre: "Genji", rol: "Daño", partidas: 150, victorias: 72 },
{ nombre: "Ana", rol: "Apoyo", partidas: 110, victorias: 66 },
{ nombre: "Winston", rol: "Tanque", partidas: 80, victorias: 38 },
];
// ──────────────────────────── formato.js ───────────────────────────────────
export function winrateDe(heroe) {
return heroe.partidas === 0 ? 0 : heroe.victorias / heroe.partidas;
}
export function formatearWinrate(winrate) {
return (winrate * 100).toFixed(1) + "%";
}
// ───────────────────────────── render.js ───────────────────────────────────
// Módulo de presentación: solo sabe de DOM. Importa lo que necesita de formato.
import { winrateDe, formatearWinrate } from "./formato.js";
export function renderHeroes(lista) {
const app = document.getElementById("app");
if (!app) return;
const items = lista
.map((h) => `<li>${h.nombre} (${h.rol}) — ${formatearWinrate(winrateDe(h))}</li>`)
.join("");
app.innerHTML = `<h2>Héroes</h2><ul>${items}</ul>`;
}
// ───────────────────────────── index.js ────────────────────────────────────
// Solo orquesta: trae los datos, trae el render y los une. Nada más.
import { heroes } from "./datos.js";
import { renderHeroes } from "./render.js";
renderHeroes(heroes); Por qué es mejor que el anterior
- El render sale a su propio módulo: cada fichero tiene UN trabajo (datos, formato, render, orquestación).
- index.js queda mínimo: importa y conecta, se lee como un índice.
- Las dependencias entre módulos son explícitas: render.js declara que necesita formato.js.
// ════════════════════════════════════════════════════════════════════════════
// NIVEL EXCELENTE — "óptimo"
//
// Por qué mejora a Mejor:
// - API pública DELIBERADA por módulo: formato.js solo expone lo que de verdad
// usan los demás (un default fichaHeroe). winrateDe y formatearWinrate se
// quedan como internos del módulo, sin export: nadie de fuera depende de
// ellos, así que mañana puedes cambiarlos sin romper a nadie.
// - export default para "la pieza principal" de un módulo (la ficha de
// formato, el render): se importa sin llaves y deja claro cuál es el uso
// central del módulo.
// - index.js sigue siendo solo orquestación, y un fichero barril (barrel)
// reuniría las exportaciones públicas en un único punto de entrada cuando los
// módulos crezcan (se muestra al final, comentado).
//
// Regla de fondo: cada módulo es una caja con una rendija (su API pública)
// pequeña y estable. Cuanto menos expone, más libre eres de cambiarlo por dentro.
//
// Vista combinada de los ficheros.
// ════════════════════════════════════════════════════════════════════════════
// ───────────────────────────── datos.js ────────────────────────────────────
export const heroes = [
{ nombre: "Tracer", rol: "Daño", partidas: 120, victorias: 78 },
{ nombre: "Reinhardt", rol: "Tanque", partidas: 90, victorias: 51 },
{ nombre: "Mercy", rol: "Apoyo", partidas: 200, victorias: 130 },
{ nombre: "Genji", rol: "Daño", partidas: 150, victorias: 72 },
{ nombre: "Ana", rol: "Apoyo", partidas: 110, victorias: 66 },
{ nombre: "Winston", rol: "Tanque", partidas: 80, victorias: 38 },
];
// ──────────────────────────── formato.js ───────────────────────────────────
// INTERNOS: sin export. Solo existen dentro de este módulo.
function winrateDe(heroe) {
return heroe.partidas === 0 ? 0 : heroe.victorias / heroe.partidas;
}
function formatearWinrate(winrate) {
return (winrate * 100).toFixed(1) + "%";
}
// API PÚBLICA: una sola pieza, la principal del módulo → export default.
// Combina cálculo y formato; el resto del mundo no necesita saber cómo.
export default function fichaHeroe(heroe) {
return `${heroe.nombre} (${heroe.rol}) — ${formatearWinrate(winrateDe(heroe))}`;
}
// ───────────────────────────── render.js ───────────────────────────────────
// import default: sin llaves, eliges tú el nombre local (aquí, fichaHeroe).
import fichaHeroe from "./formato.js";
export default function renderHeroes(lista) {
const app = document.getElementById("app");
if (!app) return;
const items = lista.map((h) => `<li>${fichaHeroe(h)}</li>`).join("");
app.innerHTML = `<h2>Héroes</h2><ul>${items}</ul>`;
}
// ───────────────────────────── index.js ────────────────────────────────────
import { heroes } from "./datos.js";
import renderHeroes from "./render.js";
renderHeroes(heroes);
// ──────────────── (opcional) barrel: src/index.js ──────────────────────────
// Cuando hay muchos módulos, un "barril" reúne sus exports públicos en un punto
// único, para que quien los use importe de un solo sitio:
//
// // modulos/index.js
// export { heroes } from "./datos.js";
// export { default as fichaHeroe } from "./formato.js";
// export { default as renderHeroes } from "./render.js";
//
// Y entonces el consumidor escribe:
// import { heroes, renderHeroes } from "./modulos/index.js"; Por qué es mejor que el anterior
- API pública deliberada: formato.js solo expone un default (fichaHeroe); winrateDe y formatearWinrate quedan internos.
- export default para la pieza principal de cada módulo, importada sin llaves.
- Cuanto menos expone un módulo, más libre eres de cambiarlo por dentro sin romper a nadie. Y muestra el patrón barrel.