No creo que sea necesario comentar la evolución que se ha producido en el desarrollo web, pasando de aplicaciones con mucho procesado de servidor y pequeños scripts para realizar acciones muy concretas, a las actuales SPA basadas, fundamentalmente, en código javascript y una gran carga de trabajo en cliente. Ese aumento de la complejidad ha llevado a la necesidad de estructurar mejor el código para que el desarrollo sea mantenible (módulos javascript, por ejemplo) e incluso utilizar lenguajes que, aunque requieren un preprocesado para convertirlos a javascript o css,  incorporan funcionalidad que nos hace más sencillo el trabajo.

Por lo tanto, ahora tenemos un paso más a realizar a la hora de poner nuestra aplicación en funcionamiento. Este paso es el de preprocesar los archivos de código para convertirlos a un lenguaje que los navegadores puedan interpretar. Y no sólo ésto, sino también la necesidad de empaquetar todos los archivos en un número reducido de ellos (con el fin de evitar muchas llamadas de petición de ficheros), la necesidad de minificarlos para reducir la cantidad de información transmitida desde el servidor a cliente o incluso la necesidad de valorar la calidad del código que hacemos y si se cumplen unas ciertas reglas de estilo que se hayan podido establecer en un equipo de trabajo.

Para llevar a cabo todas estas tareas de forma automática han ido apareciendo sucesivos taskrunners, como grunt o gulp. Su forma de trabajar se basa en definir una serie de tareas que se van a aplicar a un conjunto muy definido de ficheros. Webpack, por el contrario, es un module bundler. A partir de un punto de entrada, va recorriendo las dependencias que se va encontrando y va generando los bundles correspondientes. Aunque en esta entrada hablaremos de cómo configurar webpack, siguen apareciendo nuevas herramientas como jspm.

 

Preparación del entorno

Para la parte de servidor vamos a trabajar con asp.net core RC2.

Con el fin de evitar posibles problemas, incompatibilidades, etc… con versiones anteriores recomiendo eliminar (si existiese) la carpeta dotnet (por defecto en c:\Program Files\dotnet) y eliminar la cache de NuGet de nuestro sistema; para lo cual borraremos las carpetas siguientes:

  • %userprofile%\.nuget

  • %localappdata%\NuGet\v3-cache

Como es bien conocido, una de las novedades que nos trae esta versión es la aparición del .NET core CLI (Command line interface) que viene a sustituir a todos los comandos que se usaban en versiones anteriores (dnvm, dnx o dnu) por un único comando (dotnet) (http://dotnet.github.io/docs/core-concepts/dnx-migration.html). Así que, el primer paso es instalar el citado CLI. Nosotros lo vamos a instalar a partir del script de powershell que se nos facilita desde el repositorio en github del proyecto. Descargamos el script, o bien lo copiamos y guardamos en un fichero, asignándole un nombre conveniente (en nuestro caso install.ps1). Posteriormente, abrimos powershell, nos situamos en el directorio donde hemos guardado el archivo anterior y arrancamos la instalación utilizando: ./install.ps1 -channel preview.

image

(Cuando realicé la instalación fue necesario añadir el directorio %localappdata%\Microsoft\dotnet al Path para tener el comando dotnet disponible en todo el sistema de ficheros)

Como IDE utilizaremos Visual Studio Code. Recomiendo instalar algunas extensiones que nos faciliten el trabajo; por ejemplo, son especialmente útiles la extensión de C#, la de jsx, react.js snippets, html hints…. Para ello, pulsamos F1 y en el cuadro de diálogo que nos aparece escribimos ext install y elegimos la extensión que queramos instalar en ese momento.

Trabajando en la parte de servidor

Instalado el CLI, empezaremos creando el proyecto. Nos situamos en el directorio donde queramos trabajar y utilizamos el siguiente comando:

dotnet new

Esto nos creará un proyecto básico, consistente en tres ficheros: NuGet.Config, Program.cs y project.json:

 

image

 

El proyecto que hemos creado, al ejecutarse, escribe Hello World! en consola. Para hacerlo, utilizamos primero el comando dotnet restore, con el fin de restaurar todas las dependencias; y, cuando termine, dotnet run, que lanzará el programa.

 

image

 

Ya en Visual Studio Code, abrimos la carpeta del proyecto que hemos creado para examinar su estructura:

 

image

 

Como vemos, nos aparece un nuevo archivo (project.lock.json), creado cuando hemos realizado la restauración de dependencias. Este fichero contiene las referencias concretas a los paquetes que necesita nuestro proyecto (y las dependencias de cada uno, por supuesto) (http://blog.falafel.com/what-is-project-lock-json/). Vamos a partir de este proyecto básico de consola para generar nuestro proyecto web, modificando los archivos que se nos han generado y añadiendo las referencias oportunas.

Vamos a incluir las últimas versiones de las librerías de desarrollo disponibles, lo que nos obliga a añadir un nuevo feed de NuGet para obtenerlas. Nos vamos al fichero NuGet.Config y añadimos el feed de aspnetcidev, quedando de la siguiente forma:

[sourcecode language=»xml» padlinenumbers=»true»]
<xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<!–To inherit the global NuGet package sources remove the <clear/> line below –>;
<clear>
<add key="aspnet-core" value="https://www.myget.org/F/aspnetcidev/api/v3/index.json" />
<add key="dotnet-core" value="https://dotnet.myget.org/F/dotnet-core/api/v3/index.json" />
<add key="api.nuget.org" value="https://api.nuget.org/v3/index.json" />
</packageSources>
</configuration>
[/sourcecode]

En el fichero project.json añadimos las dependencias y configuraciones necesarias para convertir nuestro proyecto en un proyecto web. La referencia de cada una de las secciones de este fichero se pueden encontrar

Vamos a modificar el fichero project.json, para añadir todas las dependencias y configuraciones necesarias para nuestro proyecto. La referencia de cada una de las secciones de este fichero se puede encontrar aquí. Muy importante es el uso del wildcard a la hora de referirnos a la versión deseada del paquete. De la forma en que se muestra a continuación (y habiendo añadido el feed de NuGet previamente), se nos actualizaran las dependencias a la última versión disponible. Por el contrario, si queremos una versión concreta, bastará con añadir el valor exacto de la versión deseada.

[sourcecode language=»javascript» padlinenumbers=»true»]
{
"version": "1.0.0-*",
"buildOptions": {
"emitEntryPoint": true,
"debugType": "portable"
},
"dependencies": {
"Microsoft.AspNetCore.Server.IISIntegration": "1.0.0-*",
"Microsoft.AspNetCore.Server.Kestrel": "1.0.0-*",
"Microsoft.NETCore.App": {
"type": "platform",
"version": "1.0.0-*"
}
},
"frameworks": {
"netcoreapp1.0": {
"imports": ["portable-net45+wp80+win8+wpa81+dnxcore50","portable-net451+win8"]
}
},
"tools": {
"Microsoft.AspNetCore.Server.IISIntegration.Tools": {
"version": "1.0.0-*",
"imports": "portable-net45+wp80+win8+wpa81+dnxcore50"
}
},
"scripts": {
"postpublish": "dotnet publish-iis –publish-folder %publish:OutputPath% –framework %publish:FullTargetFramework%"
}
}
[/sourcecode]

Restauramos estas dependencias haciendo, de nuevo, dotnet restore.

El siguiente paso consiste en crear una nueva clase, denominada Startup.cs, donde configuraremos los parámetros de nuestra aplicación:

[sourcecode language=»csharp»]
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;

namespace WebpackExample
{
public class Startup
{
public void Configure(IApplicationBuilder app)
{
app.Run(context =>
{
return context.Response.WriteAsync($"Hello World!");
});
}
}
}
[/sourcecode]

En esta primera versión, simplemente escribimos en el contexto el mismo mensaje que teníamos antes.

Posteriormente, modificamos el punto de arranque de la aplicación (clase Program.cs), de la siguiente forma:

[sourcecode language=»csharp»]
using System.IO;
using Microsoft.AspNetCore.Hosting;

namespace WebpackExample
{
public class Program
{
public static void Main(string[] args)
{
var host = new WebHostBuilder()
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.Build();
host.Run();
}
}
}
[/sourcecode]

Una vez hecho esto, volvemos a la consola y lanzamos la aplicación utilizando dotnet run.

 

image

 

Tenemos la aplicación lanzada y lista para acceder en la dirección http://localhost:5000. Nos vamos al navegador y veremos lo siguiente:

 

image

 

Vamos a modificar el comportamiento de la aplicación para servir un fichero html. Para ello, nos vamos a la clase Startup y la modificamos de la siguiente forma:

[sourcecode language=»csharp»]
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;

namespace WebpackExample
{
public class Startup
{
public void Configure(IApplicationBuilder app)
{
app.UseDefaultFiles();
app.UseStaticFiles();
app.MapWhen(context => {
var path = context.Request.Path.Value;
return !path.Contains("api");
},
spa => {
spa.Use((context, next) =>
{
context.Request.Path = new PathString("/index.html");
return next();
});

spa.UseStaticFiles();
});

}
}
}
[/sourcecode]

(es necesario añadir la referencia a «Microsoft.AspNetCore.StaticFiles»: «1.0.0-*» en nuestro archivo project.json).

Con esta modificación, hacemos que nuestra aplicación pueda servir ficheros estáticos (imágenes, html, javascript… que se envían directamente al navegador) y, además, al añadir el app.UseDefaultFiles(), lo que conseguimos es que el middleware se encargue de buscar la página por defecto para mostrar al cliente (en nuestro caso será el index.html) (https://docs.asp.net/en/latest/fundamentals/static-files.html) . Así mismo, definimos que si la ruta no contiene el texto api seaa redirigida a index.html (para el que routing lo realice la SPA).

Tras restaurar las referencias necesarias y ejecutar la aplicación, en nuestro navegador no llegaremos a ver nada. Esto se debe a que no hemos generado el index.html, por lo que no se está está sirviendo ninguna página. Para solucionarlo, creamos la carpeta wwwroot y dentro de ella creamos nuestro index.html con el siguiente contenido:

[sourcecode language=»html»]
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Webpack Sample Project</title>
</head>
<body>
<div id="root">HELLO WORLD!!!!</div>
</body>
</html>
[/sourcecode]

Si hacemos ahora un dotnet run y en el navegador vamos a localhost:5000, veremos nuestro maravilloso Hello world.

 

image

 

 

Trabajando en el frontend

La idea es montar una aplicación sencilla para ilustrar cómo configurar webpack y los beneficios que aporta. Nosotros realizaremos una pequeña aplicación, utilizando React.js, con dos páginas, que permita la navegación entre ellas. Iremos añadiendo complejidad poco a poco, partiendo de una implementación básica, y detallando como vamos haciendo todo el proceso.

Lo primero es hacer un npm init en nuestro directorio de trabajo con el fin de que nos cree el archivo package.json. Tras introducir los datos correspondientes y generar el fichero, realizamos los siguientes pasos:

  • Añadimos las dependencias de webpack y webpack-dev-server:

npm install –save-dev webpack

npm install –save-dev webpack-dev-server

  • Creamos una carpeta de nombre app y dentro un archivo llamado app.js, con el siguiente contenido:

  • Modificamos el index.html que habíamos utilizando previamente. Hemos de eliminar el contenido del div con id root (nuestro Hello World !!!!) e incluir la referencia al archivo bundle que vamos a generar con webpack:

Una vez hecho esto, vamos a crear un archivo llamado webpack-config.json donde se recogerá la configuración de webpack: los loaders, plugins y demás que utilizaremos. En esta primera iteración generaremos la configuración mínima necesaria, quedando como sigue:

El elemento clave es el objeto configuration, donde vamos a definir los diferentes valores necesarios para que webpack haga su trabajo. Estos son:

  • Entry: Definimos el punto de entrada de nuestra aplicación. Como vemos,  le indicamos el archivo app.js que hemos generado anteriormente.
  • Output: En este objeto definimos a webpack lo que tiene que llevar a cabo una vez terminado su trabajo de bundling. En este caso, le decimos que el resultado de su operación lo deje en la carpeta indicada en la variable outFolder (wwwroot) y que genere un archivo llamado app-bundle.js.

Por último, y para facilitar el lanzamiento de nuestra aplicación, vamos a definir en el fichero package.json un script que nos ejecute primero la operación de webpack y después el comando dotnet run para lanzar nuestro servidor asp.net core. Queda de la siguiente forma:

Nos vamos a la línea de comando y ejecutamos npm run start, obteniendo el siguiente resultado:

 

image

 

Resumimos toda la información que se nos muestra. En primer lugar, vemos que se ha lanzado el comando webpack –progress. Este comando ejecuta webpack (cogiendo los parámetros de configuración del archivo webpack.config.js, que hemos preparado anteriormente) y nos va informando del progreso de la ejecución. Cuando acaba, nos indica que ha tardado 75 ms. y que se ha generado un archivo app-bundle.js formado por el archivo app.js. Posteriormente, se ha lanzado nuestra aplicación asp.net core.

Nos vamos al navegador y entramos a localhost:5000, obteniendo lo siguiente:

image

 

Observamos que nuestra aplicación consta de dos archivos, el index.html y el app-bundle.js. Este último muestra el boostrapper que genera webpack y el contenido de nuestro fichero app.js, que es el que hace la función de recuperar el div con id root y añadirle un nuevo div con el contenido HELLO WORLD FROM JS.

Uso de plugins

Gracias al uso de plugins podemos extender el funcionamiento de webpack. La idea es que durante el proceso de bundling, cada plugin añade nuevos comportamientos que afectan a todo el proceso. Para ilustrar cómo funcionan añadiremos un par de plugins interesantes:

HtmlWebpackPlugin

En el estado actual de nuestro desarrollo hemos de identificar el archivo resultante de webpack dentro de nuestro index.html para referenciarlo. La idea de este plugin es generar el archivo index.html a partir de un template que le indiquemos y, posteriormente, añadir, de forma automática, todas las referencias necesarias a los archivos resultantes del proceso de webpack.

  • Instalamos el plugin ejecutando el comando npm install –save-dev html-webpack-plugin
  • Retocamos el fichero index.html que tenemos en la carpeta wwwroot eliminando la referencia al archivo bundle que añadimos previamente.
  • Movemos el index.html al directorio raiz y lo renombramos a index.tmpl.html (para reflejar la idea de que no es el index definitivo sino un template)
  • Añadimos el plugin a nuestro archivo de configuración de webpack:

Como vemos se tiene que añadir una propiedad a nuestro objeto de configuración denominada plugins. Esta propiedad consiste en un array con todos los plugins que queramos intervengan en el proceso. En este caso hemos añadido el HtmlWebPackPlugin; el cual consta, principalmente, de dos parámetros:

  • template: dirección del fichero que se toma como template para la creación del fichero index.html. Si no se indica se utilizará uno por defecto.
  • hash: Se genera un hash para cada fichero resultante del proceso de webpack. Un detalle importante es que hemos modificado al atributo filename dentro de la propiedad output del objeto de configuración, añadiendo la referencia a este hash entre corchetes, para que los ficheros se generen con el nombre adecuado.

Si ejecutamos, de nuevo, el npm run start, observamos lo siguiente:

 

image

 

Se ha generado un fichero de nombre app-bundle-[hash].js y se ha ejecutado el plugin html-webpack-plugin, generando el index.html necesario en la carpeta wwwroot. Index.html que contiene la referencia a nuestro archivo resultante del bundle. Esto lo podemos ver si, nuevamente, abrimos en nuestro navegador la dirección localhost:5000

 

image

 

Banner plugin

Este plugin añade una leyenda a cada archivo resultante del proceso de webpack. Por ejemplo, podemos añadir el autor, la fecha en la que se generó, etc. Este plugin viene incluido en webpack, por lo que no es necesario instalar ninguna dependencia adicional. Para usarlo basta con añadirlo a nuestra variable plugins:

image

 

 

Añadiendo React

Vamos a aumentar un poco de complejidad a nuestra aplicación añadiendo React.js, el routing, para con ello ilustrar la necesidad de incorporar los loaders a nuestro proceso de bundling con webpack.

Lo primero a realizar, como siempre, es instalar las dependencias necesarias. En este caso necesitaremos las de react, react-dom y react router (los añadimos como dependencia de ejecución, en lugar de dependencia de desarrollo):

npm install –save react react-dom react-router

Sin explicarlo con todo detalle, al no ser el objetivo de este post, la aplicación defenitiva queda de la siguiente forma:

 

image

 

Mostramos el contenido de cada fichero:

app.js

routes.js

master.js

detail.js

 

Si ahora tratamos de lanzar nuestra aplicación, obtenemos el siguiente resultado:

 

image

 

Ha fallado la creación del bundle porque webpack no puede procesar adecuadamente los ficheros .js al estar codificados utilizando ES6 y jsx. Como bien nos dice el mensaje de error: “You may need an appropriate loader o handle this file type”, es decir, necesitamos incorpar el/los loaders adecuados que traten con los diferentes tipos de archivos que conforman nuestra aplicación.

 

Loaders

Uno de los elementos más importantes para el trabajo con webpack son los loaders. Los loaders se encargan de preprocesar los archivos que se le indiquen aplicándoles un determinado número de cambios y/o transformaciones. Todos los loaders que se van a utilizar se tienen que declarar dentro de una propiedad llamada module de nuestro objeto de configuración. Todos comparten la siguiente firma para su configuración:

  • test: Indicamos una expresión regular que resuelve todas las extensiones de los ficheros a los que queremos aplicar el loader. Es un parámetro obligatorio.
  • loader/loaders: Nombre del loader a usar. En el caso de que utilicemos loaders será un array con el nombre de los loaders. Es muy importante tener en cuenta que los loaders se aplicarán siempre en orden inverso al que los declaremos (siempre desde derecha a izquierda).  También obligatorio.
  • include/exclude: Podemos indicar, de forma explícita, archivos o carpetas que queramos procesar o excluir del mismo.
  • query: Es un string donde podemos pasar más parametros a nuestro loader. Cada loader tiene que especificar el formato que acepta y los valores de configuración.

Volviendo a nuestro ejemplo, necesitaríamos utilizar un loader que transformara todos nuestros archivos .js que están en formato jsx en código javascript plano. El loader que realiza esta transformación se denomina babel.

Siguiendo nuestro procedimiento habitual, lo primero es instalar las dependencias correspondientes a babel. En este caso hemos de instalar, además, un par de presets necesarios para es2015 y jsx:

npm install –save-dev babel-loader babel-core babel-preset-es2015 babel-preset-react

Incluímos el loader en nuestro archivo de configuración de webpack, con lo que el objeto configuration quedara de la siguiente forma:

También hemos de configurar babel indicándole que use los presets que hemos descargado. Para ello crearemos en el directorio raíz de nuestro proyecto un archivo con nombre .babelrc añadiendo en ese archivo lo siguiente:

Probamos de nuevo a ejecutar el comando npm run start:

 

image

 

Se ha generado el bundle correctamente y la aplicación se ha lanzado. También podemos observar como el tiempo que ha tardado en crear el bundle se ha aumentado considerablemente, así como el tamño del fichero js generado. Esto es debido a que se han añadido a ese fichero todas las librerías que son necesarias para el correcto funcionamiento de la aplicación. Si nos vamos al navegador:

image

image

 

Loaders para estilos

Al igual que hemos configurado un loader para los archivos jsx, necesitaremos otro loader para preprocesar archivos less o sass. Os recomiendo la lectura de esta entrada donde se comenta la forma de trabajar más adecuada con estilos utilizando webpack. La idea es utilizar pequeñas hojas de estilo para cada componente, que a su vez utilizan otras hojas de estilo que contienen nuestros mixins y variables. Durante el proceso de bundling, webpack irá recogiendo aquellas que sean instanciadas y las incluirá en el bundle en el orden en que hayan sido requeridas.

En nuestro ejemplo incluiremos un archivo de estilos genérico donde declararemos algunas variables y posteriormente utilizaremos una hoja de estilos para cada componente (página inicial y la de detalle) (Os recomiendo instalar la extensión para sintaxis Sass Indented en VS code para seguir el ejemplo). La estructura de la aplicación queda de la siguiente forma:

webpack-scaffolding

colors.sass

detail.sass

 

detail.js

(No hemos añadido la parte de master, porque es similar a la que hemos detallado aquí de detail)

Como siempre, instalamos las dependencias necesarias, en este caso:

npm install –save-dev style-loader css-loader sass-loader node-sass

Una vez tenemos las dependencias instaladas, actualizaremos nuestro archivo de configuración de la siguiente manera:

¿Qué hemos hecho?, hemos añadido a nuestro array de loaders un nuevo elemento que se encargará de procesar aquellos archivos con extensión .scss o .sass que se encuentren dentro de la carpeta app. En este elemento hemos indicado en lugar de la propiedad loader, la propiedad loaders, indicando el array de loaders que se aplicarán a cada uno de los ficheros. Como hemos comentado previamente, el orden de aplicación de los loaders es de derecha a izquierda. El proceso que se sigue es el siguiente: el loader sass convierte todos los archivos de extensión sass o scss a css; posteriormente, entra en juego el loader css,  encargado de buscar todos los posibles url o imports que se encuentren en estos ficheros e interpretaros como require (además le pasamos el argumento modules, que permite utilizar css de alcance local para facilitar la reutilización de módulos (css-modules)); para que, el último loader (style) inyecte los estilos resultantes.

Si ejecutamos la aplicación, observamos que no se ha generado ningún archivo css en nuestro bundle; ésto se debe a que el loader style, por defecto, inyecta los estilos en directamente en javascript:

image

image

image

 

Sin embargo, sí que hemos obtenido el beneficio de la modularidad en css, ya que en cada componente se ha aplicado el estilo adecuado, aún teniendo el mismo nombre de clase.

 

Mejorando nuestro tratamiento de estilos

En primer lugar, vamos a incorporar un nuevo loader (postcss) que nos permitirá realizar un postprocesado de nuestros estilos a través de diversos plugins. Entre ellos, vamos a utilizar autoprefixer, que nos va añadir los prefijos de cada vendor a nuestros estilos de forma automática. Además, incorporaremos el plugin ExtractTextPlugin, con el fin de extraer los estilos de los ficheros javascript y situarlos en un archivo css. Como siempre, instalamos las dependencias necesarias:

npm install –save-dev postcss-loader autoprefixer extract-text-webpack-plugin

El fichero de configuración queda tal que así:

Comentamos los cambios:

  • Hemos cambiado todas las var por const, más representativo de su función (recomiendo la lectura de este enlace)
  • Añadimos la instancia de ExtractTextPlugin en nuestro array de plugins, indicando el nombre del fichero css de salida (podemos incluir también el hash).
  • Modificamos el loader de estilos, añadiendo el extracttextplugin, teniendo en cuenta varios detalles:
    • Hemos de incorporar una nueva condición al loader de css (&importLoaders=1) con el fin de que el loader postcss pueda trabajar con módulos css.
    • Incorporamos el loader de postcss justamente antes del loader de sass.
  • En el apartado de postcss incorporamos autoprefixer.

El resultado es el esperado:

 

image

Añadiendo otras características

 

Linting

Es muy interesante contar con herramientas de linting que analicen nuestro código y nos aporten información de problemas potenciales. Curiosamente la herramienta original de linting en javascript (JSLint) no cuenta con un loader para webpack; pero, sin embargo, si exisen para JSHint y para ESLint. Obviaremos una explicación detallada de como utilizar JSHint y nos centraremos en la configuración de ESLint, que presenta muchísimos avances; como, por ejemplo, la posiblidad de implementar nuevas reglas. Instalamos las dependencias necesarias:

npm install –save-dev eslint eslint-plugin-react babel-eslint eslint-loader eslint-plugin-smells

Las dependencias que hemos instalado se corresponden al propio eslint, a un plugin de react para eslint en el que se han establecido algunas reglas específicas para proyectos que empleen react, babel-eslint, que sirve para conectar eslint con babel y permitir análisis en archivos que usen es6 o es2015; y, por último, eslint-loader, que es el loader de eslint para webpack.

Para configurar ESLint, crearemos un nuevo fichero llamado .eslintrc donde indicaremos las reglas que se van a aplicar. El conjunto de reglas que nos ofrece ESLint vienen recogidas en el siguiente enlace (aunque recordemos que se pueden crear reglas personalizadas). Por internet se pueden encontrar muchos ejemplos de archivos de configuración que tienen reglas más o menos severas, como por ejemplo: este y otras más configuraciones se pueden obtener a través de otros paquetes creados específicamente como eslint-config-airbnb. La guía de configuración completa la tenemos en esta dirección. Nuestro archivo de configuración quedará de la siguiente forma:

Resumiendo:

  • Plugins: identificamos los plugins que queramos utilizar. En nuestro caso, tenemos el plugin para react y un plugin que introduce reglas que vigilan varias malas prácticas en javascript (eslint-plugin-smells)
  • Extends: Algunos plugins traen por defecto una serie de configuraciones con distintas reglas aplicadas. En este caso introducimos las reglas recomendadas tanto en eslint base como en el plugin de react.
  • Parser: Indicamos que queremos utilizar babel (que tenemos configurado previamene en el proyecto).
  • Env: Para incluir variables de entorno definidas por cada entorno. Podemos incluir node, mocha, jest, commonjs…
  • Rules: Las reglas que queramos que se comprueben en nuestros archivos. Estas reglas son reglas adicionales a las que se especifican dentro del apartado exends. En ESLint las reglas se pueden configurar con 3 rangos de severidad:
    • 0: No se tiene en cuenta.
    • 1: Aparecerá como warning.
    • 2: Aparece como error (exit code se pone a 1)

 

Además de este fichero de configuración, es recomendable especificar los ficheros o directorios donde no queramos que se lleven a cabo todas estas comprobaciones; como, por ejemplo, la carpeta node_modules. Podemos indicarle que use cualquier fichero (.gitignore sería un buen candidato), pero por defecto ESLint buscará el fichero .eslintignore. Nosotros lo hemos generado con este contenido:

Trabajando con ESLint

Podemos incluir en nuestro package.json un nuevo script que nos lance eslint para realizar las comprobaciones pertinentes:

En ese script le indicamos que se ejecute eslint en todos los ficheros (exceptuando los incluidos en el .eslintignore) que tengan extensión .js o .jsx y que guarde una caché de resultados (generará un fichero .eslintcache, para que posteriores ejecuciones sean más rápidas).

Vamos a incluir alguna modificación incorrecta en nuestro archivo master.js para ilustrar como trabajaríamos con la herramienta. El fichero quedará así:

Ahora lanzamos el script desde nuestra consola (npm run lint) y tenemos el siguiente resultado:

 

image

 

Ups, nos han salido más errores de los esperados (hay alguna doble comilla en los strings del archivo webpack.config.js). Los errores/ warnings se nos muestran cada uno indicando la línea del fichero, la columna, la categoría de la regla (en este caso son todos errores), un comentario de lo ocurrido; y, por último, la regla que se ha visto comprometida. Solucionamos los errores y volvemos a ejecutar el script:

 

image

 

Por último, vamos a incorporar ESLint a nuestro archivo de configuración de webpack. Lo incorporaremos en la categoría preLoaders, dentro de modules. Lo que conseguimos con ello es aplicar la comprobación de reglas antes de que el resto de loadersinicien su trabajo.

Poco que comentar, al igual que el resto de loaders configurados previamente, indicamos las extensiones de los ficheros donde queramos que se aplique el proceso, el nombre del loader a usar y la carpeta base de los ficheros. Podemos, además, utilizar algunos elemento de configuración por si queremos que el proceso de bundling falle si encuentra algún error de linting (Documentación del loader). Si ahora ejecutamos nuestro bundle (habiendo introducido, de nuevo, algún error en el código) obtenemos:

image

 

Por supuesto, podemos encontrar también loaders/plugins que realicen linting sobre archivos de estilos (sass, less o css), como sass-lint-webpack-plugin, stylelint

 

Separación entre bundle de vendors y el propietario

Es bastante común el separar los archivos propios de los archivos de terceros. Hasta ahora hemos generado un archivo llamado app-bundle-[hash].js que contiene todo el código. Para ello, modificaremos nuestro archivo de configuración de webpack de la siguiente forma:

Resumimos los cambios:

  • Hemos incluido una referencia a nuestro fichero package.json ya que necesitaremos recuperar las dependencias que querramos incluir en nuestro archivo para los vendors.
  • Añadimos el plugin CommonChunksPlugin indicandole el punto de entrada llamado vendor, que lo extraiga a un archivo llamado vendor[chunkhash].js y que el número mínimo de chunks que incluya sea infinito (nos asegura que se incluyan todos los archivos que se referencien a partir de la entrada indicada).
  • Hemos modificado todos los [hash] de los nombres de los archivos  generados por [chunkhash]. La razón, muy bien explicada en este enlace, y que deberíamos implementar en nuestro bundle de producción (lo veremos en un apartado posterior) es debido a mejorar el caching de ficheros por parte del navegador. El [hash] que hemos introducido hasta ahora se genera de forma diferente en cada proceso de build, mientras que el [chunkhash] es un hash específico para cada chunk, y que sólo cambiara si se ha producido algún cambio en alguno de los ficheros que forme parte de dicho chunk (recordemos que un chunk es un fragmento de todo el código que conforma la aplicación).
  • Se ha modificado la propiedad entry, introduciendo la diferencia entre app, que es lo que teníamos hasta ahora, y vendor, donde se incluyen todas las dependencias que encontremos en nuestro archivo package.json. Importante recordar que hemos de añadir en dependences las dependencias que son necesarias para el funcionamiento de la aplicación (en nuestro caso: react, react-dom y react-router) de las devDependencies, que son aquellas necesarias durante el desarrollo y que, por supuesto, no incluiremos en nuestro bundle.

Probamos a lanzar de nuevo la aplicación y obtenemos el siguiente resultado:

 

image

 

Definir bundling de producción y desarrollo

Producción:

La configuración que tenemos es relativamente adecuada para un entorno de producción. Aparte de modificaciones más avanzadas (uso de require.ensure en nuestros ficheros para cargar módulos a demanda o separar los archivos que incorporamos a producción (versiones minificadas preparadas por los vendors), por ejemplo) bastaría con realizar un par de optimizaciones:

  • Minificación de ficheros.
  • Optimización de los ids de nuestros módulos y chunks. Webpack les asigna unos ids a cada uno de ellos, pero podemos decirle que priorice (haga los ids más pequeños) a aquellos que sean usados de forma más frecuente.

Para conseguirlo, vamos a realizar los siguientes pasos:

  • Copiamos el contenido de nuestro fichero webpack.config.js a otro fichero de nombre webpack.config.prod.js y modificamos el parámetro outFolder:

const outFolder = path.resolve(__dirname, ‘build/wwwroot’);

  • Añadimos la definición de plugin siguiente, que nos permite eliminar algún test helper que se encuentran en los archivos vendor y que no son necesarios en producción:

  • Añadimos un nuevo script en el fichero package.json de la siguiente forma:

En el script de build hemos añadido el –optimize-minimize que se encarga de minificar los ficheros utilizando el plugin uglifyJs y el –optimize-ocurrence-order se encarga de priorizar los ids de los módulos más frecuentemente usados. Además en la parte de servidor hemos incorporado el comando dotnet publish con la configuración en release y como carpeta destino la carpeta build. Tenemos los resultados siguientes:

npm run start

image_thumb.png

npm run build

image

 

La reducción de los tamaños de los ficheros es apreciable. También es interesante comentar que cuando generamos el bundle de producción nos salen warnings referidos a que hemos minificado los componentes de react de desarrollo en lugar de utilizar los componentes de producción.

 

Desarrollo:

Vamos a crear un archivo llamado webpack-dev-server.js, donde vamos a configurar un servidor de desarrollo. Anteriormente hemos añadido la dependencia de webpack-dev-server, que no es más que un pequeña aplicación express sobre node.js que se encargará de servir los ficheros estáticos procedentes de la ejecución de webpack. La mayor ventaja de usarlo es que mantiene todos esos archivos en memoria, actualizando de manera automática el navegador con los cambios que nosotros realicemos a nuestros archivos. (HMR)

Repasamos los diferentes elementos de este archivo. Lo principal es crear un nuevo webpack-dev-server, que consta de los siguientes parámeros de configuración:

  • colors: Añade colores a la salida por consola.
  • historyApiFallback: Indica que se haga uso del History Api de HTML5. Es especialmente útil si se trabaja con SPA´s.
  • hot: Sirve para indicar si queremos utilizar el hot module replacement. El HMR consiste en que cualquier cambio que apliquemos a nuestros archivos javascript o de estilos se actualizarán automáticamente en nuestro navegador sin necesidad de recargar la página.
  • inline: Añade código de gestión en el bundle para facilitar el HMR.

Tenemos que añadir igualmente unos pequeños cambios en el archivo de configuración de webpack. Recordemos que el archivo de configuración que usamos para desarrollo es webpack.config.js:

Enumeramos los cambios introducidos:

  • Hemos dejado la carpeta de salida a wwwroot.
  • Hemos quitado el BannerPlugin.
  • Se ha añadido el HotModuleReplacementPlugin.
  • Se han añadido nuevas propiedades en nuestro objeto de configuración:
    • debug: A true, indica que estamos en un entorno de desarrollo.
    • debugPort: Fija el valor del puerto del webpack-dev-server.
  • Hemos cambiado nuestro loader para estilos eliminando el ExtractTextPlugin ya que éste ocasiona problemas con el HMR. Debido a ello los estilos quedarán insertados en el javascript, Sin embargo, en desarrollo cambiaremos los estilos en código y éstos se actualizarán en el navegador, con lo que no nos supone grave perjuicio.
  • Se actualizaron las variables de la propiedad entry, en concreto la variable app, incluyendo las referencias necesarias para el webpack-dev-server.
  • Se pueden hacer más modificaciones a gusto de cada uno: eliminar los hash, no separar el vendor, cambiar la propiedad dev-tool (genera sourcemaps) a otros valores para que la generación del bundle sea más rápida (configuración webpack – devtools)…

Por último, hemos de modificar nuestro script de desarrollo, definido en el archivo package.json, para que tenga en cuenta el webpack dev server. Quedaría de la siguiente forma:

Probamos que todo funciona como se espera:

npm run start

webpack-dev-server

webpack-dev-server-running

 

Se arranca el servidor node.js escuchando en el puerto que le hayamos indicado, se procesa el bundle y se nos informa que el bundle es correcto. Con ello, acudimos al navegador:

 

hmr-ready

 

En la consola se nos informa que se ha activado HMR. Así pues, cambiamos cualquier fichero sass o js en nuestro código y veremos, al salvarlo, que la página se modifica en consecuencia, sin necesidad de recargarla:

 

hmr1

hmr2

hmr3

Vemos, que conforme se van haciendo cambios se aplican patches en la configuración original de la página para mostrar los cambios en el navegador. Sucede de igual forma tanto si aplicamos cambios en estilos como en código javascript.

 

Conclusiones

Realmente webpack es una herramienta muy versátil y realmente potente, que nos posibilita realizar multitud de tareas de forma relativamente sencilla. En esta entrada sólo hemos utilizado una configuración básica, pero aún así espero que se haya mostrado la potencia y la utilidad de usar webpack en nuestros proyectos. Quedan muchos temas por tocar, carga de imágenes, assets, cargar módulos a demanda, mejoras de los bundles, limpiar el directorio de salida cada vez que se cree un bundle…, pero con lo que se ha mostrado, el incorporar todos estos detalles debería poder hacerse sin mucha dificultad.

Sin más, como siempre, os dejo el enlace al códgio en GitHub y algunas referencias interesantes.

Un saludo y happy bundling 🙂