Fase 4: Javascript (Módulos)

Contenidos

  • Módulos Javascript
  • Importación y exportación de módulos
  • Otros módulos (CSS/JSON/...)
  • Estructura y organización de lógica

Javascript

  • En una web, tenemos dos formas de ejecutar Javascript:
<script>
  // Javascript tradicional
  const message = "Hola ManzDev!";
  console.log(message);
</script>

<!--
 🛑 Navegador bloquea la lectura de HTML
 ➡ Navegador atiende al JS inmediatamente
 ✅ Al terminar, continua con HTML
-->
<script type="module">
  // Como módulo Javascript
  const message = "Hola ManzDev!";
  console.log(message);
</script>

<!--
 ⬇ Navegador descarga JS (pero no ejecuta)
 ✅ Navegador continua leyendo el HTML
 ✅ Al terminar todo el HTML, ejecuta el JS
-->
  • Ya no es necesario usar eventos DOMContentLoaded / $(document).ready() (jquery) o añadir el <script> antes del </body>.

¿Qué es un módulo?

  • Históricamente, Javascript no se podía separar en varios archivos
  • Los módulos solucionan este problema desde 2015 → Módulos ESM

Un poco de historia para tener contexto...

var module = (function () {
  /* Data */
  /* Methods */

  // Revealing module
  return {
    /* Public data/methods */
  };
})();

module.data;        // Acceder a datos
module.method();    // Acceder a métodos

Antes de 2015

  • Antes de los módulos, se popularizó un patrón
  • El patrón Módulo revelador
  • Se basa en una IIFE (Función ejecutada inmediatamente)
  • Una clausura mantiene los datos y métodos en ese módulo
  • Devuelves sólo la información que te interesa
  • Es algo así como una clase (cuando no existían)
define(['dep1', 'dep2'], function (dep1, dep2) {
  /* ... */

  return {
    /* ... */
  };
});





AMD: Asynchronous Module Definition

  • Se define el nombre de las dependencias (parámetro 1)
  • Se define la función a ejecutar (parámetro 2)
  • Implementaciones como las de require.js
  • Era prometedor, pero llegó tarde
  • Los módulos ESM lo dejaron obsoleto
// Importar
const module = require("./module-name.js");
const package = require("package");

module.method();

// Exportar
module.exports = {
  /* ... */
}


CommonJS (CJS)

  • La que terminó adoptando Node
  • Usa require() para cargar / module.exports para exportar
  • Librerías antiguas las siguen usando y la «ocultan» con bundlers
  • npmx.dev / node-modules.dev
  • ❌ No la sigas usando
  • Historia de Javascript → CommonJS vs ESM

Módulos ESM

  • ¿Cómo se usan los módulos Javascript? (ESM)
// Importaciones
import { data } from "./file.js";

// Exportaciones
export const LIFE_UNIVERSE_ANSWER = 42;
export class Human { /* ... */ }
export const method = () => {
  /* ... */
}

Importar un módulo

  • Siempre en la parte superior del archivo .js (o <script>)
<script>
  import { LIFE_UNIVERSE_ANSWER } from "./module.js";   // ❌ No funciona
</script>

ERROR: Uncaught SyntaxError: Cannot use import statement outside a module

<script type="module">
  import { LIFE_UNIVERSE_ANSWER } from "./module.js";   // ✅ Funciona
</script>

Más opciones de importación

// Importamos y renombramos
import { LIFE_UNIVERSE_ANSWER as lifeAnswer } from "./module.js";

// Importamos varios elementos
import { one, two, three } from "./module.js";

// Importamos en un "namespace"
import * as data from "./file.js";

/* Equivale a... */
const data = {
  one: /* ... */,
  two: /* ... */,
}

Importaciones por defecto

  • El nombre se elige en el import. Ejemplo: Usamos Data pero puede ser otro.
  • Pueden parecer cómodas, pero están ligeramente mal vistas (son menos intuitivas).
  • Úsalas sólo si las otras importaciones (nombradas) no te sirven
import Data from "./module.js";

// Data = () => 42;
// export default 42;

export default () => {
  /* ... */
  return 42;
}

export const HELLO = () => "Hello world";

Ruta de los módulos

  • Siempre deben empezar por . o por /.
    • Rutas relativas → empiezan por ./ o ../
    • Rutas absolutas → empiezan por /
  • También puede indicarse una URL/CDN: empezar por http(✅ Browsers ✅ Deno ❌ Bun ❌ Node)
import { element } from "./module.js";                  // Ruta actual
import { element } from "../module.js";                 // Ruta anterior
import { element } from "/module.js";                   // Ruta raíz
import { element } from "https://manz.dev/module.js";   // Ruta absoluta
import { element } from "package";                      // Bare imports (ver más adelante)
import { element } from "node:module";                  // Backend (ver tema de NodeJS)

Uso de CDN

  • unpkg, que trae un paquete o librería de NPM
  • JSDelivr, open source, de NPM también
  • CDNjs, de Cloudflare
  • Cuidado al usar desde URL de terceros → Dependencia de esa web
import { Howl } from "https://unpkg.com/howler?module";
/* Se redirecciona a: https://unpkg.com/howler@2.2.4/dist/howler.js?module */

import { Howl } from "https://cdn.jsdelivr.net/npm/howler@2.2.4/+esm";

/* ⚠ Algunas puede que no funcionen si la librería no está hecha con módulos ESM */
import { Howl } from "https://cdnjs.cloudflare.com/ajax/libs/howler/2.2.4/howler.min.js";

Bare imports

  • Importaciones "desnudas" → Bare imports
  • No son estándar (recordemos que fue un invento de NodeJS)
// Esto generalmente significa que va a buscar el paquete en la carpeta `howler`,
// dentro de la carpeta `node_modules/`
import { Howl } from "howler";
  • ✅ Cómodo de escribir
  • ❌ Dependes de NodeJS (si no tienes Node, no funciona)
  • ❌ No funciona en browsers directamente (salvo que uses un transpilador)

Import Maps

import { element } from "package";
  • Si usamos Node, busca el paquete en la carpeta node_modules/.
  • Import Maps sirven como índices.
  • Se puede redireccionar a ficheros concretos, URL/CDN, carpetas o incluso alias.
  • JSON externos aún no soportados. ¿O si?
    • No uses src, no está soportado aún
    • Usa import o fetch + DOM
<script type="importmap">
  {
    "imports": {
      "package": "/node_modules/package_folder/index.js",
      "other": "https://unpkg.com/package-name/index.js",
      "folder/": "https://unpkg.com/package-name/",
      "./rename.js": "https://unpkg.com/package-name/index.js"
    }
  }
</script>

Barrels

Barrel imports

  • ¡Evitarlos! (o al menos ser muy cuidadoso si usas transpiladores) → Razones
  • ✅ Ventajas: Rutas cortas, centralizado todo en un "barrel".
  • ❌ Desventajas: Herramientas automáticas no pueden optimizar con tree-shaking (Ver más adelante)
// index.js
import { module1, module2, module3 } from "./modules/barrel.js";
// /modules/barrel.js
export { module1 } from "./module1.js";
export { module2 } from "./module2.js";
export { module3 } from "./module3.js";

Import estáticos

  • Hasta ahora, lo que hemos visto son los import estáticos
import { fx } from "./module.js";        // ✅

if (isValid) {
  import { element } from "./file.js";   // ❌
  import { other } from `./${name}.js`;  // ❌
}
  • ❌ Deben hacerse al principio
  • ❌ No puedes interpolar datos
  • ⚠️ Siempre carga el módulo completo, se use o no
  • ❌ No puedes importar si depende de otra cosa (síncrono)
  • ❌ Sólo válidos en un <script type="module">

Import dinámicos

if (isValid) {
  const mod = await import("./file.js");    // ✅
  const mod = await import(`./${name}.js`); // ✅
}

// ✅ Lazy load (sólo cuando ocurre el evento)
button.addEventListener("click", async () => {
  const module = await import("./bigModule.js");
  module.init();
});
  • ✅ No es necesario que estén al principio
  • ✅ Puedes interpolar datos
  • ✅ Se puede importar de forma condicional
  • ✅ Es asíncrono
  • ✅ Se puede ejecutar en un <script>
    (en una IIFE autoejecutable)

¡No sólo módulos Javascript!

import songs from "./songs.json" with { type: "json" };

// Equivalente a:
const songs = [
  { name: "No quiero cambiar", author: "ManzDev" },
  { name: "Resolví el problema", author: "ManzDev" }
];
  • Módulos JSON
  • Equivale a leer un .json + parsearlo
  • ✅ Soporte completo → CanIUse
// No funciona en browsers
import styles from "./songs.css";

// Equivalente a:
const styles = `
body {
  background: indigo;
  color: white;
}`;
  • Importar estilos (usado en frameworks)
  • ❌ No son módulos (lentos, ineficientes)
  • ❌ Requieren transpiladores (vite, webpack, etc...)
  • 📄 Importan strings (cadenas de texto)
import styles from "./songs.css" with { type: "css" };

document.adoptedStyleSheets.push(styles);
element.shadowRoot.adoptedStyleSheet.push(styles);

// Equivalente a:
const styles = new CSSStyleSheet();
styles.replaceSync(`body {
  background: indigo;
  color: white;
}`);
document.adoptedStyleSheets.push(styles);
  • Módulos CSS
  • ✅ Importan un objeto CSSStyleSheet
  • ✅ Funciona en el browser
  • ✅ Permite modificarlo en tiempo real
  • ✅ Eficiente, usa un objeto CSS
  • 🟨 Soporte: Todos menos Safari
import { container } from "./template.html" with { type: "html" };

document.body.append(container);

// Equivalente a:
const container = document.createElement("div");
div.classList.add("container");
div.textContent = "Hola, soy un container";   /* ... */
document.body.append(container);
  • Módulos HTML
  • ✅ Importan un objeto del DOM
  • ✅ Funciona en el browser
  • ✅ Te ahorra toda la gestión del DOM
  • ✅ Ideal como motor de plantillas
  • 🟥 Soporte: Aún en fase de propuesta
import text from "./robots.txt" with { type: "text" };
import html from "./index.html" with { type: "text" };
import css from "./index.css" with { type: "text" };

// Equivalente a:
const text = `Disallow: /legal`;
  • Módulos de texto
  • ✅ Importa un fichero como texto plano
  • ✅ Funciona en el browser
  • ✅ Ideal para tratar contenido
  • 🟥 Soporte: Aún en fase de propuesta
import image from "./image.png" with { type: "bytes" };

// Equivalente en dynamic import()
const image = await import("./image.png", { with: { type: "bytes" }});

// Equivalente a (Deno):
const file = new URL("./image.png", import.meta.url);
const content = await Deno.readFile(file);
const image = new Uint8Array(content);

// Base64 → data:text/plain;base64,
const b64 = btoa(String.fromCharCode(...image));
console.log(b64);
  • Módulos binarios
  • ✅ Importa los bytes del fichero
  • ✅ Funciona en el browser
  • ✅ Ideal para contenido binario
  • ✅ Deno ya lo soporta
  • 🟥 Soporte: Aún en propuesta

Patrón "nomodule"

  • Si necesitas soporte para navegadores muy antiguos...
  • Los navegadores modernos ejecutan modern.js e ignoran los <script nomodule>
  • Los navegadores antiguos no entienden el type="module" pero si cargan <script nomodule>
<script type="module" src="modern.js"></script>
<script nomodule src="legacy.js"></script>

Recordemos:

Script Parsea HTML (sin bloquear) Respeta orden Cuándo ejecuta
<script> clásico ❌ ✅ Inmediato
defer ✅ ✅ Tras parsear HTML
async ⚠️ ❌ Al terminar descarga
type="module" ✅ ✅ Tras parsear HTML
type="module" async ⚠️ ❌ Al terminar descarga
nomodule ❌ ✅ Inmediato (si no soporta ESM)
  • ⚠️ El parseo de HTML se puede bloquear si se descarga antes el Javascript.
  • Los <script nomodule> se ignoran en navegadores modernos.
  • Los <script> con defer o async sólo funcionan con src (no inline).
  • El <script type="module" defer> es redundante.

Módulo como gestor de estados

// Estado global (a nivel de módulo)
let state = { count: 0 };

// Se ejecuta cuando cambia (reactividad casera)
const subscribers = new Set();
export const getState = () => state;

// Actualiza estado y notifica a suscriptores
export const setState = (partial) => {
  state = { ...state, ...partial };
  subscribers.forEach(fn => fn(state));
}

// Suscribe función (se llama cuando cambia estado)
export const subscribe = (fn) => {
  subscribers.add(fn);
  return () => subscribers.delete(fn);  // Fn → cancela sub
}
// ***** counter.js *****
import { getState, setState } from "./store.js";

// Actualiza el estado actual (incrementa en uno)
setState({ count: getState().count + 1 });

// ***** ui.js **********
import { subscribe } from "./store.js";

// Suscripción: Cuando haya cambios en el estado, ejecuta
subscribe(state => {
  console.log("Nuevo estado: ", state);
});

Módulos como bus de eventos

  • Los módulos son un singleton natural. Sólo una instancia en memoria.
// Módulo: EventBus.js

// Estructura para almacenar los eventos escuchados
const listeners = new Map();  // Map<ev, Set<funcion>>

// Obtiene el set de listeners de un evento
// (Si no existe, lo crea)
const getSet = (ev) => {
  let set = listeners.get(ev);
  if (!set) {
    set = new Set();
    listeners.set(ev, set);
  }
  return set;  // Siempre lo devuelve
}
// Asocia una función a un evento
export const on = (ev, fn) => {
  const set = getSet(ev);
  set.add(fn);
  return () => set.delete(fn);  // Para dejar de escuchar
}

// Emite un evento (ejecuta todos sus listeners)
export const emit = (ev, payload) => {
  const set = listeners.get(ev);
  if (!set) return;

  for (const fn of set) {
    fn(payload);
  }
}
// A la hora de utilizar nuestro BUS de eventos:

// ***** user.js **********
import { emit } from "./eventBus.js";

emit("login", { user: "Manz" });

// ***** analytics.js *****
import { on } from "./eventBus.js";

on("login", (data) => {
  console.log("Usuario logueado: ", data.user);
});

Estructura de carpetas

📁 project-name
 ├── 📁 src
 │    ├── 🟧 index.html
 │    ├── 🟨 main.js
 │    ├── 📁 modules
 │    │    ├── 🟨 UserCard.js       # Tarjeta de usuario
 │    │    └── 🟨 ListUsers.js      # Lista de tarjetas de usuario
 │    │
 │    :
 :
  • Hay que comenzar a separar en archivos individuales.
  • Intenta que cada archivo tenga, como máximo, ~150-300 líneas.
📁 project-name
 ├── 📁 src
 │    ├── 🟧 index.html
 │    ├── 🟨 main.js
 │    ├── 📁 modules
 │    │    ├── 🟨 UserCard.js       # Tarjeta de usuario
 │    │    ├── 🟥 UserCard.test.js  # Tests del módulo
 │    │    ├── 🟨 ListUsers.js      # Listas de usuarios
 │    │    └── 🟥 ListUsers.test.js # Tests del módulo:
 :
  • Más adelante, añadiremos archivos para testear nuestro código.
📁 project-name
 ├── 📁 src
 │    ├── 🟧 index.html
 │    ├── 🟨 main.js
 │    ├── 📁 components   # De momento, módulos (más adelante también componentes)
 │    │    ├── 📁 UserCard
 │    │    │    ├── 🟨 UserCard.js       # Componente de tarjeta de usuario
 │    │    │    ├── 🟪 UserCard.css      # Estilos CSS del usuario
 │    │    │    └── 🟥 UserCard.test.js  # Tests del módulo
 │    │    └── 📁 ListUsers
 │    │         ├── 🟨 ListUsers.js      # Componente de lista de usuarios
 │    │         ├── 🟪 ListUsers.css     # Estilos CSS de la lista:         └── 🟥 ListUsers.test.js # Tests del módulo
 :
📁 project-name
 ├── 📁 src
 │    ├── 🟧 index.html
 │    ├── 🟨 main.js
 │    ├── 📁 components   # Aquí irán los componentes
 │    ├── 📁 modules      # Aquí módulos con features específicas:
 :
  • Es buena práctica mantener los componentes sólo para la parte visual.
  • En modules/ o features/ puedes añadir lógica separable y reutilizable.
  • Existen muchas variaciones de estas arquitecturas.
📁 project-name
 ├── 📁 src
 │    ├── 📁 user-profiles   # Aquí todo lo relacionado con los usuarios
 │    │    ├── 📁 UserCard
 │    │    │    ├── 🟨 UserCard.js
 │    │    │    ├── 🟪 UserCard.css
 │    │    │    ├── 🟥 UserCard.test.js
 │    ├── 📁 listing         # Aquí todo lo relacionado con las listas de usuarios
 │    │    ├── 📁 ListUsers
 │    │    ::
 :
  • Ej: Screaming Architecture, «grita» directamente carpetas con lo que hace en lugar de separar por tecnologías

Ejercicio: Prompt de IA + modularizar

Crea una web que va a ser una plataforma de cursos donde voy a añadir videos de youtube que formen parte de un temario. Quiero que a la izquierda aparezca un grid (simulando un árbol de habilidades como el de los videojuegos), pero respecto a los temas del curso y sus secciones (terminal, git, html, css, javascript, etc...).

Quiero que las habilidades salgan como cuadraditos o cubos, apilados encima, debajo, o a los lados (Similar al selector de jugadores del street fighter, debe recordar a ese selector grid) y al seleccionarlo, se vea en grande su información a la derecha. Esta parte los cuadraditos rectos, con esquinas afiladas, como cubos, compactos y que no sean muy grandes. Es importante que esta parte esté hecha con webcomponents para que sea facil de reutilizar.

Toda esta parte estará en un menú lateral del lado izquierdo de la página. La página principal estará a la derecha, donde aparecerá el contenido seleccionado.

Ten en cuenta que el contenido es así:

  • Curso: Nombre del curso, que incluye varias fases.
  • Fase: Sección virtual para agrupar varios temas.
  • Tema: Cada uno de los temas de una fase. Cada tema es un cuadradito de los que mencionamos. Al pulsar en el tema, mostramos a la derecha todo el contenido de ese tema.
  • Cada tema tiene: un video del tema completo, generalmente de varias horas (4-5). Ese tema tiene highlights a minutos especificos. Opcionalmente puede tener capitulos.
  • Capitulos: Videos editados y resumidos del tema que puede agrupar un conjunto de highlights.

Busca una forma de mostrarlo todo, que sea natural y recordando que todos los temas tienen highlights, pero puede que no tengan capitulos, o puede que si.

Quiero que lo hagas en HTML/CSS/JS vanilla y que esten componentizados (webcomponents) sus partes más sensibles a reutilizar, para poder personalizarlo mas tarde y reutilizar al maximo. Estilo moderno, limpio y darkmode con accents indigo y deeppink.

Lee esta imagen para entender el estilo visual que va a tener.