Compilando el Frontend con webpack

Cuando estamos haciendo desarrollo Frontend es una buena práctica separar nuestro código en distintos módulos, tanto el código de JavaScript como el CSS, e incluso nuestros templates HTML.

Sin embargo, al momento de enviarlo al usuario lo mejor es enviar un archivo, o al menos unos pocos, para eso una vez tenemos nuestro código listo lo normal es usar herramientas que tomen todos nuestro módulo y los empaqueten en un solo archivos.

Si trabajamos con preprocesadores de CSS estos nos permiten trabajar nuestros estilos en módulos y luego al compilarlos generar un único archivo final, de la misma forma existen herramientas como Browserify que nos permiten trabajar nuestro JS en módulos de CommonJS (e incluso de ES2015/6).

Aunque estas herramientas son muy buenas, existe una herramienta llamada webpack realizada específicamente para el desarrollo Frontend de aplicaciones web.

¿Qué es webpack?

Webpack es simplemente un empaquetador de módulos hecho especificamente para desarrollo Frontend, pero posible de usar incluso en Backend. Webpack esta pensado para módulos de JS, usando los sistemas de módulos de CommonJS, AMD e incluso ES6.

A diferencia de Browserify también soporta módulos descargados tanto desde npm como de Bower, y gracias a su increible sistema de loaders es posible extenderlo para poder usarlo con CSS, HTML, y más. Para terminar webpack permite la carga asíncrona de módulos para evitar mandar código no esencial al usuario.

Instalando webpack

Instalar webpack es tan fácil como instalar cualquier cosa desde npm, simplemente con un comando en la terminal:

sudo npm i -g webpack

Al igual que con Gulp.js es necesario instalar webpack de forma global y luego de forma local en cada proyecto donde lo vayamos a utilizar.

Una vez instalado global vamos a la carpeta de nuestro proyecto y ahí instalamos otra vez webpack, además instalamos los loaders que vayamos a utilizar:

npm i -D webpack
npm i -D babel-loader eslint-loader json-loader
npm i -D styles-loader css-loader stylus-loader

Loaders

Los loaders de webpack son la forma de extender webpack para que además de entender JS nativo (tal cual lo soporte al versión de Node.js usada) pueda entender cualquier otro tipo de recursos, gracias a los loaders cualquier recurso es un módulo de webpack.

En este ejemplo vamos a cargar varios loaders:

  • babel-loader: Este loader es el que habilita el soporte a ES6 gracias al uso de Babel.js.
  • eslint-loader: Este loader nos permite ejecutar ESLint antes de compilar nuestro JS para evitar errores en nuestros código y asegurarnos de seguír un estandar.
  • json-loader: Este loader nos permite cargar archivos JSON dentro de nuestros módulos JS y guardarlos en una constante como un objeto de JS ya parseado.
  • styles-loader: Este loader es el que nos permite desde nuestro módulos de JS cargar archivos de estilos y que se agreguen a etiquetas <style> o generar un archivo .css.
  • css-loader: Este loader nos permite cargar archivos .css en nuestros archivos JS, es necesario para usar styles-loader.
  • stylus-loader: Este loader compila código de Stylus a CSS para luego usarlo con css-loader.

Configurando webpack

Para poder empezar a utilizar webpack es necesario crear un archivo de configuración llamada webpack.config.js, este archivo (el cual es completamente programable con JS) es donde vamos a configurar los loaders a usar, el entry point de nuestra aplicación y la ubicación del paquete generado.

Nuestro archivo de configuración va a tener entonces algo similar a esto:

const webpack = require('webpack');
module.exports = {
entry: {
main: './src/main.js',
},
output: {
path: './build/',
filename: '[name].js',
},
};

Esta es la configuración mínima, en esta estamos definiendo un entry point llamado main y el path a dicho archivo y los datos del output donde le indicamos a webpack el path de los paquetes generados y el nombre del archivo, en el nombre vemos que dice [name], esto webpack lo substituye con el nombre del entry point, en nuestro caso main.

En nuestros entry points es posible agregar tantos como queramos y webpack los compilaría a todos a la vez.

Usando webpack

Una vez configurado webpack iniciarlo es simplemente ejecutar un comando en la terminal:

webpack

Con esto ya es suficiento para iniciar webpack una vez y generar nuestro compilado. Si quisieramos también podemos decirle a webpack que se quede escuchando nuestro entry points y los módulos que carguen para volver a compilarse automaticamente, para esto usamos el comando:

webpack --watch

Acá es donde podemos ver en acción una una de las mejores cosas de webpack, cada vez que cambiemos un módulo de nuestro código en vez de recompilar todo, como hace Browserify, webpack recompila solo ese módulo y actualiza el archivo compilado con la nueva versión del módulo modificado. Esto hace que la primera vez que se ejecuta webpack pueda tardar un poco, pero la próxima vez que se compile sea mucho más rápido.

Para terminar es posible ejecutar webpack es modo producción con el parámetro -p, esto genera archivos minificados, en el caso de JS usando Uglify, además agregando el comando -d es posible hacer que webpack genere archivos de sourcemaps para desarrollo.

webpack -p
webpack -d

Usando loaders

Con lo que hicimos hasta ahora ya es posible usar webpack para archivos JS normales, sin embargo antes descargamos varios loaders para poder extender webpack, para poder usarlos es necesario agregar una propiedad de nuestro configuración llamada module.

const webpack = require('webpack');
module.exports = {
entry: {
main: './src/main.js',
},
output: {
path: './build',
filename: '[name].js',
},
module: {
loaders: [
{
test: /\.jsx?$/,
loaders: ['babel-loader', 'eslint-loader']
},
{
test: /\.json?$/,
loader: 'json-loader'
},
]
},
};

Agregando la propiedad module con un array llamado loaders podemos ir pasando cada uno de los loaders que necesitamos, cada loader debe ser entonces un objeto con al menos dos propiedades.

La propiedad test es una expresión regular para los formatos de archivos a los que queremos aplicarle los loaders. En estos dos ejemplos a los archivos .js y .jsx (/\.jsx?$/ sirve para ambos formatos) le aplicamos 2 loaders y a los .json le aplicamos otro loader diferente.

La segunda propiedad es loaders o loader (el primero si son varios), si usamos varios loaders le tenemos que pasar un array con los nombres de cada loader en orden de derecha a izquierda, en nuestro ejemplo primero se aplicaría el eslint-loader y si este no devuelve un error se ejecutaría babel-loader para compilar el código en ES2015 y ES2016.

Si usamos un solo loader simplemente pasamos un string con el nombre del loader a usar, en este caso json-loader para parsear los archivos JSON. En algunos módulos de npm es posible que carguen archivos JSON por lo que es mejor siempre cargar este loader.

Compilando estilos con webpack

Como dije antes es posible extender webpack para que funcione con archivos de estilos e incluso otros tipos de archivos. Para el caso de los estilos antes descargamos 3 loaders, usarlos haría que webpack agregue una etiqueta <style> con nuestro CSS, aunque esto puede llegar a ser aceptable nos haría dependientes de nuestro JS para tener CSS, lo cual no es buena idea, para evitar esto podemos descargar un plugin de webpack que nos permite extraer el CSS generado a un archivo separado.

Plugins

Además de los loaders es posible cargar plugins para extender las funciones de webpack, mientras los loaders permiten que webpack entiendan otros formatos de archivos, los plugins nos permiten agregar nuevas funciones como pueden ser extraer el CSS generado o crear un archivo commons.js en el caso de que tengamos múltiples entry points.

Continuando con nuestro estilos vamos a descargar el plugin con el comando:

npm i -D extract-text-webpack-plugin

Una vez descargado vamos a actualizar nuestra configuración:

const webpack = require('webpack');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const extractCSS = new ExtractTextPlugin('[name].css');
module.exports = {
entry: {
main: './src/main.js',
},
output: {
path: './build',
filename: '[name].js',
},
module: {
loaders: [
{
test: /\.jsx?$/,
loaders: ['babel-loader', 'eslint-loader']
},
{
test: /\.json?$/,
loader: 'json-loader'
},
{
test: /\.styl$/,
loader: ExtractTextPlugin.extract('style-loader', 'css-loader!stylus-loader')
}
]
},
plugins: [extractCSS],
};

En las nuevas líneas podemos ver que primero cargamos el plugin que acabamos de descargar, luego inicializamos pasandole que use como nombre del archivo de estilos [name].css, de forma similar a como hacemos en el output.filename el [name] se va substituir con el nombre del entry point (main en nuestro caso).

Luego agreamos el nuevo loader para archivos .styl el cual va a usar el plugin para extraer los estilos generado luego de la compilación de los archivos de estilos.

Por último agregamos una propiedad plugins donde pasamos un array de los plugins usados, en este caso solo extractCSS (la instancia de del plugin extract-text-webpack-plugin que inicializamos antes.

Creando un commons.js

Antes dije que era posible crear un archivo commons.js, para el que no sabe para que serviría una archivo de este tipo basicamente es un archivo con todo el código compartido entre varios entry points, para esto necesitamos agregar más de un entry point.

Para agregar múltiples entry points solo tenemos que modificar nuestra configuración en la propiedad entry.

const webpack = require('webpack');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const extractCSS = new ExtractTextPlugin('[name].css');
module.exports = {
entry: {
home: './src/home.js',
about: './src/about.js',
},
output: {
path: './build',
filename: '[name].js',
},
module: {
loaders: [
{
test: /\.jsx?$/,
loaders: ['babel-loader', 'eslint-loader']
},
{
test: /\.json?$/,
loader: 'json-loader'
},
{
test: /\.styl$/,
loader: ExtractTextPlugin.extract('style-loader', 'css-loader!stylus-loader')
}
]
},
plugins: [extractCSS],
};

Como se puede ver simplemente cambiamos nuestro entry point main por dos entry points, uno para el home y el otro para el about. Esto solo nos permite tener multiples entry points, sin embargo no crea el commons.js, para esto vamos a cargar otro plugin de webpack, solo que esta vez el plugin ya viene incorporado con webpack.

const webpack = require('webpack');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const extractCSS = new ExtractTextPlugin('[name].css');
const commonsPlugin = new webpack.optimize.CommonsChunkPlugin('commons', 'common.js');
module.exports = {
entry: {
home: './src/home.js',
about: './src/about.js',
},
output: {
path: './build',
filename: '[name].js',
},
module: {
loaders: [
{
test: /\.jsx?$/,
loaders: ['babel-loader', 'eslint-loader']
},
{
test: /\.json?$/,
loader: 'json-loader'
},
{
test: /\.styl$/,
loader: ExtractTextPlugin.extract('style-loader', 'css-loader!stylus-loader')
}
]
},
plugins: [extractCSS, commonsPlugin],
};

Como se ve los que hacemos es que iniciamos el plugin CommonsChunkPlugin pasandole dos strings, el primero es el nombre del archivo commons.css y el segundo es el nombre del archivo commons.js.

Luego le pasamos lo que nos devuelve el plugin al array de plugins de la configuración, con solo estos dos cambios la próxima vez que ejecutemos webpack para compilar nuestro código va a generar el archivo de commons.js y commons.css.


Con esta configuración (que casi siempre se repite) es posible empezar a usar webpack de una forma bastante útil tanto para empaquetar nuestro JS como nuestro CSS.

Para concluir, webpack tiene muchas más posibilidades, como puede ser cargar módulos de forma asíncrono o realizar algo llamado hot module replacement que nos permite actualizar módulos de JS o CSS sin necesidar de recargar toda la pagína, simplemente cambiando el módulo necesario al vuelo. Combinadolo con React.js es incluso posible actualizar componentes de React.js sin perder props o states gracias a react-hot-reload.

Como se ve webpack nos permite mejorar nuestro proceso de empaquetado de módulos frontend e incluso de backend mediante un solo empaquetador para todo capaz de entender casi cualquier tipo de archivo.