HDP115

Módulos y Empaquetadores

Aprende a organizar tu código JavaScript en módulos reutilizables y a utilizar empaquetadores como Webpack, Rollup y Parcel para optimizar tus aplicaciones.

CE

Cristian Escalante

Última actualización: 23 de abril de 2025

javascript
programación web
desarrollo frontend

Introducción a los módulos en JavaScript

Los módulos son unidades de código independientes y reutilizables que permiten organizar el código en archivos separados. Esto facilita:

  • Mantener el código organizado y modular
  • Evitar conflictos de nombres (namespace)
  • Gestionar dependencias entre componentes
  • Reutilizar código en diferentes partes de la aplicación
  • Mejorar la colaboración en equipos de desarrollo

Históricamente, JavaScript no tenía un sistema de módulos nativo, lo que llevó a la creación de diferentes soluciones como AMD, CommonJS y UMD. Actualmente, ECMAScript Modules (ESM) es el estándar oficial.

Sistemas de módulos en JavaScript

ECMAScript Modules (ESM)

Es el sistema de módulos nativo de JavaScript, introducido en ES6 (ES2015).

Exportar módulos

// math.js

// Exportación con nombre
export function suma(a, b) {
  return a + b;
}

export function resta(a, b) {
  return a - b;
}

// Variables exportadas
export const PI = 3.14159;

// Exportación por defecto (solo una por archivo)
export default function multiplicacion(a, b) {
  return a * b;
}

// También se puede exportar al final del archivo
const division = (a, b) => a / b;
const potencia = (a, b) => a ** b;

export { division, potencia };

Importar módulos

// app.js

// Importar exportación por defecto
import multiplicacion from './math.js';

// Importar exportaciones con nombre
import { suma, resta, PI } from './math.js';

// Renombrar importaciones
import { suma as sumar, resta as restar } from './math.js';

// Importar todas las exportaciones con nombre en un objeto
import * as MathUtils from './math.js';

// Importar exportación por defecto y con nombre juntas
import multiplicacion, { suma, resta } from './math.js';

// Uso
console.log(suma(5, 3));  // 8
console.log(multiplicacion(4, 2));  // 8
console.log(MathUtils.potencia(2, 3));  // 8
console.log(PI);  // 3.14159

Importaciones dinámicas

// Cargar un módulo bajo demanda
const cargarModulo = async () => {
  try {
    const modulo = await import('./modulo-grande.js');
    modulo.inicializar();
  } catch (error) {
    console.error('Error al cargar el módulo:', error);
  }
};

// Uso
document.getElementById('boton').addEventListener('click', cargarModulo);

CommonJS (CJS)

Es el sistema de módulos utilizado por Node.js. No es nativo del navegador.

// math.js
function suma(a, b) {
  return a + b;
}

const PI = 3.14159;

// Exportar
module.exports = {
  suma,
  PI,
  multiplicacion: (a, b) => a * b
};

// También se puede exportar individualmente
module.exports.resta = (a, b) => a - b;

// app.js
// Importar el módulo
const math = require('./math.js');
// O con desestructuración
const { suma, PI } = require('./math.js');

console.log(math.suma(5, 3));  // 8
console.log(suma(5, 3));  // 8

AMD (Asynchronous Module Definition)

Diseñado para navegadores, permite cargar módulos de forma asíncrona.

// Con RequireJS
define('miModulo', ['dependencia1', 'dependencia2'], 
  function(dep1, dep2) {
    return {
      metodo: function() {
        return dep1.metodo() + dep2.metodo();
      }
    };
  }
);

// Uso
require(['miModulo'], function(miModulo) {
  miModulo.metodo();
});

UMD (Universal Module Definition)

Combina CommonJS y AMD para funcionar en múltiples entornos.

(function(root, factory) {
  if (typeof define === 'function' && define.amd) {
    // AMD
    define(['dependencia'], factory);
  } else if (typeof module === 'object' && module.exports) {
    // CommonJS
    module.exports = factory(require('dependencia'));
  } else {
    // Global
    root.miModulo = factory(root.dependencia);
  }
}(typeof self !== 'undefined' ? self : this, function(dependencia) {
  // Tu módulo aquí
  return {
    metodo: function() {
      return 'resultado';
    }
  };
}));

Uso de módulos en el navegador

Incluir módulos ES directamente

<script type="module">
  import { suma } from './math.js';
  console.log(suma(5, 3));
</script>

<!-- O importar desde un archivo externo -->
<script type="module" src="app.js"></script>

Características de los módulos en el navegador

  • Se ejecutan en modo estricto ('use strict') automáticamente
  • Tienen su propio ámbito (scope)
  • Se cargan con CORS
  • Se ejecutan de forma diferida (como defer)
  • Se cargan una sola vez, independientemente de cuántas veces se importen
  • No tienen acceso a this global

Limitaciones y consideraciones

  • Requieren un servidor web (no funcionan con file://)
  • Navegadores antiguos no los soportan
  • Las rutas deben ser completas (./module.js, no module.js)
  • Pueden generar muchas peticiones HTTP si hay muchos módulos pequeños

Empaquetadores (Bundlers)

Los empaquetadores son herramientas que combinan múltiples archivos de código en uno o varios "paquetes" optimizados para producción.

¿Por qué usar empaquetadores?

  • Reducir el número de peticiones HTTP
  • Optimizar y minificar el código
  • Transformar código moderno a versiones compatibles
  • Procesar otros tipos de archivos (CSS, imágenes, etc.)
  • Gestionar dependencias automáticamente
  • Implementar técnicas de división de código (code splitting)

Webpack

Webpack es uno de los empaquetadores más populares y completos.

Instalación básica

# Crear package.json
npm init -y

# Instalar webpack
npm install webpack webpack-cli --save-dev

Configuración básica (webpack.config.js)

const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  mode: 'development' // o 'production'
};

Uso de loaders

Los loaders permiten procesar diferentes tipos de archivos.

module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      },
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        }
      },
      {
        test: /\.(png|svg|jpg|gif)$/,
        use: ['file-loader']
      }
    ]
  }
};

Plugins

Los plugins extienden la funcionalidad de webpack.

const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  // ...
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html'
    }),
    new MiniCssExtractPlugin({
      filename: '[name].[contenthash].css'
    })
  ]
};

División de código (Code Splitting)

module.exports = {
  // ...
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all'
        }
      }
    }
  }
};

Servidor de desarrollo

npm install webpack-dev-server --save-dev
module.exports = {
  // ...
  devServer: {
    contentBase: './dist',
    open: true,
    hot: true
  }
};

Rollup

Rollup se centra en la creación de paquetes más pequeños y eficientes, especialmente para bibliotecas.

Instalación básica

npm install rollup --save-dev

Configuración básica (rollup.config.js)

export default {
  input: 'src/main.js',
  output: {
    file: 'dist/bundle.js',
    format: 'esm' // o 'cjs', 'umd', 'amd', 'iife'
  }
};

Plugins comunes

import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import babel from '@rollup/plugin-babel';
import { terser } from 'rollup-plugin-terser';

export default {
  input: 'src/main.js',
  output: {
    file: 'dist/bundle.js',
    format: 'esm',
    sourcemap: true
  },
  plugins: [
    resolve(), // Resolver módulos de node_modules
    commonjs(), // Convertir CommonJS a ES modules
    babel({ babelHelpers: 'bundled' }), // Transpilar con Babel
    terser() // Minificar
  ]
};

Parcel

Parcel se destaca por su configuración "zero-config" y su velocidad.

Instalación básica

npm install parcel-bundler --save-dev

Uso básico

# Apuntar al archivo de entrada
npx parcel src/index.html

Configuración (si es necesaria)

// .parcelrc
{
  "extends": "@parcel/config-default",
  "transformers": {
    "*.{js,mjs,jsx,cjs,ts,tsx}": ["@parcel/transformer-babel"]
  }
}

Comparativa de empaquetadores

CaracterísticaWebpackRollupParcel
ConfiguraciónCompleja pero flexibleModeradaMínima o nula
VelocidadModeradaAltaMuy alta
EcosistemaMuy amplioModeradoCreciente
Code SplittingAvanzadoBásicoAutomático
Tree ShakingExcelente
Ideal paraAplicaciones complejasBibliotecasProyectos pequeños/medianos
Hot Module ReplacementCon plugins

Patrones de diseño con módulos

Patrón Módulo Revelador

// userModule.js
const userModule = (() => {
  // Variables privadas
  let name = '';
  let email = '';
  
  // Métodos privados
  const validateEmail = (email) => {
    return /\S+@\S+\.\S+/.test(email);
  };
  
  // API pública
  return {
    setUser: (userName, userEmail) => {
      if (!validateEmail(userEmail)) {
        throw new Error('Email inválido');
      }
      name = userName;
      email = userEmail;
    },
    getUser: () => {
      return { name, email };
    }
  };
})();

export default userModule;

Patrón Singleton con módulos

// database.js
let instance = null;

class Database {
  constructor() {
    if (instance) {
      return instance;
    }
    
    this.connections = 0;
    this.data = {};
    instance = this;
  }
  
  connect() {
    this.connections++;
    console.log(`Conexión #${this.connections} establecida`);
  }
  
  save(key, value) {
    this.data[key] = value;
  }
  
  get(key) {
    return this.data[key];
  }
}

export default new Database();

Patrón Fábrica (Factory) con módulos

// componentFactory.js
export const componentTypes = {
  BUTTON: 'button',
  INPUT: 'input',
  CHECKBOX: 'checkbox'
};

export function createComponent(type, config = {}) {
  switch (type) {
    case componentTypes.BUTTON:
      return {
        render: () => `<button class="${config.className || ''}">${config.text || 'Click'}</button>`,
        events: {
          click: config.onClick || (() => {})
        }
      };
      
    case componentTypes.INPUT:
      return {
        render: () => `<input type="text" class="${config.className || ''}" placeholder="${config.placeholder || ''}">`,
        events: {
          input: config.onInput || (() => {})
        }
      };
      
    case componentTypes.CHECKBOX:
      return {
        render: () => `<input type="checkbox" class="${config.className || ''}" ${config.checked ? 'checked' : ''}>`,
        events: {
          change: config.onChange || (() => {})
        }
      };
      
    default:
      throw new Error(`Tipo de componente no soportado: ${type}`);
  }
}

Mejores prácticas

Organización de módulos

src/
├── components/
│   ├── Button.js
│   ├── Form.js
│   └── index.js       # Re-exporta componentes
├── utils/
│   ├── formatters.js
│   ├── validators.js
│   └── index.js       # Re-exporta utilidades
├── services/
│   ├── api.js
│   └── auth.js
└── index.js           # Punto de entrada

Archivo de barril (index.js)

// components/index.js
export { default as Button } from './Button';
export { default as Form } from './Form';

// Uso
import { Button, Form } from './components';

Evitar dependencias circulares

// ❌ Mal: Dependencia circular
// moduleA.js
import { functionB } from './moduleB';
export function functionA() {
  return functionB() + 1;
}

// moduleB.js
import { functionA } from './moduleA';
export function functionB() {
  return functionA() + 1;
}

// ✅ Bien: Extraer funcionalidad común
// common.js
export function baseFunction() {
  return 1;
}

// moduleA.js
import { baseFunction } from './common';
export function functionA() {
  return baseFunction() + 1;
}

// moduleB.js
import { baseFunction } from './common';
export function functionB() {
  return baseFunction() + 2;
}

Módulos pequeños y enfocados

// ❌ Mal: Módulo grande con múltiples responsabilidades
// userUtils.js
export function validateUser(user) { /* ... */ }
export function formatUserName(name) { /* ... */ }
export function calculateUserAge(birthdate) { /* ... */ }
export function getUserPermissions(user) { /* ... */ }
export function renderUserProfile(user) { /* ... */ }

// ✅ Bien: Módulos pequeños y enfocados
// userValidation.js
export function validateUser(user) { /* ... */ }

// userFormatters.js
export function formatUserName(name) { /* ... */ }

// userCalculations.js
export function calculateUserAge(birthdate) { /* ... */ }

// userPermissions.js
export function getUserPermissions(user) { /* ... */ }

// userUI.js
export function renderUserProfile(user) { /* ... */ }

Exportaciones por defecto vs. con nombre

// ❌ Evitar múltiples exportaciones por defecto
// math.js
export default {
  suma: (a, b) => a + b,
  resta: (a, b) => a - b,
  multiplicacion: (a, b) => a * b
};

// ✅ Preferir exportaciones con nombre para múltiples funciones
// math.js
export const suma = (a, b) => a + b;
export const resta = (a, b) => a - b;
export const multiplicacion = (a, b) => a * b;

// ✅ Usar exportación por defecto para componentes/clases principales
// Button.js
export default class Button {
  // ...
}

Ejemplo práctico: Aplicación modular

Estructura de archivos

src/
├── components/
│   ├── TaskForm.js
│   ├── TaskList.js
│   └── TaskItem.js
├── services/
│   └── taskService.js
├── utils/
│   └── dateUtils.js
└── index.js

Implementación

// utils/dateUtils.js
export function formatDate(date) {
  return new Date(date).toLocaleDateString();
}

export function isToday(date) {
  const today = new Date();
  const taskDate = new Date(date);
  return today.toDateString() === taskDate.toDateString();
}

// services/taskService.js
const STORAGE_KEY = 'tasks';

export function getTasks() {
  const tasks = localStorage.getItem(STORAGE_KEY);
  return tasks ? JSON.parse(tasks) : [];
}

export function saveTask(task) {
  const tasks = getTasks();
  const newTask = {
    ...task,
    id: Date.now(),
    createdAt: new Date().toISOString()
  };
  
  tasks.push(newTask);
  localStorage.setItem(STORAGE_KEY, JSON.stringify(tasks));
  return newTask;
}

export function deleteTask(id) {
  const tasks = getTasks();
  const filteredTasks = tasks.filter(task => task.id !== id);
  localStorage.setItem(STORAGE_KEY, JSON.stringify(filteredTasks));
}

export function toggleTaskStatus(id) {
  const tasks = getTasks();
  const updatedTasks = tasks.map(task => 
    task.id === id ? { ...task, completed: !task.completed } : task
  );
  localStorage.setItem(STORAGE_KEY, JSON.stringify(updatedTasks));
}

// components/TaskItem.js
import { formatDate, isToday } from '../utils/dateUtils';
import { deleteTask, toggleTaskStatus } from '../services/taskService';

export default class TaskItem {
  constructor(task, onUpdate) {
    this.task = task;
    this.onUpdate = onUpdate;
  }
  
  handleDelete = () => {
    deleteTask(this.task.id);
    this.onUpdate();
  }
  
  handleToggle = () => {
    toggleTaskStatus(this.task.id);
    this.onUpdate();
  }
  
  render() {
    const { id, title, completed, createdAt } = this.task;
    const dateText = isToday(createdAt) ? 'Hoy' : formatDate(createdAt);
    
    const element = document.createElement('li');
    element.className = `task-item ${completed ? 'completed' : ''}`;
    element.innerHTML = `
      <input type="checkbox" id="task-${id}" ${completed ? 'checked' : ''}>
      <label for="task-${id}">${title}</label>
      <span class="date">${dateText}</span>
      <button class="delete-btn">Eliminar</button>
    `;
    
    element.querySelector('input').addEventListener('change', this.handleToggle);
    element.querySelector('.delete-btn').addEventListener('click', this.handleDelete);
    
    return element;
  }
}

// components/TaskList.js
import TaskItem from './TaskItem';
import { getTasks } from '../services/taskService';

export default class TaskList {
  constructor(containerId) {
    this.container = document.getElementById(containerId);
  }
  
  render() {
    const tasks = getTasks();
    this.container.innerHTML = '';
    
    if (tasks.length === 0) {
      const emptyMessage = document.createElement('p');
      emptyMessage.textContent = 'No hay tareas pendientes.';
      this.container.appendChild(emptyMessage);
      return;
    }
    
    const list = document.createElement('ul');
    list.className = 'task-list';
    
    tasks.forEach(task => {
      const taskItem = new TaskItem(task, () => this.render());
      list.appendChild(taskItem.render());
    });
    
    this.container.appendChild(list);
  }
}

// components/TaskForm.js
import { saveTask } from '../services/taskService';

export default class TaskForm {
  constructor(formId, onTaskAdded) {
    this.form = document.getElementById(formId);
    this.onTaskAdded = onTaskAdded;
    this.bindEvents();
  }
  
  bindEvents() {
    this.form.addEventListener('submit', this.handleSubmit);
  }
  
  handleSubmit = (event) => {
    event.preventDefault();
    const titleInput = this.form.querySelector('input[name="title"]');
    const title = titleInput.value.trim();
    
    if (!title) return;
    
    const task = saveTask({ title, completed: false });
    titleInput.value = '';
    this.onTaskAdded(task);
  }
}

// index.js
import TaskForm from './components/TaskForm';
import TaskList from './components/TaskList';

document.addEventListener('DOMContentLoaded', () => {
  const taskList = new TaskList('tasks-container');
  
  const taskForm = new TaskForm('task-form', () => {
    taskList.render();
  });
  
  // Renderizar la lista inicial
  taskList.render();
});

HTML correspondiente

<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Gestor de Tareas</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <div class="container">
    <h1>Gestor de Tareas</h1>
    
    <form id="task-form">
      <input type="text" name="title" placeholder="Nueva tarea..." required>
      <button type="submit">Agregar</button>
    </form>
    
    <div id="tasks-container"></div>
  </div>
  
  <script type="module" src="dist/bundle.js"></script>
</body>
</html>
Módulos y Bundlers
Aprende a organizar y optimizar tu código JavaScript utiliza...
Manipulación del DOM Avanzada
Técnicas sofisticadas para interactuar con el DOM.
Referencias
Webpack. Webpack Documentation. https://webpack.js.org/concepts/
Rollup. Rollup.js Guide. https://rollupjs.org/guide/en/
Parcel. Parcel Documentation. https://parceljs.org/docs/

Conceptos Básicos de HTML

Aprende los conceptos básicos de HTML

Conceptos Básicos de CSS

Aprende los conceptos básicos de CSS

Conceptos Básicos SQL

Aprende los conceptos básicos de SQL

Conceptos Básicos de GIT

Aprende los conceptos básicos de GIT

Conceptos Básicos de Python

Aprende los conceptos básicos de Python

Conceptos Básicos de UML

Aprende los conceptos básicos de UML

Refuerzo Academico de Herramientas de Productividad 2025