HDP115

Módulos y Bundlers

Aprende a organizar y optimizar tu código JavaScript utilizando sistemas de módulos modernos y herramientas de empaquetado (bundlers) para aplicaciones web escalables.

CE

Cristian Escalante

Última actualización: 22 de abril de 2025

javascript
programación web
desarrollo frontend

Introducción a los módulos en JavaScript

Los módulos son una forma de organizar el código en archivos separados, permitiendo una mejor estructura, reutilización y mantenimiento del código.

¿Por qué necesitamos módulos?

En los primeros días de JavaScript, todo el código se escribía en un solo archivo o se dividía en varios archivos que compartían el mismo ámbito global. Esto generaba varios problemas:

  1. Colisiones de nombres: Variables y funciones con el mismo nombre podían sobrescribirse.
  2. Dependencias implícitas: Era difícil saber qué archivos dependían de otros.
  3. Mantenimiento complicado: Los proyectos grandes se volvían difíciles de mantener.
  4. Dificultad para reutilizar código: No existía una forma estándar de compartir código entre proyectos.

Beneficios de los módulos

  • Encapsulamiento: Las variables y funciones están limitadas al ámbito del módulo.
  • Reutilización: El código puede reutilizarse fácilmente en diferentes partes de la aplicación.
  • Dependencias explícitas: Cada módulo declara claramente de qué otros módulos depende.
  • Organización: El código se organiza de forma lógica y estructurada.
  • Mantenimiento: Es más fácil mantener, probar y depurar módulos individuales.

Evolución de los sistemas de módulos

IIFE (Immediately Invoked Function Expressions)

Antes de los sistemas de módulos formales, se usaban IIFE para simular módulos:

// Módulo usando IIFE
var miModulo = (function() {
  // Variables privadas
  var contador = 0;
  
  // Funciones privadas
  function incrementar() {
    contador++;
  }
  
  // API pública
  return {
    incrementar: incrementar,
    obtenerContador: function() {
      return contador;
    }
  };
})();

// Uso
miModulo.incrementar();
console.log(miModulo.obtenerContador()); // 1

CommonJS

Popularizado por Node.js, utiliza require() y module.exports:

// matematica.js
function sumar(a, b) {
  return a + b;
}

function restar(a, b) {
  return a - b;
}

module.exports = {
  sumar,
  restar
};

// app.js
const matematica = require('./matematica');
console.log(matematica.sumar(5, 3)); // 8

AMD (Asynchronous Module Definition)

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

// Definir un módulo
define('miModulo', ['dependencia1', 'dependencia2'], 
  function(dependencia1, dependencia2) {
    return {
      metodo: function() {
        return dependencia1.algo() + dependencia2.otro();
      }
    };
  }
);

// Usar un módulo
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) {
  return {
    // Implementación del módulo
  };
}));

ES Modules (ESM)

El sistema de módulos nativo de JavaScript, introducido en ES6 (ES2015):

// matematica.js
export function sumar(a, b) {
  return a + b;
}

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

// app.js
import { sumar, restar } from './matematica.js';
console.log(sumar(5, 3)); // 8

ES Modules en detalle

Sintaxis básica de exportación

// Exportaciones con nombre
export const PI = 3.14159;
export function calcularArea(radio) {
  return PI * radio * radio;
}

// Exportación por defecto (solo una por archivo)
export default class Circulo {
  constructor(radio) {
    this.radio = radio;
  }
  
  obtenerArea() {
    return PI * this.radio * this.radio;
  }
}

// Exportaciones agrupadas
const PI = 3.14159;
function calcularArea(radio) {
  return PI * radio * radio;
}

export { PI, calcularArea };

Sintaxis básica de importación

// Importar exportaciones con nombre
import { PI, calcularArea } from './matematica.js';

// Importar con alias
import { PI as CONSTANTE_PI, calcularArea } from './matematica.js';

// Importar todo como un objeto
import * as Matematica from './matematica.js';
console.log(Matematica.PI); // 3.14159

// Importar exportación por defecto
import Circulo from './circulo.js';
const miCirculo = new Circulo(5);

// Importar exportación por defecto y con nombre
import Circulo, { PI } from './circulo.js';

Re-exportación

// Re-exportar todo de otro módulo
export * from './matematica.js';

// Re-exportar elementos específicos
export { sumar, restar } from './matematica.js';

// Re-exportar con alias
export { sumar as suma, restar as resta } from './matematica.js';

// Re-exportar exportación por defecto
export { default } from './matematica.js';
export { default as Matematica } from './matematica.js';

Importaciones dinámicas

Las importaciones dinámicas permiten cargar módulos bajo demanda:

// Importación estática (se carga al inicio)
import { funcionA } from './moduloA.js';

// Importación dinámica (se carga cuando se necesita)
async function cargarModuloCuandoSeaNecesario() {
  try {
    // Devuelve una promesa
    const modulo = await import('./moduloGrande.js');
    modulo.funcionEspecial();
  } catch (error) {
    console.error('Error al cargar el módulo:', error);
  }
}

// Con then/catch
document.getElementById('boton').addEventListener('click', () => {
  import('./moduloGrande.js')
    .then(modulo => {
      modulo.funcionEspecial();
    })
    .catch(error => {
      console.error('Error al cargar el módulo:', error);
    });
});

Módulos en el navegador

Para usar ES Modules directamente en el navegador:

<!-- Indicar que es un módulo con type="module" -->
<script type="module">
  import { sumar } from './matematica.js';
  console.log(sumar(2, 3));
</script>

<!-- También funciona con archivos externos -->
<script type="module" src="app.js"></script>

Características importantes de los módulos en el navegador:

  1. Ámbito estricto: Los módulos siempre se ejecutan en modo estricto.
  2. Diferido por defecto: Los módulos se cargan como si tuvieran el atributo defer.
  3. CORS: Las solicitudes de módulos respetan las políticas de CORS.
  4. Cargados una vez: Cada módulo se evalúa solo una vez, sin importar cuántas veces se importe.
  5. Sin acceso global: Las variables definidas en módulos no contaminan el ámbito global.

Bundlers (Empaquetadores)

Los bundlers son herramientas que toman múltiples archivos JavaScript y sus dependencias, y los combinan en uno o varios archivos optimizados para producción.

¿Por qué usar bundlers?

  1. Rendimiento: Reducen el número de solicitudes HTTP combinando archivos.
  2. Compatibilidad: Transforman código moderno en código compatible con navegadores antiguos.
  3. Optimización: Minimizan y optimizan el código para producción.
  4. Gestión de dependencias: Resuelven y gestionan dependencias automáticamente.
  5. Procesamiento de recursos: Permiten importar CSS, imágenes y otros recursos no JavaScript.

Principales bundlers

Webpack

El bundler más popular y completo:

// webpack.config.js básico
const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
        },
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
      },
    ],
  },
};

Rollup

Especializado en bibliotecas, con excelente tree-shaking:

// rollup.config.js
import babel from '@rollup/plugin-babel';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';

export default {
  input: 'src/main.js',
  output: {
    file: 'dist/bundle.js',
    format: 'esm', // o 'cjs', 'umd', 'iife'
  },
  plugins: [
    resolve(),
    commonjs(),
    babel({
      babelHelpers: 'bundled',
      exclude: 'node_modules/**',
    }),
  ],
};

Parcel

Configuración cero, muy fácil de usar:

# Instalación
npm install -g parcel-bundler

# Uso (sin archivo de configuración)
parcel index.html

esbuild

Extremadamente rápido, escrito en Go:

// esbuild.config.js
require('esbuild').build({
  entryPoints: ['src/index.js'],
  bundle: true,
  minify: true,
  sourcemap: true,
  target: ['chrome58', 'firefox57', 'safari11', 'edge16'],
  outfile: 'dist/bundle.js',
}).catch(() => process.exit(1));

Vite

Moderno, rápido, optimizado para desarrollo:

// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  build: {
    outDir: 'dist',
  },
});

Webpack en detalle

Webpack es el bundler más utilizado en proyectos JavaScript modernos. Veamos cómo configurarlo y utilizarlo.

Instalación básica

# Crear package.json
npm init -y

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

# Opcional: instalar servidor de desarrollo
npm install --save-dev webpack-dev-server

Conceptos clave de Webpack

  1. Entry: Punto(s) de entrada de la aplicación
  2. Output: Dónde y cómo se generarán los archivos
  3. Loaders: Transforman archivos que no son JavaScript
  4. Plugins: Realizan tareas más complejas
  5. Mode: Modo de construcción (development, production, none)

Configuración completa

// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = (env, argv) => {
  const isProduction = argv.mode === 'production';
  
  return {
    // Punto de entrada
    entry: './src/index.js',
    
    // Salida
    output: {
      path: path.resolve(__dirname, 'dist'),
      filename: isProduction ? 'js/[name].[contenthash].js' : 'js/[name].js',
      publicPath: '/',
    },
    
    // Modo
    mode: isProduction ? 'production' : 'development',
    
    // Mapas de origen
    devtool: isProduction ? 'source-map' : 'eval-source-map',
    
    // Servidor de desarrollo
    devServer: {
      contentBase: path.join(__dirname, 'dist'),
      compress: true,
      port: 9000,
      hot: true,
      historyApiFallback: true,
    },
    
    // Reglas para diferentes tipos de archivos
    module: {
      rules: [
        // JavaScript con Babel
        {
          test: /\.js$/,
          exclude: /node_modules/,
          use: {
            loader: 'babel-loader',
            options: {
              presets: ['@babel/preset-env'],
            },
          },
        },
        
        // CSS y SASS
        {
          test: /\.(css|scss)$/,
          use: [
            isProduction ? MiniCssExtractPlugin.loader : 'style-loader',
            'css-loader',
            'postcss-loader',
            'sass-loader',
          ],
        },
        
        // Imágenes
        {
          test: /\.(png|svg|jpg|jpeg|gif)$/i,
          type: 'asset/resource',
          generator: {
            filename: 'images/[hash][ext][query]',
          },
        },
        
        // Fuentes
        {
          test: /\.(woff|woff2|eot|ttf|otf)$/i,
          type: 'asset/resource',
          generator: {
            filename: 'fonts/[hash][ext][query]',
          },
        },
      ],
    },
    
    // Plugins
    plugins: [
      // Limpia la carpeta dist antes de construir
      new CleanWebpackPlugin(),
      
      // Genera el HTML
      new HtmlWebpackPlugin({
        template: './src/index.html',
        minify: isProduction,
      }),
      
      // Extrae CSS a archivos separados en producción
      ...(isProduction ? [
        new MiniCssExtractPlugin({
          filename: 'css/[name].[contenthash].css',
        }),
      ] : []),
    ],
    
    // Optimizaciones
    optimization: {
      // Divide el código
      splitChunks: {
        chunks: 'all',
        name: false,
      },
    },
    
    // Resolución de módulos
    resolve: {
      extensions: ['.js', '.json'],
      alias: {
        '@': path.resolve(__dirname, 'src'),
      },
    },
  };
};

Scripts en package.json

{
  "scripts": {
    "start": "webpack serve --mode development",
    "build": "webpack --mode production",
    "analyze": "webpack --mode production --analyze"
  }
}

Técnicas avanzadas de optimización

Code Splitting

Divide el código en múltiples archivos para cargar solo lo necesario:

// Webpack config para code splitting
module.exports = {
  // ...
  optimization: {
    splitChunks: {
      chunks: 'all',
      maxInitialRequests: Infinity,
      minSize: 0,
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name(module) {
            // Obtén el nombre del paquete
            const packageName = module.context.match(
              /[\\/]node_modules[\\/](.*?)([\\/]|$)/
            )[1];
            // npm package names are URL-safe, but some servers don't like @ symbols
            return `npm.${packageName.replace('@', '')}`;
          },
        },
      },
    },
  },
};

// En el código: importaciones dinámicas
const ComponenteLazy = React.lazy(() => import('./ComponentePesado'));

function App() {
  return (
    <React.Suspense fallback={<div>Cargando...</div>}>
      <ComponenteLazy />
    </React.Suspense>
  );
}

Tree Shaking

Elimina código no utilizado del bundle final:

// modulo.js - solo se incluirá funcionUsada en el bundle final
export function funcionUsada() {
  return 'Esta función se usa';
}

export function funcionNoUsada() {
  return 'Esta función no se incluirá en el bundle';
}

// app.js
import { funcionUsada } from './modulo.js';
funcionUsada();

Para que el tree shaking funcione correctamente:

  1. Usar ES Modules (import/export)
  2. Evitar efectos secundarios en los módulos
  3. Configurar correctamente el bundler
  4. En webpack, usar mode: 'production' o configurar optimization.usedExports: true

Lazy Loading

Cargar módulos solo cuando se necesitan:

// Rutas con lazy loading en React
import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';

const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));
const Contact = lazy(() => import('./routes/Contact'));

function App() {
  return (
    <Router>
      <Suspense fallback={<div>Cargando...</div>}>
        <Switch>
          <Route exact path="/" component={Home} />
          <Route path="/about" component={About} />
          <Route path="/contact" component={Contact} />
        </Switch>
      </Suspense>
    </Router>
  );
}

Preloading y Prefetching

Técnicas para mejorar el rendimiento percibido:

// Prefetch: carga en segundo plano cuando el navegador está inactivo
import(/* webpackPrefetch: true */ './modulo-futuro.js');

// Preload: carga con mayor prioridad
import(/* webpackPreload: true */ './modulo-necesario-pronto.js');

Mejores prácticas

  1. Estructura de proyecto clara:
    /proyecto
      /src
        /components
        /utils
        /styles
        index.js
      /dist
      package.json
      webpack.config.js
    
  2. Separar código por responsabilidades:
    // api.js - Maneja llamadas a API
    export async function fetchUsuarios() {
      // ...
    }
    
    // utils.js - Funciones de utilidad
    export function formatearFecha(fecha) {
      // ...
    }
    
    // componente.js - Lógica de UI
    import { fetchUsuarios } from './api.js';
    import { formatearFecha } from './utils.js';
    
  3. Usar alias para importaciones:
    // webpack.config.js
    module.exports = {
      // ...
      resolve: {
        alias: {
          '@': path.resolve(__dirname, 'src'),
          '@components': path.resolve(__dirname, 'src/components'),
          '@utils': path.resolve(__dirname, 'src/utils'),
        }
      }
    }
    
    // En el código
    import Button from '@components/Button';
    import { formatDate } from '@utils/dates';
    
  4. Analizar el tamaño del bundle:
    # Instalar
    npm install --save-dev webpack-bundle-analyzer
    
    # En webpack.config.js
    const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
    
    module.exports = {
      // ...
      plugins: [
        // ...
        new BundleAnalyzerPlugin()
      ]
    }
    
  5. Dividir configuración por entorno:
    /config
      webpack.common.js
      webpack.dev.js
      webpack.prod.js
    
    // webpack.common.js
    const path = require('path');
    
    module.exports = {
      entry: './src/index.js',
      module: {
        rules: [
          // Reglas comunes
        ]
      }
    };
    
    // webpack.dev.js
    const { merge } = require('webpack-merge');
    const common = require('./webpack.common.js');
    
    module.exports = merge(common, {
      mode: 'development',
      devtool: 'inline-source-map',
      devServer: {
        // Configuración del servidor de desarrollo
      }
    });
    
    // webpack.prod.js
    const { merge } = require('webpack-merge');
    const common = require('./webpack.common.js');
    const MiniCssExtractPlugin = require('mini-css-extract-plugin');
    
    module.exports = merge(common, {
      mode: 'production',
      devtool: 'source-map',
      plugins: [
        new MiniCssExtractPlugin()
      ]
    });
    

Ejemplos prácticos

Proyecto básico con Webpack

# Estructura de directorios
mkdir mi-proyecto && cd mi-proyecto
npm init -y
mkdir src dist

# Instalar dependencias
npm install --save-dev webpack webpack-cli webpack-dev-server
npm install --save-dev babel-loader @babel/core @babel/preset-env
npm install --save-dev css-loader style-loader
npm install --save-dev html-webpack-plugin
// src/utils.js
export function saludar(nombre) {
  return `¡Hola, ${nombre}!`;
}

// src/index.js
import { saludar } from './utils.js';
import './styles.css';

document.getElementById('app').innerHTML = saludar('Mundo');

// src/styles.css
body {
  font-family: Arial, sans-serif;
  background-color: #f0f0f0;
}

#app {
  padding: 20px;
  background-color: white;
  border-radius: 5px;
  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
  margin: 20px auto;
  max-width: 600px;
}

// src/index.html
<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Mi Proyecto</title>
</head>
<body>
  <div id="app"></div>
</body>
</html>

// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js',
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        }
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html'
    })
  ],
  devServer: {
    static: {
      directory: path.join(__dirname, 'dist'),
    },
    port: 8080,
    open: true
  }
};

// package.json (scripts)
{
  "scripts": {
    "start": "webpack serve --mode development",
    "build": "webpack --mode production"
  }
}

Proyecto con múltiples entradas

// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: {
    main: './src/index.js',
    admin: './src/admin.js'
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash].js',
  },
  // ...
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html',
      chunks: ['main'],
      filename: 'index.html'
    }),
    new HtmlWebpackPlugin({
      template: './src/admin.html',
      chunks: ['admin'],
      filename: 'admin.html'
    })
  ],
  // ...
};

Herramientas complementarias

Babel

Transpila código JavaScript moderno a versiones compatibles con navegadores antiguos:

npm install --save-dev babel-loader @babel/core @babel/preset-env
// .babelrc
{
  "presets": [
    ["@babel/preset-env", {
      "targets": "> 0.25%, not dead",
      "useBuiltIns": "usage",
      "corejs": 3
    }]
  ]
}

ESLint

Analiza el código para encontrar problemas:

npm install --save-dev eslint eslint-webpack-plugin
// .eslintrc.js
module.exports = {
  env: {
    browser: true,
    es2021: true,
    node: true
  },
  extends: 'eslint:recommended',
  parserOptions: {
    ecmaVersion: 12,
    sourceType: 'module'
  },
  rules: {
    // Reglas personalizadas
  }
};

// webpack.config.js
const ESLintPlugin = require('eslint-webpack-plugin');

module.exports = {
  // ...
  plugins: [
    // ...
    new ESLintPlugin()
  ]
};

PostCSS

Procesa CSS con plugins como Autoprefixer:

npm install --save-dev postcss postcss-loader autoprefixer
// postcss.config.js
module.exports = {
  plugins: [
    require('autoprefixer')
  ]
};

// webpack.config.js
module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader',
          'postcss-loader'
        ]
      }
    ]
  }
};
Programación Orientada a Objetos
Aprende a organizar tu código utilizando el paradigma orient...
Módulos y Empaquetadores
Aprende a organizar tu código JavaScript en módulos reutiliz...
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