Fase 3: Javascript Avanzado Contenidos Transpilación (Vite) Lógica de Javascript Código limpio Patrones Una vez comienzas a comprender los detalles, ¡desbloqueas los límites! ¿Qué es la transpilación? project-name └── src ├── index.html ├── css │ └── global.css ├── modules │ ├── getData.js # Obtener datos │ └── renderCard.js # Pintar en web └── index.js project-name ├── src │ ├── index.html │ ├── css │ │ └── global.css │ ├── modules │ │ ├── getData.js # Obtener datos │ │ └── renderCard.js # Pintar en web │ └── index.js └── dist ├── index.html ├── index.css └── index.js project-name ├── src │ ├── index.html │ ├── css │ │ └── global.css │ ├── modules │ │ ├── getData.js # Obtener datos │ │ └── renderCard.js # Pintar en web │ └── index.js └── dist ├── index.html ├── index-34f7822e.css # hash en CSS └── index-1d901f84.js # hash en JS Hasta ahora, trabajamos directamente con nuestro código. Sin embargo, en el desarrollo web es muy habitual trabajar con transpiladores. Los transpiladores son herramientas que leen un lenguaje o formato, y lo convierten a otro lenguaje. Ejemplos de transpiladores (o herramientas que transpilan): Normalmente el origen está en src/ y el destino en dist/. A veces añade un hash (por motivos de evitar caché). Instalación de Vite Documentación de Vite Creamos estructura de carpetas y un fichero vite.config.js mkdir project-name cd project-name pnpm init --init-type module pnpm add -D vite touch vite.config.js code . pnpm add -D gh-pages import { defineConfig } from 'vite'; export default defineConfig({ root: "/", /* index.html fuera de src/ */ publicDir: "public", plugins: [], server: { port: 1234 }, build: { outDir: "dist", } }); Seguridad Evitar problemas de seguridad de tipo Supply chain (ataque a la cadena de suministro) Tu web es vulnerable al hacer un npm install porque una de tus dependencias fue comprometida Ejemplo de problema de seguridad: paquete axios comprometido v1.14.1 (31-03-2026) minimumReleaseAge: 1440 minimumReleaseAgeExclude: - vite - lightningcss Seguridad Crea un fichero pnpm-workspace.yaml (necesita pnpm 10.16.0+) Añade el campo minimumReleaseAge con el tiempo 1440 (24h) Pnpm no instalará ninguna dependencia que haya sido publicada en menos de 24h Con minimumReleaseAgeExclude no influye a ciertas dependencias Scripts de generación Creamos 3 scripts: desarrollo, generación de build y despliegue # Entorno de desarrollo pnpm run dev # Creamos producción en dist/ pnpm run build # Revisamos la web de producción pnpm run preview # Desplegamos producción pnpm run deploy { /* ... */ "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview", "deploy": "gh-pages -d dist" }, /* ... */ } Preparación para despliegue en GitHub Pages (varia en otros providers) import { defineConfig } from 'vite'; export default defineConfig({ root: "src", base: "/repo-name/", publicDir: "../public", plugins: [], server: { port: 1234 }, build: { outDir: "../dist", } }); import path from "node:path"; import { defineConfig } from 'vite'; const prod = process.env.NODE_ENV === "production"; export default defineConfig({ root: "src", base: prod ? `/${path.basename(process.cwd())}/` : "/", mode: prod ? "production" : "development", publicDir: "../public", plugins: [], server: { port: 1234 }, build: { outDir: "../dist", } }); GitHub Pages requiere despliegue en ruta específica. La URL es https://user.github.io/repo-name/ Por lo tanto, la base es /repo-name/ Automatización: Esta configuración automatiza para que use / en local, y por otro lado /repo-name/ en producción La variable prod detecta si estás ejecutando vite en modo build (producción) En ese caso, hace el build usando el nombre de la carpeta como nombre de repo (mode) ¡Ahora puedes usar ciertas tecnologías! Esto NO es posible en navegadores. Pero SI al transpilarlo Vite a dist/ project-name ├── src │ ├── index.html │ ├── index.scss │ └── index.ts └── dist ├── index.html ├── index-34f7822e.css └── index-1d901f84.js <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> <link rel="stylesheet" href="./index.scss"> <script src="./index.ts"></script> </head> <body> <!-- ... --> </body> </html> Reemplazar PostCSS por LightningCSS Documentación: LightningCSS Instalar LightningCSS → pnpm i -D lightningcss Ejemplo: Drafts: Custom Media Queries import { defineConfig } from 'vite'; export default defineConfig({ /* ... */ css: { transformer: "lightningcss", /* "postcss" */ // lightningcss: { // drafts: { // customMedia: true // } // } } }); @custom-media --modern (width <= 900px); body { background: #111; color: #eee; @media (--modern) { background: indigo; } } Importaciones en Vite NO SON estándar: Sin Vite no te funcionarán ( dependencia de Vite ) import image from "../assets/image.png"; // URL producción: "/assets/image.2d8efhg.png" import image from "../assets/image.png?inline"; // Obtiene como Base64 (inline) import image from "../assets/image.svg?raw"; // Obtiene como texto (contenido) import data from "./assets/file.json"; // Obtiene JSON y parsea directamente a objeto import styles from "./assets/file.css"; // Obtiene CSS y parsea directamente a texto Consejo: Prefiere siempre que puedas la vía estándar (funcionará en proyectos sin Vite) → Rolldown no soporta CSS modules aún import data from "./assets/file.json" with { type: "json" }; // Actualmente, no compatible con Vite import styles from "./assets/file.css" with { type: "css" }; // Actualmente, no compatible con Vite Plugins de Vite Vite se puede extender a base de plugins Busca en npmx por vite-plugin- Ejemplo de plugin → vite-plugin-standard-css-modules Instalación → pnpm i -D vite-plugin-standard-css-modules import { standardCssModules } from 'vite-plugin-standard-css-modules'; import { defineConfig } from 'vite'; export default defineConfig({ /* ... */ plugins: [ standardCssModules() ] }); Lógica Javascript Contenidos Array functions (continuación) Introducción a la complejidad Iteradores Iterator functions Repaso: Array functions: .forEach() 'Gato' 'Pato' 'Oso' 'Topito' // No devuelve nada nunca (undefined) animals.forEach((animal, index) => { console.log(`Elemento #${index}: ${el}`); }); El resultado sería el siguiente: Elemento #0: Gato Elemento #1: Pato Elemento #2: Oso Elemento #3: Topito Array functions: .forEach() vs .map() 'Gato' 'Pato' 'Oso' 'Topito' // No devuelve nada nunca (undefined) animals.forEach((el, index) => { console.log(`Elemento #${index}: ${el}`); }); 4 4 3 6 // Transforma el array original en uno derivado const sizes = animals.map((el, index, array) => { return el.length; }); // Versión abreviada const sizes = animals.map((el) => el.length); Array functions: .every() vs .some() 'Gato' 'Pato' 'Oso' 'Topito' const all = animals.every((el) => { if (el.includes("a")) return true; else return false; }); // Lo anterior es redundante: animals.every(el => el.includes("a")); // F animals.every(el => el.endsWith("o")); // T 'Gato' 'Pato' 'Oso' 'Topito' const all = animals.some((el) => { if (el.includes("a")) return true; else return false; }); // Lo anterior es redundante: animals.some(el => el.includes("a")); // T animals.some(el => el.endsWith("o")); // T Array functions: .find() vs .findIndex() vs .filter() const users = [ { name: "ManzDev", life: 39 }, { name: "felixicaza", life: 75 }, { name: "krepssi", life: 99 }, { name: "DHardySD", life: -5 }, ]; users.find(el => el.life < 50); // { name: "ManzDev", life: 39 } users.findIndex(el => el.life < 50); // 0 const filteredUsers = users.filter(el => el.life > 50); // [ // { name: "felixicaza", life: 75 } // { name: "krepssi", life: 99 } // ] users.filter(el => el.life > 500); // [] users.find(el => el.life > 500); // undefined users.findIndex(el => el.life > 500); // -1 users.findLast(); // Idem pero buscando desde el final users.findLastIndex(); // Idem pero buscando desde el final Array functions: .flat() vs .flatMap() const animals = [ ["Gato", "Tigre"], ["Pato", [ "Ganso", "Gansa" ] ], "Oso" ]; animals.flat(1); // ['Gato', 'Tigre', 'Pato', Array(2), 'Oso'] animals.flat(2); animals.flat(Infinity); // ['Gato', 'Tigre', 'Pato', 'Ganso', 'Gansa', 'Oso'] animals.flatMap(el => /* ... */); // Equivalente a: animals.map(el => /* ... */).flat(1); // ...pero más eficiente: // → flatMap (1 bucle) // → map + flat (2 bucles) Array functions: .reduce() y .reduceRight() const values = [25, 25, 25, 25]; values.reduce((acc, first) => acc + first); // 25 + 25 + 25 + 25 = 100 values.reduce((acc, first) => acc - first); // 25 - 25 - 25 - 25 = -50 // Último parámetro: Valor inicial values.reduce((acc, first) => acc - first, 0); // 0 - 25 - 25 - 25 - 25 = -100 const values = [5, 10, 15, 20]; values.reduceRight((acc, first) => acc - first); // 20 - 15 - 10 - 5 = -10 // Último parámetro: Valor inicial values.reduceRight((acc, first) => acc - first, 0); // 0 - 20 - 15 - 10 - 5 = -50 Notación Big O(N) Complejidad: La notación Big O(N) Es una forma de medir la complejidad del código (tiempo de CPU o memoria) según aumenta el tamaño de la entrada. Complejidad Denominación Descripción O(1) Constante Acceso una carta concreta. Da igual cuantas sean. Inmediato. O(log n) Logarítmica Cartas ordenadas. Búsqueda binaria. Eliminas la mitad y sigues buscando. O(n) Lineal Buscas una carta concreta. Carta por carta hasta encontrarla. O(n log n) Lineal-logarítmica Mazo desordenado. Divides en montones, ordenadas y combinas. O(n²) Cuadrática Ordenas mazo comparando cada carta con el resto. O(2ⁿ) Exponencial Cada carta que añades, dobla el total. O(n!) Factorial Explora todas las combinaciones posibles. Complejidad { "$schema": "./node_modules/oxlint/configuration_schema.json", "env": { "es2024": true }, "ignorePatterns": ["**/node_modules/**"], "plugins": ["eslint", "unicorn", "oxc"], "jsPlugins": ["oxlint-plugin-complexity"], "rules": { "complexity/complexity": ["warn", { "cyclomatic": 4, "cognitive": 4 }] } } Complejidad ciclomática: Número de caminos independientes que puede seguir el código (matemático). Complejidad cognitiva: Mide lo difícil que es para un humano leer y entender el código. Recordemos: Debes tener instalado oxlint pnpm i -D oxlint pnpx oxlint --init Instala dependencia oxlint-plugin-complexity Fichero de ejemplo para testear → bad.js Iteradores Iterar es avanzar por una estructura de forma eficiente (bajo demanda) const data = ["One", "Two", "Three", "Four"]; // Sin iteradores for (let i = 0; i < data.length; i++) { const number = data[i]; console.log(number); } const data = ["One", "Two", "Three", "Four"]; // Iterador implícito data.forEach(number => console.log(number)); // En los arrays, tenemos un iterador "escondido" data[Symbol.iterator] // Veamos como funciona... const data = ["One", "Two", "Three", "Four"]; data[Symbol.iterator] === data.values // true const iterator = data.values() // Array Iterator {} iterator.next() // { value: "One", done: false } iterator.next() // { value: "Two", done: false } // ... iterator.next() // { value: undefined, done: true } [...iterator] // [] (Ya se han "consumido") // Al usar .values(), creas una copia Métodos de iteradores .take(n) → Toma los elementos desde el actual hasta el indicado .drop(n) → Descarta los n primeros elementos .toArray() → Convierte a Array (o usar spread [...]) const data = ["One", "Two", "Three", "Four", "Five", "Six", "Seven"]; const iterator = data.values(); data.values() // Hace copia .drop(3) // Descarta los 3 primeros .take(2) // Se queda sólo con los dos siguientes .toArray(); // Convierte a Array // ["Four", "Five"] Array functions para Iteradores Podemos aplicar las clásicas array functions a los iteradores const messages = ["Hola a todos", "Estoy muy contento", "Adiós"]; const iterator = Iterator.from(messages); iterator.values().forEach(...) // Por cada elemento... iterator.values().every(...) // Si todas cumplen... iterator.values().some(...) // Si al menos una cumple... iterator.values().filter(...) // Filtrar elementos iterator.values().find(...) // Busca un elemento... iterator.values().map(...) // Transforma cada elemento... iterator.values().flatMap(...) // Transforma y aplana cada elemento... iterator.values().reduce(...) // Aplica función, acumulándola... Crear estructuras iterables Funciones pausables, con múltiples retornos → Funciones generadoras Valores únicos e irrepetibles → Symbols const data = { from: 1, to: 5, } [...data] // Uncaught TypeError: data is not iterable function *generator() { yield 1; yield "a"; yield 42; } const it = generator() it.next() // {value: 1, done: false } it.next() // {value: "a", done: false } it.next() // {value: 42, done: false } it.next() // {value: undefined, done: true } const data = { from: 1, to: 5, *[Symbol.iterator]() { for (let i = this.from; i <= this.to; i++) { yield i; } } } [...data] // [1, 2, 3, 4, 5] Código limpio Nombrado inteligente const d = 42; // const days = 42; // const daysSinceCreation = 42; // const genymdhms = /* ... */ // const generateTimestamp = /* ... */ // const calculateTotal = () => { ... } const sendEmail = () => { ... } class User { ... } class Invoice { ... } const isValid = true; Buen nombre → código limpio, breve y sin explicaciones Verbos → funciones Sustantivos → clases Prefijos is/has → booleanos Evita tipos en nombres (ya los da el compilador/editor) Nombres de una letra, sólo en bucles DRY (No te repitas) // No te repitas $product.textContent = `$${(product.price).toFixed(2)}`; $cart.textContent = `$${(cart.total).toFixed(2)}`; $summary.textContent = `$${(summary.amount).toFixed(2)}`; // Generaliza const formatPrice = (n) => `$${n.toFixed(2)}`; $product.textContent = formatPrice(product.price); $cart.textContent = formatPrice(cart.total); $summary.textContent = formatPrice(summary.amount); Si detectas que repites la misma lógica... significa que podrías reutilizar. Extrae la lógica, generaliza si es necesario, y usa una función reutilizable. Más legible, más fácil de mantener si tienes que hacer cambios. YAGNI (No lo necesitas) ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣠⣤⣀⠀⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⣀⣀⣀⣠⠔⠊⠑⠒⣷⠆⢸⢳⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⢀⠔⠋⠁⠀⠀⠉⠁⠒⠤⣄⠋⠀⠈⢧⡇⠀⠀⣰⣶⢆⠀ ⠀⠀⡏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢉⣐⠒⠼⠿⠔⣊⣿⣿⢸⠀ ⠀⠀⡇⠀⠀⠀⠀⢀⣤⡒⠋⣩⢉⠙⠛⢿⣿⡶⣾⣿⣿⣿⡌⠀ ⠀⠀⠹⡄⠀⠀⢀⣾⣾⠁⢘⣭⣷⣶⠃⣿⣾⣷⣿⣿⣟⠝⠀⠀ ⠀⠀⠀⠈⠢⣀⣸⢳⠛⡄⠀⠀⠀⠁⠀⣧⡀⢸⡽⠗⠁⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⠈⠳⡄⠁⢠⠀⠀⠋⢱⢿⣷⡿⠁⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⡿⠀⢀⠐⠀⢭⣭⣽⣩⡇⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⢀⣴⡇⠑⢬⣀⠀⠀⠀⠀⣿⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⢀⠎⠹⡕⠠⢀⣈⠻⢿⣿⣿⣷⣄⠀⠀⠀⠀⠀⠀ ⠀⣀⠤⠒⠒⡇⠀⠀⠈⠢⣔⣭⣙⣛⣿⣽⣇⡏⣣⣀⠀⠀⠀⠀ ⠉⠀⠀⠀⠀⠘⡄⠀⠀⣠⠴⡟⠍⡻⠟⣿⠘⣷⣇⢧⡉⠉⠉⠁ ⠀⠀⠀⠀⠀⠀⣇⣠⠾⠃⠀⠓⠤⢔⣄⢣⠃⡇⣿⠚⠉ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⠉⢧⠁⡇ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠃⠀⠛ Busca el punto medio entre dos extremos: YAGNI → You Aren't Gonna Need It (evita el «por si acaso») COWBOY → Estilo «llanero solitario» (no pensar NADA en el futuro) Evita construir cosas genéricas para problemas que aún no tienes. Abstracción prematura, añade complejidad sin beneficios. Pero tampoco vayas estilo libre, sin pensar más allá del futuro inmediato Funciones puras // Función pura: Misma entrada, siempre misma salida const add = (a, b) => a + b; // Función impura: Depende de algo externo (y lo modifica) let total = 0; const addToTotal = (n) => { total += n; return total; } Una función que siempre produce el mismo resultado para los mismos argumentos. Una función que no modifica nada fuera de su ámbito. Son predecibles y fáciles de testear Una función aleatoria sería impura (no es predecible). Una función que consulta una API es impura (no es predecible). Efectos secundarios: Cambios fuera del alcance de la función. Ej: modificar variable global, escribir en disco, llamar API... No se trata de evitarlas, sino de aislarlas. Tamaño de las funciones // oxlint.json { "rules": { "max-lines": ["warn", { "max": 300, "skipBlankLines": false, "skipComments": false }], "max-lines-per-function": ["warn", 50], } } Si una función no cabe en tu pantalla... hace demasiadas cosas. Divídela y que tengan solo una responsabilidad cada una. No se trata de hacer literalmente una cosa, sino una tarea. Puedes configurar oxlint para que te ayude a detectarlo. Formato rápido (número), formato largo (objeto con opciones) Puedes limitar también el número de líneas por fichero. Evita números y strings mágicos // Números mágicos if (user.role === 3) showAdminPanel(); if (password.length < 8) throw new Error('Muy corta'); setTimeout(logout, 900000); // 15 min // Constantes descriptivas const ROLE_ADMIN = 3; const MIN_PASSWORD_LENGTH = 8; const SESSION_TIMEOUT = 15 * 60 * 1000; if (user.role === ROLE_ADMIN) showAdminPanel(); if (password.length < MIN_PASSWORD_LENGTH) throw new Error('Muy corta'); setTimeout(logout, SESSION_TIMEOUT); Un 42 o un "manzdev" suelto no dice nada. Crea una constante MAX_VALUE o ADMIN_USER y ayudas a que se entienda mejor. Los números con separador: 900_000 mejor que 900000. Pero aún mejor, 15 * 60 * 1000 (min x seg x ms) Composición vs Herencia El problema no es usar herencia, es abusar de la herencia class Bird { fly() { /* ... */ } } class Duck extends Bird { quack() { /* ... */ } } // No vuela, pero hereda fly() class RubberDuck extends Duck { fly() { throw new Error('Patito de goma y no hay nada mal'); } } // Composición const canQuack = { quack() {} }; const canFly = { fly() {} }; const duck = { ...canQuack, ...canFly }; const rubberDuck = { ...canQuack }; Cláusulas de guarda / Early returns Retorna lo antes posible cuando una condición no se cumple. Evita anidamiento excesivo y hace que el flujo del código sea más fácil de leer. // Anidación excesiva, difícil de leer function processOrder(user, cart) { if (user) { if (user.isActive) { if (cart.items.length > 0) { if (cart.total > 0) { return placeOrder(user, cart); } } } } } // Uso de cláusulas de guarda / early return function processOrder(user, cart) { if (!user) return null; if (!user.isActive) return null; if (!cart.items.length) return null; if (cart.total <= 0) return null; return placeOrder(user, cart); } Patrones Patrones Patrones: Estrategias y formas de resolver problemas (que sabemos que funcionan muy bien) Patrones de diseño (Programación): Formas de resolver con código un problema concreto. Patrones de UI/UX (Diseño): Formas de ofrecer al usuario una interfaz para una tarea concreta. Patrón Observador (Programación) class EventEmitter { constructor() { this.listeners = {}; // Store de listeners } on(event, fn) { // Si no existe, inicializamos this.listeners[event] ??= []; this.listeners[event].push(fn); } emit(event, data) { this.listeners[event]?.forEach(fn => fn(data)); } } const emitter = new EventEmitter(); // Escuchar emitter.on('login', user => console.log(`Bienvenido ${user}`) ); emitter.on('login', user => console.log(`Notificando acceso de ${user}`) ); // Emitir emitter.emit('login', 'Manz'); Patrón de Reactividad La reactividad es un patrón derivado del patrón «Observador». Es un patrón en el que los datos se «actualizan» de forma automática (muy usado en ciertos frameworks). Existe una propuesta para añadirlo nativamente a Javascript import { Signal } from "https://cdn.jsdelivr.net/npm/signal-polyfill@0.2.2/dist/index.min.js"; const price = new Signal.State(100); const quantity = new Signal.State(3); const total = new Signal.Computed( () => price.get() * quantity.get() ); console.log(total.get()); // 300 (100 * 3) price.set(150); console.log(total.get()); // 450 (150 * 3) Patrón Breadcrumbs (Diseño) <div class="breadcrumbs"> <div class="part">Home</div> <div class="part">Articles</div> <div class="part">Temática</div> </div> :root { --offset: 0.75rem; --shape-part: polygon(0 0, calc(100% - var(--offset)) 0, 100% 50%, calc(100% - var(--offset)) 100%, 0 100%, var(--offset) 50% ); } .breadcrumbs { display: flex; color: #ccc; font-family: "Victor Mono", sans-serif; font-weight: 600; filter: drop-shadow(0 0 2px #2229); cursor: pointer; .part { background: #222; padding: 0.5rem 2rem; clip-path: var(--shape-part); &:hover { background: indigo } } } Introducción a componentes Cambiar el enfoque global por un enfoque local Pequeños fragmentos de HTML/CSS/JS reutilizables, enfocados en una parte concreta <body> <div class="user"> <h1>Paco</h1> <img src="images/paco.webp" alt="Paco"> </div> <div class="user"> <h1>Sara</h1> <img src="images/sara.webp" alt="Sara"> </div> <!-- ... --> </body> <body> <user-component name="Paco"></user-component> <user-component name="Sara"></user-component> <script type="module"> const user = document.querySelector("user-component"); user.action(); // Su propio JS user.shadowRoot.adoptedStyleSheet // Su propio CSS </script> </body> Preguntas