Skip to content
Aprende a hacer apps móviles con Ionic 2 

Introducción a Webpack

Sabes que soy un gran fan de Angular 2 y ES6. Son herramientas potentes y modernas, pero no tienen ninguna utilidad sin una herramienta como Babel para transpilar el código a javascript tradicional. Y por eso hoy te voy a introducir a Webpack, el module bundler de frontend por excelencia.

¿Que vas a encontrar aquí?

En este artículo te voy a explicar:

  • Los conceptos básicos de Webpack
  • Cómo instalar Webpack
  • Como usar Webpack a nivel básico
  • Como incluir recursos SaSS con Webpack (loaders)
  • Como transpilar código ES6 con Webpack (loaders)
  • Como minificar código con Webpack (plugins)
  • Ejemplo práctico donde vas a hacer paso a paso todos los puntos anteriores.

¿Qué es Webpack?

Webpack es un empaquetador de módulos, es decir, te permite generar un achivo único con todos aquellos módulos que necesita tu aplicación para funcionar. Para hacerte una idea, te permite meter todos tus archivos javascript en un único archivo, llamémoslo bundle.js.

Como definición inicial basta, pero Webpack va mas allá y se ha convertido en una herramienta de build muy versátil. Entre otras cosas, destaca que:

  • Puedes generar solo aquellos fragmentos de JS que realmente necesita cada página (haciendo más rápida su carga).

  • Tiene varios loaders para importar y empaquetar también otros recursos (CSS, templates, …) así como otros lenguajes (ES6 con Babel, TypeScript, SaSS, etc).

  • Sus plugins te permiten hacer otras tareas importantes como por ejemplo minificar y ofuscar el código.

Nociones básicas de Webpack

Instalando Webpack

Pongamos que tengo un proyecto llamado webpackTest en el que quiero instalar Webpack. Haré lo siguiente (parto de la base de que conoces npm).

$:  npm install webpack -g
$:  npm install webpack webpack-dev-server -D

De este modo instalo webpack a nivel global, e instalo tanto la CLI de webpack como su servidor de desarrollo añadiéndolos como dependencias de desarrollo del proyecto.

Archivo de configuración

Webpack se basa en un archivo de configuración, denominado webpack.config.js, donde se define todo el proceso de build con archivos de entrada y salida, loaders, plugins, etc.

Punto de entrada y salida

La parte básica de la configuración de webpack es definir:

  • entry: un archivo de entrada a partir del cual Webpack buscará todas las dependencias requeridas (ya sea con CommonJS, AMD o incluso ES6 si usas el loader de Babel)
  • output: Cómo se llamará el paquete de salida con todos esos datos y donde estará ubicado

Sería algo así:

//  webpack.config.js 
module.exports = {
    entry: './index.js',
    output: {
        filename: 'bundle.js',
        path: __dirname
    }
};

Donde __dirname hace referencia al directorio donde se está ejecutando el script.

Esta configuración de webpack lo que hará es copiar index.js y todas las dependecias que encuentre (imagina que estás importando código de otros archivos JS) y meterlo todo en “bundle.js”, en el orden correcto.

Generando el bundle

Ahora ya tienes un archivo de configuración básico de webpack y puedes cargarte todos los imports de JS de tu index.html y reemplazarlo por un único import a tu bundle.js, pero… ¿Cómo creas ese archivo?

Tienes 2 opciones:

Para producción

Esto normalmente se hace cuando quieres generar el bundle.js para subirlo a producción.
Solo tienes que irte a terminal y escribir:

$:  webpack

Esto generará el archivo bundle.js en el path que le has indicado.

Para desarrollo

Lo anterior te sirve igual para desarrollo, claro, e incluso puedes pasar el parámetro --watch para que Webpack actualice el bundle cada vez que haces cambios de código, es decir:

$:  webpack --watch

Pero lo normal trabajando en entorno de desarrollo es utilizar el servidor de desarrollo de webpack, que te has instalado antes (webpack-dev-server).

Para eso, solo tienes que ejecutar por terminal:

$:  node node_modules/.bin/webpack-dev-server

Esto genera un bundle.js en memoria y lo sirve a la app desde el servidor de desarrollo que utiliza para que veas los resultados en el navegador. Además también escucha tus cambios de código y actualiza el bundle en consecuencia.

El bundle.js que genera webpack-dev-server no se guarda en ningún archivo, sino que se carga en memoria.

Source maps

Si generas un bundle, ejecutas la app y te vas a inspeccionar el código en Chrome, verás que solo hay un archivo javascript con todo lo que has importado ahí bien mezcladito. No te digo que esté mal, pero seguramente para debugar te gustaría poder ver tus archivos de JS por separado tal y como los tienes en realidad.

Para eso sirven los Source Maps, y la buena noticia es que WebPack tiene una opción muy simple para generarlos. La propiedad de configuración devtool, que básicamente genera una carpeta llamada “webpack” con la misma estructura de archivos que tienes en tu proyecto (solo aparecen aquellos que se han importado).

La propiedad devtool acepta varias opciones. Las principales serían:

Para desarrollo

  • eval: es rápido pero tus archivos incluyen también parte de código generado por webpack.
  • eval-source-map: no es tan rápido, pero los archivos son idénticos a los del proyecto.

Para producción

  • source-map: Crea literalmente un archivo de tipo Source map (bundle.js.map) que permite relacionar el código del bundle con los fuentes originales. Tarda bastante más.
  • cheap-module-source-map: Crea un Source map simplificado tardando algo menos de tiempo que la opción anterior.

Para aclarar su uso, te muestro como poner la propiedad devtool a eval-source-map:

//  webpack.config.js 
module.exports = {
    entry: //... ,
    output: //... ,

    devtool: 'eval-source-map',

    //...more config stuff...
};

Loaders (CSS, Babel, etc)

Además de cargar y empaquetar código javascript clásico (ES5), webpack también permite importar CSS, ES6 y una gran variedad de tipologías de archivos. Para eso añade la propiedad module con el array de loaders que necesites al archivo de configuración, como en el siguiente ejemplo:

//  webpack.config.js 
module.exports = {
    entry: //...,
    output: //...

    module: {
        loaders: [
            {test:/\.js$/, loader:'babel', exclude: /node_modules/, query: { presets: ['es2015'] } },,
            {test:/\.css$/, loader:'style!css', exclude: /node_modules/}
        ]
    }
};

¿Cual es la estructura del loader?

  • test: aquí va una expresión regular para detectar los archivos a cargar con este loader

  • loader: es el nombre del loader a aplicar. Además de incluirlo aquí tendrás que instalarlo a través de consola (junto con sus dependencias). En este enlace tienes una lista de los loaders disponibles.
    Para el caso de babel con ES6:

npm install babel-loader babel-core babel-preset-es2015 -D
  • exclude: Puedes indicar directorios en los que no quieres que se busquen dependencias. El directorio node_modules, donde npm instala todos los paquetes, es un buen candidato a excluir.

  • query: Estructura de parámetros opcionales que le puedes pasar al loader.

Se pueden encadenar varios loaders con el símbolo de exclamación ! encadenándose de derecha a izquierda.

En el ejemplo, se aplica primero el loader css para cargar las hojas de estilo con extensión .css, y luego se aplica el loader style para incluir esas hojas de estilo en el código.

Plugins

Además de los loaders para cargar archivos, Webpack también dispone de plugins para realizar otras tareas relacionadas con el proceso de build. En este enlace tienes un listado de los plugins disponibles.

El caso más habitual en desarrollo web es cuando quieres subir tu web a producción y quieres minificar y ofuscar tu código para que ocupe menos y sea más difícil de entender frente a ojos curiosos.

Eso se hace justamente pasando el plugin UglifyJsPlugin al array plugins de la configuración, pero para acceder al plugin primero debes importar la librería webpack.

Sería algo así:

//  webpack.config.js 

var webpack = require('webpack');

module.exports = {
    entry: //...,
    output: //...

    plugins: [
        new webpack.optimize.UglifyJsPlugin()
    ],
};

De este modo, cuando se genere tu bundle.js, éste ocupará menos espacio y será más difícil de entender, manteniendo el código referenciado por tus Source maps intacto.

Aprende Webpack con un ejemplo

Ahora que ya has visto las nociones básicas de Webpack es hora de ponerlas en práctica.

Aquí te explico como hacer todo lo que has visto hasta el momento, paso a paso, partiendo de un proyecto básico sin webpack.

Puedes acceder a todo el código fuente aquí:

Proyecto inicial (sin webpack)

Vamos con un ejemplo muy simple. Tengo una web con la siguiente estructura, de momento sin usar webpack:

|miProyecto
   |
   |app/
      |index.js
      |logger.js
   |style.css
   |index.html

El archivo index.html muestra un div con un párrafo vacío que el archivo index.jsva a modificar y lo indicará por consola, gracias al método Logger de logger.js.

Veamos la vista:

index.html

<!-- index.html -->
<!DOCTYPE html>

  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- import javascript and css -->
    <script src="app/logger.js"></script>
    <script src="app/index.js"></script>
    <link rel="stylesheet" type="text/css" href="styles.css">
  </head>


  <body>
    <div>
      <p id="greeting"></p>
    </div>
  </body>

</html>

Si te fijas, aquí index.html tiene que importar 2 archivos Javascript y 1 de CSS. Y estamos con un ejemplo la mar de sencillo. Imagina si tienes un proyecto de verdad… No sufras, webpack viene al rescate.

Pero antes, veamos también el código para ponerte en situación:

logger.js

//  app/logger.js
//defining global function with improved console.log
var Logger = function(message){
    console.log(new Date(Date.now()), " - ", message);
}

index.js

//  app/index.js
//update content of greeting element when DOM has been loaded
(function(){
    document.addEventListener('DOMContentLoaded', function(){
        document.getElementById('greeting').innerHTML = "Hello my friend!";
        Logger("Greeting has been updated");
    })
})();

Y algo de estilo que le doy para parecer más cool:

styles.css

html, body{
    height: 100%;
}
body{
    margin: 0;
}

div {
    display: flex;
    height: 100%;
    background-color: #2eb398;
    color: white;
    font-size: 22px;
}

div p {
    flex: 1;
    text-align: center;
    margin-top: auto;
    margin-bottom: auto;
}

Como ves un proyecto así solo necesita un servidor para poder ejecutarse. Instalo SimpleHttpServer desde terminal con el comando:

$:  npm install simplehttpserver -g

Y ejecuto el servidor desde la carpeta del proyecto:

$:  cd webpackTest
$:  simplehttpserver ./

Si voy a localhost:8000 y muestro el inspector de chrome para ver la consola, tengo el siguiente resultado:

introducción a webpack

Cargando el proyecto desde webpack

Sé que cuando empiece a echarle horas al proyecto va a crecer mucho y mi index.html será insufrible, así que me decido a empaquetarlo con webpack.

Instalación

Lo primero es instalarlo. Te recuerdo:

$:  cd miProyecto
$:  npm install webpack -g #(si no lo tengo ya instalado en global)
$:  npm install webpack webpack-dev-server -D

ahora, creo en el directorio principal el archivo webpack.config.js con el siguiente contenido:

webpack.config.js

//  webpack.config.js 
module.exports = {
    entry: './app/index.js',
    output: {
        filename: 'bundle.js',
        path: __dirname
    }
};

Importando código, sintaxis CommonJS

Si ejecuto webpack por consola, puedo ver como me crea ese nuevo archivo bundle.js, con algo de código de webpack y el contenido de mi index.js todo dentro de una función. En cambio, falta el contenido de logger.js y eso es por que en index.js no he indicado esta dependencia.

Esto en ES6 se soluciona con los imports, pero recuerda que de momento estoy usando ES5.

Modifico index.js para quitar la función en la que englobaba mi código (ya lo hace webpack por mi). También aprovecho para importar “Logger” mediante sintaxis CommonJS:

index.js

//  app/index.js

//import Logger
var Logger = require('./logger.js');

//update content of greeting element when DOM has been loaded
document.addEventListener('DOMContentLoaded', function(){
    document.getElementById('greeting').innerHTML = "Hello my friend!";
    Logger("Greeting has been updated");
})

Tengo que modificar ligeramente logger.js para exportar correctamente el código como módulo CommonJS (con module.exports):

logger.js

//  app/loger.js
module.exports = function(message){
    console.log(new Date(Date.now()), " - ", message);
}

Ahora sí, mis dependencias de javascript están correctamente definidas.

Generando el paquete bundle.js

Ya lo he adelantado antes. Para generar el bundle.js solo tienes que ir a terminal y ejecutar:

$:  webpack

Esto creará el output que he definido antes (en este caso bundle.js) con el input (index.js) y todas sus dependencias.

Cargando el bundle.js

Modifico index.html para que únicamente se incluya el JS de bundle.js. Fíjate en la sección <head>:

<!DOCTYPE html>

  <head>
    <!-- ... some meta tags -->
    <!-- import javascript and css -->
    <script src="bundle.js"></script>
    <link rel="stylesheet" type="text/css" href="styles.css">
  </head>

    <!-- ...body content... -->
</html>

Ejecutando

De momento, como he generado el archivo bundle.js, puedo usar el servidor anterior para ejecutar la aplicación. Verás como lanzando por terminal

$:  simplehttpserver

Se carga la aplicación sin ningún problema.

Ejecutando con webpack – scripts npm

Cuando trabajas con webpack, es interesante que cada vez que modificas tus archivos se ejecute webpack para actualizar el contenido del servidor de desarrollo. Lo podrías conseguir con webpack --watch y un servidor como simplehttpserver, pero en realidad te he hecho instalar una herramienta que ya se encarga de todo esto: webpack-dev-server.

Al principio del artículo he explicado el comando a ejecutar, pero para hacerlo más simple, voy a asociar la instrucción “npm start” al comando necesario para lanzar el servidor de desarrollo de webpack.

Para eso, edito el archivo package.json del siguiente modo:

{
  //...some stuff
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node node_modules/.bin/webpack-dev-server",
  },
  //...some stuff  
}

Ahora si ejecutas por terminal:

$:  npm start

Verás como te indica que ha creado un bundle válido a partir de app/index.js y app/logger.js, y que está disponible en localhost:8080/webpack_dev_server.

webpack-dev-server

Todo funciona correctamente, pero además, la vista muestra un indicador conforme la app está funcionando correctamente, como se muestra en la siguiente imagen.

webpack-dev-server app running

Importando SaSS con loaders

Lo siguiente que puedo hacer es importar también la hoja de estilos con Webpack. De hecho, puedo trabajar con SaSS, que es más molón, y aprovechar para encargarle también que me lo compile a css.

Para eso necesito instalar los loaders:

  • sass-loader
  • css-loader
  • style-loader

Es decir:

npm install sass-loader css-loader style-loader -D

Además, necesitaré el compilador SaSS de node:

npm install node-sass --save-dev

Ahora que tengo los loaders, le indico a webpack que debe usarlos de forma encadenada (fíjate en que he cambiado la expresión regular de test a .scss, que es la extensión de SaSS):

webpack.config.js

//  webpack.config.js 
module.exports = {
    entry: './app/index.js',
    output: {
        filename: 'bundle.js',
        path: __dirname
    },
    module: {
        loaders: [
            {test:/\.scss$/, loader:'style!css!sass', exclude: /node_modules/}
        ]
    }
};

Ahora ya puedo cambiar mis estilos para usar SaSS.

Renombro el archivo style.css a style.scss, y le hago cambios para aprovechar SaSS, por ejemplo:

div {
    display: flex;
    height: 100%;
    background-color: #2eb398;
    color: white;
    font-size: 22px;

    p {
        flex: 1;
        text-align: center;
        margin-top: auto;
        margin-bottom: auto;
    }
}

Solo falta eliminar la llamada a styles.css desde index.html , e importar mis estilos por código. Para esto último, voy a index.js y lo modifico así (fíjate en que la URL es relativa a index.js):

index.js

//  app/index.js

//import CSS
require('../styles.scss');

//import Logger
var Logger = require('./logger.js');

//update content of greeting element when DOM has been loaded
document.addEventListener('DOMContentLoaded', function(){
    document.getElementById('greeting').innerHTML = "Hello my friend!";
    Logger("Greeting has been updated");
})

De nuevo, paro el servidor de desarrollo y vuelvo a ejecutarlo con npm start: funciona.

Debugando con WebPack

También he explicado antes que si abro el inspector de Chrome, de entrada veo todo el código Javascript entremezclado, como ves en la imagen.

webpack bundled code

Para ahorrarme este dolor de cabeza, puedo crear Source maps que enlacen el código del bundle con los archivos originales.

Para eso, modifico webpack.config.js para añadir la propiedad devtool:

//  webpack.config.js 
module.exports = {
    entry: './app/index.js',
    output: {
        filename: 'bundle.js',
        path: __dirname
    },
    devtool: 'eval-source-map',
    module: {
        loaders: [
            {test:/\.scss$/, loader:'style!css!sass', exclude: /node_modules/}
        ]
    }
};

Si paro el servidor y lo vuelvo a ejecutar, ahora sí puedo ver mis archivos originales (y usarlos para debugar el código con breakpoints y todo). Fíjate en la nueva carpeta que aparece en el Inspector de Chrome.

webpack devtool

Transpilando ES6 con Babel

Pongamos que ahora me quiero modernizar (solo un poco), y se me ocurre actualizar mi código JS a ES6.

Cambiando el código a ES6

Pues lo primero es lo primero. Modificaré mis archivos para usar Javascript moderno ¡guay!

loader.js

// app/loader.js

//export as a regular ES6 module and as an arrow function
export default (message) =>{
    console.log(new Date(Date.now()), " - ", message);
}

index.js

// app/index.js

//ES6 import Logger
import Logger from './logger.js';

//import CSS
require('../styles.scss');

//update content of greeting element when DOM has been loaded. Using arrow function.
document.addEventListener('DOMContentLoaded', () =>{
    document.getElementById('greeting').innerHTML = "Hello my friend!";
    Logger("Greeting has been updated");
})

Configurar e instalar el loader

Desde terminal, instalo el loader de babel para webpack (babel-loader). Necesito también la librería de babel para node (babel-core) y el paquete de ES6 (babel-preset-es2015):

$:  npm install babel-loader babel-core babel-preset-es2015 -D

Además, voy a mi webpack.config.js y añado babel a los loaders:

//  webpack.config.js 
module.exports = {
    entry: './app/index.js',
    output: {
        filename: 'bundle.js',
        path: __dirname
    },
    devtool: 'eval-source-map',
    module: {
        loaders: [
            {test:/\.js$/, loader:'babel', exclude: /node_modules/, query: { presets: ['es2015'] } },

            {test:/\.scss$/, loader:'style!css!sass', exclude: /node_modules/}
        ]
    }
};

Fíjate que al loader le paso una nueva propiedad query con el parámetro presets: ['es2015']. Esto es imprescindible para que el loader de Babel sea capaz de detectar los imports de código de ES6 (también llamado ES2015).

Resultados

Si has seguido los pasos anteriores correctamente, solo tienes que cerrar y volver a lanzar el servidor de desarrollo y… ¡voilà! La app funciona y estás usando ES6.

¡Incluso te has desecho de ese sucio require('logger') en CommonJS!

Minificando el código

Ya has visto casi todo y ahora solo me faltaría crear un bundle.js con el código minificado para que mi web se descargue más rápido.

Para eso, solo tengo que importar la librería webpack en el archivo de configuración y añadir el plugin UglifyJsPlugin. Mi archivo queda así:

webpack.config.js

//  webpack.config.js

var webpack = require('webpack');

module.exports = {
    entry: './app/index.js',
    output: {
        filename: 'bundle.js',
        path: __dirname
    },

    devtool: 'eval-source-map',

    module: {
        loaders: [
            {test:/\.js$/, loader:'babel', exclude: /node_modules/, query: { presets: ['es2015'] } },
            {test:/\.scss$/, loader:'style!css!sass', exclude: /node_modules/}
        ]
    },

    plugins: [
        new webpack.optimize.UglifyJsPlugin()
    ]
};

¡Y ya está! Así de simple.

Para el servidor de desarrollo, vuelve a lanzarlo y verás como se carga la web sin problemas. Pero fíjate en el código que revela el Inspector de Chrome, tu bundle.js ahora es una única linea de funciones compuestas por letras aisladas. Menos espacio. Más ilegible.

webpack-minified-bundle

Conclusiones

Esto es solo una pincelada de lo que puede darte Webpack, pero si has seguido el artículo hasta el final, ya tienes una buena base para empezar a usarlo y trabajar de una forma más eficiente.
Dime ¿te ha convencido Webpack?

Published inAngular 2ES6Javascript

5 Comments

  1. Oscar Oscar

    buenísimo 😀 hasta ahora en todos los tutoriales de react usan webpack pero nadie lo ha explicado detenidamente… gracias me suscribo de inmediato! =)

    • Enrique Oriol Enrique Oriol

      🙂

  2. Gracias. Excelente explicación, una base necesaria para iniciar proyectos con JS.

  3. C:\Users\ndchi\Documents\angular2BinariaOnline\webpackSimple\node_modules\.bin\webpack-dev-server:2
    basedir=$(dirname “$(echo “$0″ | sed -e ‘s,\\,/,g’)”)
    ^^^^^^^
    SyntaxError: missing ) after argument list
    cuando desde terminal hago npm start

  4. Buenas, podrías hablar algo más sobre lo que has comentado: “Puedes generar solo aquellos fragmentos de JS que realmente necesita cada página (haciendo más rápida su carga)”

Deja un comentario