learning-front

Nivel 3 · JavaScript moderno y asíncrono

Módulos: import y export

Partir el código en ficheros con un trabajo claro cada uno: export para ofrecer, import para tomar, default frente a nombrado, y por qué cada cosa vive en su sitio.

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í:

html
<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:

html
<!-- 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/export habilitados: la sintaxis de módulos solo funciona en scripts de tipo module. Si intentas escribir import en un <script> sin type="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.

javascript
// 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).

javascript
// 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:

javascript
// 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).

javascript
// 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:

javascript
// 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 nombradoExport default
Cuántos por módulomuchoscomo mucho uno
Al importarcon llaves, nombre exactosin llaves, nombre libre
Encaja paraun conjunto de utilidades, constantes, varias funcionesla 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:

javascript
// 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éntesisimport('./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:

javascript
// 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';
javascript
// 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.

Un apunte sobre rutas. Aquí escribimos ./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.
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.