webpack es una herramienta de agregación de recursos para aplicaciones web que permite generar una distribución única a partir de un conjunto establecido de assets.
Entre los recursos que es capaz de gestionar webpack, podemos encontrar formatos soportados por defecto como HTML, JavaScript, CSS, así como otros formatos que necesitan ser transformados como SASS, CoffeeScript o Jade.
Cuando trabajamos en un proyecto web, en un modo de funcionamiento más tradicional, es habitual partir de un fichero HTML con ficheros JavaScript y CSS relacionados mediante los tags script
, link
o style
.
El problema suele ser que, a medida que las dependencias crecen y el desarrollo se complica, aumenta el número de assets a ser incluidos en el HTML y con ello el número de peticiones que el navegador debe realizar al servidor para cargar la página, haciendo que el rendimiento se vea perjudicado ostensiblemente.
Con webpack definiremos un único pipeline de procesamiento de recursos, obteniendo un único fichero empaquetado y minificado con todos los recursos necesarios para nuestro desarrollo.
En este aritículo examinaremos a fondo webpack, centrándonos en los principales casos de uso con los que nos podemos encontrar.
Instalación
webpack utiliza NodeJS, por lo que tenemos que poder ejecutar previamente (cualquier versión actual de NodeJS servirá):
$ node -v
v5.10.1
Por otra parte, vamos a instalar webpack mediante NPM, por lo que también lo deberemos tenerlo operativo:
$ npm -v
3.8.3
Con NodeJS activo y funcionando, ya podemos instalar webpack para su uso en línea de comandos:
$ npm install webpack -g
NOTA: Para usar una dependencia en la sección de scripts
del package.json
, no es necesario instalar esta dependencia como global con -g
. En este caso, con un npm install
convencional es suficiente al enlazarse siempre las dependencias en el node_modules/.bin
.
Otro punto importante es que instalar un paquete como webpack de forma global hace que no tengamos un excesivo control sobre la versión que estamos utilizando. Si omitimos el flag -g
, webpack se instalará en nuestro directorio de trabajo y tendremos que ejecutarlo de la siguiente manera:
$ node_modules/.bin/webpack
Creación de un proyecto simple
Antes de nada, inicializamos el proyecto:
$ mkdir webpack-simple
$ cd webpack-simple
$ npm init -y
NOTA: Como recomendación, es interesante añadir al package.json
el atributo "private": true
para que este módulo nunca pueda ser subido a un repositorio NPM público.
A continuación, debemos instalar la dependencia de webpack en el proyecto con:
$ npm install webpack --save-dev
NOTA: Si eres un fan de la linea de comandos, recuerda que NPM tiene un modo abreviado para lanzar instrucciones. Por ejemplo, el comando anterior podría simplicarse de la siguiente forma: npm i webpack -D
Para nuestro primer ejemplo "simple", vamos a crear un fichero app.js
con un contenido sencillo:
console.log('Webpack rocks!!');
Y un index.html
desde el que se incluya el resultado de ejecutar webpack sobre estos assets:
<!doctype html>
<html>
<head>
<script src="bundle.js">
</head>
<body>
</body>
</html>
Si ahora ejecutamos webpack en línea de comandos, podremos generar el fichero bundle.js
y ya podremos cargar index.html
en nuestro navegador y comprobar la consola para ver el mensaje que hemos generado:
$ webpack app.js bundle.js
Por último, podemos registrar como script en NPM el comando que acabamos de ejecutar en línea de comandos, con el fin de simplificar el acceso al mismo, quedando el package.json
como sigue:
{
"name": "webpack-simple",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"build": "webpack app.js bundle.js"
}
}
Finalmente, ya podemos ejecutar el nuevo script de una forma sencilla:
$ npm run build
Hash: 05a0258fd3b2db7cabe0
Version: webpack 1.12.1
Time: 30ms
Asset Size Chunks Chunk Names
bundle.js 1.42 kB 0 [emitted] main
[0] ./app.js 32 bytes {0} [built]
Uso de módulos
NodeJS implementa un sistema de definición e importación de módulos conocido como CommonJS. De forma muy resumida, CommonJS permite que podamos exportar funciones y objetos complejos desde un fichero JavaScript o módulo, para luego importarlos y poder así reutilizarlos donde sea necesario mediante el uso de exports
y require
.
Así pues, podemos tener un fichero messenger.js
con el siguiente contenido y exportar la función hola
mediante module.exports
:
module.exports = function hola(valor) {
console.log('Hola ' + valor);
}
De esta forma, podemos usar posteriormente este módulo y la función exportada desde nuestra aplicación principal app.js
mediante require
:
var saluda = require('./messenger.js');
saluda();
Como comentaba al principio del apartado, este mecanismo está soportado por NodeJS, pero no tenemos una forma nativa de utilizar CommonJS en los navegadores (al menos hasta que todos implementen ES6 modules).
Para conseguir esta modularización y encapsulación de nuestro código, podemos optar por utilizar el Module Pattern o delegar en herramientas como webpack o browserify para que nos provean de esta funcionalidad en el navegador.
Para ponerlo en práctica, crearemos un nuevo proyecto llamado webpack-modules
siguiendo el proceso de configuración del ejemplo anterior (crear directorios, npm init
e instalar dependencia de webpack).
A continuación, aplicaremos los cambios descritos (cambiamos el app.js
y creamos el messenger.js
) y ejecutaremos npm start
para lanzar webpack y generar así el resultado final:
$ npm run build
Hash: c100270751e8d3b6a8d7
Version: webpack 1.12.1
Time: 37ms
Asset Size Chunks Chunk Names
bundle.js 1.59 kB 0 [emitted] main
[0] ./app.js 50 bytes {0} [built]
[1] ./messenger.js 72 bytes {0} [built]
Si ahora abrimos el fichero index.html
con el navegador, veremos el mismo resultado de antes por consola, pero esta vez la lógica ejecutada ha sido importada desde el módulo correspondiente.
Si tienes curiosidad por ver como webpack ha obrado el milagro de los módulos, edita el fichero generado bundle.js
y verás todo el montaje que hace falta :)
Gestión de la configuración
Como hemos visto, es posible llamar al comando webpack con los parámetros necesarios para realizar el procesamiento. En cualquier caso, a medida que ajustemos su funcionamiento con más opciones, será mucho más sencillo definir todas las configuraciones en un fichero único que webpack sea capaz de cargar cuando lo lancemos.
Este fichero de configuración se suele llamar webpack.config.js
y una definición básica del mismo puede ser:
module.exports = {
entry: './app.js',
output: {
path: '.',
filename: 'bundle.js'
}
}
Con este fichero creado en nuestro directorio de proyecto, podremos ejecutar webpack sin pasarle ninguna opción y él mismo cargará automaticamente el fichero de configuración con las opciones definidas.
Loaders: Hojas de estilo CSS
Una de las premisas que sigue webpack es que cualquier recurso utilizado en la aplicación debe definirse y ser tratado como un módulo, independientemente de si es un fichero JavaScript, CSS o cualquier otro recurso. Ya se que suena un poco raro, pero es más fácil de lo que parece :)
Esto quiere decir que, si queremos que nuestros estilos CSS sean minificados e incluidos en la distrubición generada, tendremos que hacer un require
de estos ficheros desde el punto de entrada principal (app.js
en nuestro caso) o incluirlos como "entry point" (lo mismo pasará si queremos incluir alguna imagen, por ejemplo).
De esta manera, webpack los procesará y permitirá su aplicación y carga desde JavaScript directamente, evitando el tener referenciados los ficheros CSS mediante link
o style
dentro de la página HTML principal y minimizando así el número de peticiones que se hacen al servidor para completar la carga de la página.
Vamos a ver como funciona realmente!!
Partiendo de nuestro ejemplo de uso de módulos webpack-modules
, vamos a crear un proyecto nuevo llamado webpack-css
(ya sabes, hay que inicializar el proyecto como en todos los casos anteriores).
Para este proyecto, vamos a intentar incluir en el pipeline de procesamiento de webpack el tratamiento de los estilos CSS que hemos definido en el fichero style.css
:
body {
background-color: #ddd;
color: red;
}
Para ello y si todo en webpack es un módulo, tendremos que modificar el app.js
y hacer un require
de nuestro fichero CSS. Sí, has oido bien, un require
de CSS:
require('./style.css');
var saluda = require('./messenger.js');
saluda();
Es bastante curioso, pero de esta forma va a intentar procesar este módulo mediante uno de los loaders
que tenga cargados. Como inicialmente sólo contamos con un loader para ficheros JavaScript que viene por defecto, si después de esta modificación intentamos ejecutar el comando webpack
, se producirá un error indicándonos esta situación:
Hash: b07e232caa73ce9c11c0
Version: webpack 1.12.1
Time: 46ms
Asset Size Chunks Chunk Names
bundle.js 1.75 kB 0 [emitted] main
[0] ./app.js 75 bytes {0} [built] [1 error]
[1] ./style.css 0 bytes [built] [failed]
[2] ./messenger.js 72 bytes {0} [built]
ERROR in ./style.css
Module parse failed: /base/webpack/webpack-css/style.css Line 1: Unexpected token {
You may need an appropriate loader to handle this file type.
| body {
| background-color: #ddd;
| color: red;
@ ./app.js 1:0-22
Para solucionar el problema, necesitamos hacer dos cosas. La primera es instalar el loader correcto utilizando NPM:
npm install css-loader style-loader --save-dev
Y la segunda es configurar webpack
para cargar este módulo de procesamiento CSS cuando procese nuestro pipeline.
Como hemos visto en el apartado anterior, esto lo podremos hacer modificando el fichero webpack.config.js
, el cual quedará de la siguiente forma:
module.exports = {
entry: './app.js',
output: {
path: '.',
filename: 'bundle.js'
},
module: {
loaders: [
{
test: /\.css$/,
loader: 'style!css'
}
]
}
}
NOTA: Cuando veamos en un loader más de un término separados por una admiración, entenderemos que se van a aplicar varios loaders encadenados comenenzando siempre de derecha a izquierda.
Si ejecutamos de nuevo el comando webpack
, el resultado será correcto:
$ webpack
Hash: 1a274dc84f3f29fd250a
Version: webpack 1.12.1
Time: 352ms
Asset Size Chunks Chunk Names
bundle.js 10.7 kB 0 [emitted] main
[0] ./app.js 75 bytes {0} [built]
[5] ./messenger.js 72 bytes {0} [built]
+ 4 hidden modules
Si queremos ver en detalle todos los módulos intermedios que ha tenido que cargar webpack
para completar la tarea de incluir nuestro CSS, tendremos que incluir el paràmetro --display-modules
:
$ webpack --display-modules
Hash: 1a274dc84f3f29fd250a
Version: webpack 1.12.1
Time: 274ms
Asset Size Chunks Chunk Names
bundle.js 10.7 kB 0 [emitted] main
[0] ./app.js 75 bytes {0} [built]
[1] ./style.css 895 bytes {0} [built]
[2] ./~/css-loader!./style.css 208 bytes {0} [built]
[3] ./~/css-loader/lib/css-base.js 1.51 kB {0} [built]
[4] ./~/style-loader/addStyles.js 6.09 kB {0} [built]
[5] ./messenger.js 72 bytes {0} [built]
No estaría de más revisar el contenido del bundle.js
. Ahora podremos ver que hay mucho más código y que hay una sección dedicada a cargar nuestros estilos CSS y aplicarlos.
Hojas de estilo externas
En general, mezclar dos tipos de outputs como son el procesamiento de JavaScript y el CSS, no es del todo deseable, ya que es importante favorecer el cacheo de los recursos en el navagador y si enviamos todos los recursos juntos, este fichero completo va a cambiar muchas más veces y se deberá descargar completo cada vez.
En el ejemplo anterior, si lo que queremos es que los estilos se generen un fichero .css
separado, sólo tendremos que usar un plugin para webpack llamado extract-text-webpack-plugin
. Este plugin permite detectar todas las definiciones de estilos, procesarlas y extraerlas del bundle.js
general, permitiendo ser guardadas en el fichero que especifiquemos:
var webpack = require('webpack');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = {
entry: './app.js',
output: {
path: '.',
filename: 'bundle.js'
},
module: {
loaders: [
{
test: /\.css$/,
loader: ExtractTextPlugin.extract('style', 'css')
}
]
},
plugins: [
new ExtractTextPlugin('style.min.css', {allChunks: true})
]
}
Multimódulo para distintos contextos
En ciertas aplicaciones, puede resultar interesante a la hora de realizar la construcción el poder disponer de un fichero de salida para cada módulo o parte de la aplicación y no generar un único fichero mega grande con todo el código. Es el caso de un proyecto en el que tenemos una parte del desarrollo para web y otro para mbile.
webpack nos ofrece soporte para esta problemática con la definición de múltiples entry
. De este forma nos permite separar el procesamiento de cada módulo implicado y generar ficheros distintos de salida. En nuestro caso, el webpack.config.js
quedaría de la siguiente forma:
module.exports = {
entry: {
web: './app-web.js',
mobile: './app-mobile.js'
},
output: {
path: '.',
filename: '[name].build.js'
}
}
NOTA: En cada caso, el comodín [name]
se sustituirá por el valor de cada una de las entry
definidas.
Generación de versiones para producción
Si después de la ejecución del comando webpack has ido viendo el contenido del fichero bundle.js
, habrás visto que este fichero no está minificado.
Para su despliegue en producción, es recomendable optimizar el tamaño del fichero final generado, por lo que webpack proporciona el parámetro -p
con el que podemos optimizar su salida.
$ webpack -p
Hash: f0b9ee353a1bb7213bae
Version: webpack 1.12.1
Time: 503ms
Asset Size Chunks Chunk Names
bundle.js 3.66 kB 0 [emitted] main
[0] ./app.js 75 bytes {0} [built]
[1] ./messenger.js 72 bytes {0} [built]
+ 4 hidden modules
WARNING in bundle.js from UglifyJs
Side effects in initialization of unused variable sourceMap [./~/style-loader/addStyles.js:185,0]
Side effects in initialization of unused variable media [./~/style-loader/addStyles.js:203,0]
Condition always false [./~/style-loader/addStyles.js:23,0]
Dropping unreachable code [./~/style-loader/addStyles.js:24,0]
Condition always false [./~/style-loader!./~/css-loader!./style.css:10,0]
Dropping unreachable code [./~/style-loader!./~/css-loader!./style.css:12,0]
Side effects in initialization of unused variable update [./~/style-loader!./~/css-loader!./style.css:7,0]
Como podemos ver, webpack utiliza internamente UglifyJS para minificar y optimizar nuestro fichero JavaScript de salida. En este sentido, puede ser normal ver algunos warnings como salida del comando.
Recarga automática de cambios
watch
Ejecutar constantemente el comando webpack todo el tiempo puede resultar realmente pesado. Con el fin de trabajar de forma desatendida, webpack incorpora el parámetro -w
(equivalente a --watch
), el cual permite monitorizar los ficheros definidos y lanzar la construcción de nuevo si alguno de ellos cambia.
Para ello sólo tenemos que modificar nuestro package.json
para que webpack se inicie con este parámetro:
{
"name": "webpack-simple",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"build": "webpack -w app.js bundle.js"
}
}
webpack-dev-server
En cualquier caso, esto sólo actualiza los carmbios en disco, por lo que tendremos que ir al navegador y recargar la página manualmente después de cada cambio.
Para mejorar este flujo de trabajo, webpack incorpora el webpack-dev-server
, el cual permite que se notifique al navegador ante cambios con el fin de que este pueda recargar la página.
Si queremos utilizarlo, actualizaremos nuestro package.json
de forma que en lugar de lanzar el comando webpack, sea el webpack-dev-server
el que sea invocado:
{
"name": "webpack-simple",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"start": "webpack-dev-server --progress --colors"
},
"devDependencies": {
"webpack": "^1.12.1",
"webpack-dev-server": "^1.10.1"
}
}
NOTA: No olvidemos que webpack-dev-server
es una nueva dependencia para nuestro proyecto y que, como tal, necesita ser instalada previamente mediante npm i webpack-dev-server -D
(al igual que hacíamos con la de webpack
).
Si aplicamos estos cambios al package.json
del proyecto webpack-simple
, podremos ejecutar npm start
y ver la siguiente salida:
$ npm start
> webpack-simple@1.0.0 start /../webpack-simple
> webpack-dev-server --progress --colors
Hash: 396f0bfb9d565b6f60f0
Version: webpack 1.13.0
Time: 9ms
webpack: bundle is now VALID.
http://localhost:8080/webpack-dev-server/
webpack result is served from /
content is served from /../webpack-simple
Como podemos ver, webpack-dev-server
nos informa de la URL que tenemos que cargar en nuestro navegador para que se produzca la magia y el navegador se recargue cada vez que realizamos algún cambio.
Es momento de probarlo!!!
Actualizaremos toda la config, lanzaremos el proyecto webpack-simple
con npm start
y accederemos al URL que se nos propone (http://localhost:8080/webpack-dev-server/ en este caso).
Si ahora vamos con nuestro editor y modificamos el fichero app.js
, veremos como el navegador realiza la recarga y el resultado de la ejecución ya recoge nuestras modificaciones (podemos cambiar el mensaje que lanza el console.log
a la consola del navegador).
Hot Module Replacement (HMR)
Por último, aunque la notificación de recarga al navegador resulta muy conveniente, sería incluso mejor si a la hora de realizar la recarga de una página, se conservara el estado de las variables en ese punto. Imaginemos que tenemos una aplicación rica que tiene pestañas, listas y múltiples botones. Si cada vez que refrescamos tenemos que reproducir toda la navegación realizada para verificar cada pequeño cambio, entonces esta forma de trabajo no nos resultará muy productiva.
Mediante la implementación de Hot Module Replacement o HMR, webpack recargará los cambios manteniendo el estado actual de nuestras variables.
Para poder utilizarlo, sólo tenemos que modificar ligeramente la configuración de nuestro package.json
:
{
"name": "webpack-hot-loader",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"start": "webpack-dev-server --hot --inline --progress --colors"
},
"devDependencies": {
"webpack": "^1.12.1",
"webpack-dev-server": "^1.10.1"
}
}
Simplemente añadiendo --hot
y --inline
, webpack habilitará la recarga con estado.
Podemos comprobarlo definiendo el siguiente webpack.config.js
:
module.exports = {
entry: './app.js',
output: {
path: '.',
filename: 'bundle.js'
}
}
Y un ejemplo en el que se mantenga estado en el fichero app.js
:
var counter = 0;
document.addEventListener("DOMContentLoaded", function(e) {
document.getElementById("action").addEventListener("click", function() {
console.log("Webpack rocks!! " + counter);
counter++;
});
});
Sólo nos queda arrancar el webpack-dev-server
con npm start
y probar que funciona:
$ npm start
> webpack-hot-loader@1.0.0 start /../webpack-hot-loader
> webpack-dev-server --hot --inline --progress --colors
70% 1/1 build moduleshttp://localhost:8080/
webpack result is served from /
content is served from /../webpack-hot-loader
Hash: 5de8396088de010c8afa
Version: webpack 1.13.0
Time: 343ms
Asset Size Chunks Chunk Names
bundle.js 254 kB 0 [emitted] main
chunk {0} bundle.js (main) 216 kB [rendered]
[0] multi main 52 bytes {0} [built]
[1] (webpack)-dev-server/client?http://localhost:8080 2.67 kB {0} [built]
[2] ./~/url/url.js 22.3 kB {0} [built]
[3] ./~/url/~/punycode/punycode.js 14.6 kB {0} [built]
[4] (webpack)/buildin/module.js 251 bytes {0} [built]
[5] ./~/querystring/index.js 127 bytes {0} [built]
...
[73] ./~/ansi-regex/index.js 135 bytes {0} [built]
[74] (webpack)/hot/dev-server.js 1.85 kB {0} [built]
[75] (webpack)/hot/log-apply-result.js 813 bytes {0} [built]
[76] ./app.js 232 bytes {0} [built]
webpack: bundle is now VALID.
A continuación, accederemos a la página del webpack-dev-server
(http://localhost:8080/webpack-dev-server/), donde cargará la aplicación. Si ahora abrimos las Chrome Tools y modificamos el console.log
que se hace en el fichero app.js
, veremos algunos mensajes que nos informan de la recarga y los cambios se aplicarán inmediatamente sin perder el estado actual. Esta es la salida de las Chrome Tools:
Webpack rocks: 0
bundle.js:8098 Webpack rocks: 1
bundle.js:8098 Webpack rocks: 2
2bundle.js:619 [WDS] App updated. Recompiling...
bundle.js:684 [WDS] App hot update...
bundle.js:8048 [HMR] Checking for updates on the server...
bundle.js:8011 [HMR] Cannot apply update. Need to do a full reload!
bundle.js:8053 [HMR] Waiting for update signal from WDS...
bundle.js:616 [WDS] Hot Module Replacement enabled.
bundle.js:8098 Webpack rocks!! 3
bundle.js:8098 Webpack rocks!! 4
Webpack 2: Llega el tree-shaking
El proyecto Rollup de Rich Harris ha popularizado una funcionalidad muy interesante en el mundo del empaquetado JavaScript: El tree-shaking
. Hablamos de tree-shaking
cuando existe algún proceso que, tras evaluar nuestro código fuente, es capaz de decidir qué funciones se utilizan o no en los módulos que importamos. Si alguna función no se utiliza, no se importará a la hora de generar el bundle
final.
Como podemos imaginar, esta funcionalidad reduce en gran medida el temaño final del bundle
.
La buena noticia es que tendremos esta funcionalidad disponible en Webpack 2, ya que ha sido incluida en su roadmap!! :)
Conclusiones
Tener nuestros assets bajo control es muy importante en proyectos de tamaño medio y grande. Con webpack podemos gestionarlos de una manera unificada y sencilla, dando solución a los casos de uso más habituales como es el procesado de ficheros JavavScript, CSS, SASS, incluso asistiendonos a la hora de desarrollar mediante el uso de Hot Module Replacement.
Si aun no has utilizado nunca webpack, dale una oportunidad y mejora tu flujo de desarrollo.