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.
Cristian Escalante
Última actualización: 23 de abril de 2025
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ística | Webpack | Rollup | Parcel |
---|---|---|---|
Configuración | Compleja pero flexible | Moderada | Mínima o nula |
Velocidad | Moderada | Alta | Muy alta |
Ecosistema | Muy amplio | Moderado | Creciente |
Code Splitting | Avanzado | Básico | Automático |
Tree Shaking | Sí | Excelente | Sí |
Ideal para | Aplicaciones complejas | Bibliotecas | Proyectos pequeños/medianos |
Hot Module Replacement | Sí | Con plugins | Sí |
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>