Webpack o Browserify & Gulp: ¿Cuál es mejor?

Publicado: 2022-03-11

A medida que las aplicaciones web se vuelven cada vez más complejas, hacer que su aplicación web sea escalable se vuelve de suma importancia. Mientras que en el pasado bastaba con escribir ad-hoc JavaScript y jQuery, hoy en día crear una aplicación web requiere un grado mucho mayor de disciplina y prácticas formales de desarrollo de software, como:

  • Pruebas unitarias para garantizar que las modificaciones a su código no rompan la funcionalidad existente
  • Linting para asegurar un estilo de codificación consistente y libre de errores
  • Compilaciones de producción que difieren de las compilaciones de desarrollo

La web también ofrece algunos de sus propios desafíos de desarrollo únicos. Por ejemplo, dado que las páginas web realizan muchas solicitudes asincrónicas, el rendimiento de su aplicación web puede verse significativamente degradado al tener que solicitar cientos de archivos JS y CSS, cada uno con su propia pequeña sobrecarga (encabezados, protocolos de enlace, etc.). Este problema en particular a menudo se puede abordar agrupando los archivos, por lo que solo está solicitando un único archivo JS y CSS incluido en lugar de cientos de archivos individuales.

Compensaciones de herramientas de agrupación: Webpack vs Browserify

¿Qué herramienta de agrupación debería usar: Webpack o Browserify + Gulp? Aquí está la guía para elegir.
Pío

También es bastante común usar preprocesadores de lenguaje como SASS y JSX que se compilan en JS y CSS nativos, así como transpiladores de JS como Babel, para beneficiarse del código ES6 y mantener la compatibilidad con ES5.

Esto equivale a un número significativo de tareas que no tienen nada que ver con escribir la lógica de la propia aplicación web. Aquí es donde entran los ejecutores de tareas. El propósito de un ejecutor de tareas es automatizar todas estas tareas para que pueda beneficiarse de un entorno de desarrollo mejorado mientras se enfoca en escribir su aplicación. Una vez que se configura el ejecutor de tareas, todo lo que necesita hacer es invocar un solo comando en una terminal.

Usaré Gulp como un ejecutor de tareas porque es muy amigable para los desarrolladores, fácil de aprender y fácil de entender.

Una introducción rápida a Gulp

La API de Gulp consta de cuatro funciones:

  • gulp.src
  • gulp.dest
  • gulp.task
  • gulp.watch

Cómo funciona Gulp

Aquí, por ejemplo, hay una tarea de muestra que utiliza tres de estas cuatro funciones:

 gulp.task('my-first-task', function() { gulp.src('/public/js/**/*.js') .pipe(concat()) .pipe(minify()) .pipe(gulp.dest('build')) });

Cuando se realiza my-first-task , todos los archivos que coinciden con el patrón /public/js/**/*.js se minimizan y luego se transfieren a una carpeta de build .

La belleza de esto está en el encadenamiento .pipe() . Toma un conjunto de archivos de entrada, los canaliza a través de una serie de transformaciones y luego devuelve los archivos de salida. Para hacer las cosas aún más convenientes, las transformaciones de canalización reales, como minify() , a menudo las realizan las bibliotecas NPM. Como resultado, es muy raro en la práctica que necesite escribir sus propias transformaciones más allá de cambiar el nombre de los archivos en la canalización.

El siguiente paso para comprender Gulp es comprender la variedad de dependencias de tareas.

 gulp.task('my-second-task', ['lint', 'bundle'], function() { ... });

Aquí, my-second-task solo ejecuta la función de devolución de llamada después de que se completan las tareas de lint y bundle . Esto permite la separación de preocupaciones: crea una serie de tareas pequeñas con una sola responsabilidad, como convertir LESS a CSS , y crea una especie de tarea maestra que simplemente llama a todas las demás tareas a través de la matriz de dependencias de tareas.

Finalmente, tenemos gulp.watch , que observa un patrón de archivo global en busca de cambios y, cuando se detecta un cambio, ejecuta una serie de tareas.

 gulp.task('my-third-task', function() { gulp.watch('/public/js/**/*.js', ['lint', 'reload']) })

En el ejemplo anterior, cualquier cambio en un archivo que coincida con /public/js/**/*.js la tarea de lint y reload . Un uso común de gulp.watch es activar recargas en vivo en el navegador, una característica tan útil para el desarrollo que no podrá vivir sin ella una vez que la haya experimentado.

Y así, entiendes todo lo que realmente necesitas saber sobre gulp .

¿Dónde encaja Webpack?

Cómo funciona el paquete web

Al usar el patrón CommonJS, agrupar archivos JavaScript no es tan simple como concatenarlos. Más bien, tiene un punto de entrada (generalmente llamado index.js o app.js ) con una serie de instrucciones de import o require en la parte superior del archivo:

ES5

 var Component1 = require('./components/Component1'); var Component2 = require('./components/Component2');

ES6

 import Component1 from './components/Component1'; import Component2 from './components/Component2';

Las dependencias deben resolverse antes que el código restante en app.js , y esas dependencias pueden tener más dependencias para resolver. Además, es posible que require la misma dependencia en varios lugares de su aplicación, pero solo desea resolver esa dependencia una vez. Como puede imaginar, una vez que tiene un árbol de dependencias de varios niveles de profundidad, el proceso de agrupar su JavaScript se vuelve bastante complejo. Aquí es donde entran los paquetes como Browserify y Webpack.

¿Por qué los desarrolladores usan Webpack en lugar de Gulp?

Webpack es un paquete, mientras que Gulp es un ejecutor de tareas, por lo que esperaría ver estas dos herramientas comúnmente utilizadas juntas. En cambio, existe una tendencia creciente, especialmente entre la comunidad React, de usar Webpack en lugar de Gulp. ¿Por qué es esto?

En pocas palabras, Webpack es una herramienta tan poderosa que ya puede realizar la gran mayoría de las tareas que de otro modo haría a través de un ejecutor de tareas. Por ejemplo, Webpack ya ofrece opciones de minificación y mapas fuente para su paquete. Además, Webpack se puede ejecutar como middleware a través de un servidor personalizado llamado webpack-dev-server , que admite tanto la recarga en vivo como la recarga en caliente (hablaremos de estas funciones más adelante). Mediante el uso de cargadores, también puede agregar transpilación de ES6 a ES5 y preprocesadores y posprocesadores de CSS. Eso realmente deja las pruebas unitarias y el linting como tareas principales que Webpack no puede manejar de forma independiente. Dado que hemos reducido al menos media docena de posibles tareas de trago a dos, muchos desarrolladores optan por usar scripts NPM directamente, ya que esto evita la sobrecarga de agregar Gulp al proyecto (de lo que también hablaremos más adelante) .

El principal inconveniente de usar Webpack es que es bastante difícil de configurar, lo cual no es atractivo si está tratando de poner en marcha un proyecto rápidamente.

Nuestras 3 configuraciones de Task Runner

Configuraré un proyecto con tres configuraciones diferentes del ejecutor de tareas. Cada configuración realizará las siguientes tareas:

  • Configure un servidor de desarrollo con recarga en vivo en los cambios de archivos observados
  • Agrupe nuestros archivos JS y CSS (incluida la transpilación de ES6 a ES5, la conversión de SASS a CSS y los mapas de origen) de manera escalable en los cambios de archivos observados
  • Ejecute pruebas unitarias como una tarea independiente o en modo de observación
  • Ejecute linting como una tarea independiente o en modo reloj
  • Proporcione la capacidad de ejecutar todo lo anterior a través de un solo comando en la terminal
  • Tenga otro comando para crear un paquete de producción con minificación y otras optimizaciones

Nuestras tres configuraciones serán:

  • Gulp + Navegador
  • trago + paquete web
  • Webpack + secuencias de comandos NPM

La aplicación utilizará React para el front-end. Originalmente, quería usar un enfoque independiente del marco, pero usar React en realidad simplifica las responsabilidades del ejecutor de tareas, ya que solo se necesita un archivo HTML y React funciona muy bien con el patrón CommonJS.

Cubriremos los beneficios y los inconvenientes de cada configuración para que pueda tomar una decisión informada sobre qué tipo de configuración se adapta mejor a las necesidades de su proyecto.

Configuré un repositorio de Git con tres ramas, una para cada enfoque (enlace). Probar cada configuración es tan simple como:

 git checkout <branch name> npm prune (optional) npm install gulp (or npm start, depending on the setup)

Examinemos el código en cada rama en detalle...

Código común

Estructura de carpetas

 - app - components - fonts - styles - index.html - index.js - index.test.js - routes.js

índice.html

Un archivo HTML sencillo. La aplicación React se carga en <div></div> y solo usamos un único archivo JS y CSS incluido. De hecho, en nuestra configuración de desarrollo de Webpack, ni siquiera necesitaremos bundle.css .

índice.js

Esto actúa como el punto de entrada JS de nuestra aplicación. Esencialmente, solo estamos cargando React Router en la app div with id que mencionamos anteriormente.

rutas.js

Este archivo define nuestras rutas. Las direcciones URL / , /about y /contact se asignan a los componentes HomePage , AboutPage y ContactPage , respectivamente.

índice.prueba.js

Esta es una serie de pruebas unitarias que prueban el comportamiento nativo de JavaScript. En una aplicación de calidad de producción real, escribiría una prueba unitaria por componente de React (al menos los que manipulan el estado), probando el comportamiento específico de React. Sin embargo, para los propósitos de esta publicación, es suficiente simplemente tener una prueba de unidad funcional que pueda ejecutarse en modo reloj.

componentes/App.js

Esto se puede considerar como el contenedor de todas las vistas de nuestra página. Cada página contiene un componente <Header/> así como this.props.children , que evalúa la vista de la página en sí (por ejemplo, ContactPage si está en /contact en el navegador).

componentes/inicio/HomePage.js

Esta es nuestra vista de casa. Opté por usar react-bootstrap ya que el sistema de cuadrícula de bootstrap es excelente para crear páginas receptivas. Con el uso adecuado de bootstrap, la cantidad de consultas de medios que debe escribir para ventanas de visualización más pequeñas se reduce drásticamente.

Los componentes restantes ( Header , AboutPage , ContactPage ) están estructurados de manera similar (marcado react-bootstrap , sin manipulación de estado).

Ahora hablemos más sobre el estilo.

Enfoque de estilo CSS

Mi enfoque preferido para aplicar estilo a los componentes de React es tener una hoja de estilo por componente, cuyos estilos se apliquen solo a ese componente específico. Notarás que en cada uno de mis componentes de React, el div de nivel superior tiene un nombre de clase que coincide con el nombre del componente. Entonces, por ejemplo, HomePage.js tiene su marcado envuelto por:

 <div className="HomePage"> ... </div>

También hay un archivo HomePage.scss asociado que está estructurado de la siguiente manera:

 @import '../../styles/variables'; .HomePage { // Content here }

¿Por qué es tan útil este enfoque? Da como resultado un CSS altamente modular, eliminando en gran medida el problema del comportamiento en cascada no deseado.

Supongamos que tenemos dos componentes React, Component1 y Component2 . En ambos casos, queremos anular el tamaño de fuente h2 .

 /* Component1.scss */ .Component1 { h2 { font-size: 30px; } } /* Component2.scss */ .Component2 { h2 { font-size: 60px; } }

El tamaño de fuente h2 de Component1 y Component2 es independiente si los componentes son adyacentes o si un componente está anidado dentro del otro. Idealmente, esto significa que el estilo de un componente es completamente autónomo, lo que significa que el componente se verá exactamente igual sin importar dónde se coloque en el marcado. En realidad, no siempre es tan simple, pero ciertamente es un gran paso en la dirección correcta.

Además de los estilos por componente, me gusta tener una carpeta de styles que contenga una hoja de estilo global global.scss , junto con parciales SASS que manejen una responsabilidad específica (en este caso, _fonts.scss y _variables.scss para fuentes y variables, respectivamente). ). La hoja de estilo global nos permite definir la apariencia general de toda la aplicación, mientras que las hojas de estilo por componente pueden importar los parciales auxiliares según sea necesario.

Ahora que el código común en cada rama se ha explorado en profundidad, cambiemos nuestro enfoque al primer enfoque del ejecutor de tareas/agrupador.

Configuración de Gulp + Browserify

gulpfile.js

Esto resulta en un archivo gulp sorprendentemente grande, con 22 importaciones y 150 líneas de código. Entonces, en aras de la brevedad, solo revisaré en detalle las tareas js , css , server , watch y default .

Paquete JS

 // Browserify specific configuration const b = browserify({ entries: [config.paths.entry], debug: true, plugin: PROD ? [] : [hmr, watchify], cache: {}, packageCache: {} }) .transform('babelify'); b.on('update', bundle); b.on('log', gutil.log); (...) gulp.task('js', bundle); (...) // Bundles our JS using Browserify. Sourcemaps are used in development, while minification is used in production. function bundle() { return b.bundle() .on('error', gutil.log.bind(gutil, 'Browserify Error')) .pipe(source('bundle.js')) .pipe(buffer()) .pipe(cond(PROD, minifyJS())) .pipe(cond(!PROD, sourcemaps.init({loadMaps: true}))) .pipe(cond(!PROD, sourcemaps.write())) .pipe(gulp.dest(config.paths.baseDir)); }

Este enfoque es bastante feo por varias razones. Por un lado, la tarea se divide en tres partes separadas. Primero, crea su objeto de paquete Browserify b , pasando algunas opciones y definiendo algunos controladores de eventos. Luego tiene la tarea Gulp en sí, que tiene que pasar una función con nombre como devolución de llamada en lugar de incluirla (ya que b.on('update') usa esa misma devolución de llamada). Esto apenas tiene la elegancia de una tarea de Gulp en la que simplemente pasa un gulp.src y canaliza algunos cambios.

Otro problema es que esto nos obliga a tener diferentes enfoques para recargar html , css y js en el navegador. Mirando nuestra tarea de watch Gulp:

 gulp.task('watch', () => { livereload.listen({basePath: 'dist'}); gulp.watch(config.paths.html, ['html']); gulp.watch(config.paths.css, ['css']); gulp.watch(config.paths.js, () => { runSequence('lint', 'test'); }); });

Cuando se modifica un archivo HTML, la tarea html se vuelve a ejecutar.

 gulp.task('html', () => { return gulp.src(config.paths.html) .pipe(gulp.dest(config.paths.baseDir)) .pipe(cond(!PROD, livereload())); });

La última canalización llama a livereload() si NODE_ENV no es production , lo que activa una actualización en el navegador.

La misma lógica se utiliza para el reloj CSS. Cuando se cambia un archivo CSS, la tarea css se vuelve a ejecutar y la última tubería en la tarea css activa livereload() y actualiza el navegador.

Sin embargo, el reloj js no llama a la tarea js en absoluto. En cambio, el controlador de eventos de b.on('update', bundle) maneja la recarga utilizando un enfoque completamente diferente (es decir, reemplazo de módulo activo). La inconsistencia en este enfoque es irritante, pero desafortunadamente necesaria para tener compilaciones incrementales . Si ingenuamente llamamos a livereload() al final de la función de bundle , esto reconstruiría todo el paquete JS en cualquier cambio de archivo JS individual. Tal enfoque obviamente no escala. Cuantos más archivos JS tenga, más tardará cada reagrupación. De repente, sus reagrupaciones de 500 ms comienzan a tomar 30 segundos, lo que realmente inhibe el desarrollo ágil.

Paquete CSS

 gulp.task('css', () => { return gulp.src( [ 'node_modules/bootstrap/dist/css/bootstrap.css', 'node_modules/font-awesome/css/font-awesome.css', config.paths.css ] ) .pipe(cond(!PROD, sourcemaps.init())) .pipe(sass().on('error', sass.logError)) .pipe(concat('bundle.css')) .pipe(cond(PROD, minifyCSS())) .pipe(cond(!PROD, sourcemaps.write())) .pipe(gulp.dest(config.paths.baseDir)) .pipe(cond(!PROD, livereload())); });

El primer problema aquí es la engorrosa inclusión de CSS del proveedor. Cada vez que se agrega un nuevo archivo CSS de proveedor al proyecto, debemos recordar cambiar nuestro archivo gulp para agregar un elemento a la matriz gulp.src , en lugar de agregar la importación en un lugar relevante en nuestro código fuente real.

El otro problema principal es la lógica intrincada en cada tubería. Tuve que agregar una biblioteca NPM llamada gulp-cond solo para configurar la lógica condicional en mis canalizaciones, y el resultado final no es demasiado legible (¡corchetes triples en todas partes!).

Tarea del servidor

 gulp.task('server', () => { nodemon({ script: 'server.js' }); });

Esta tarea es muy sencilla. Es esencialmente un contenedor alrededor de la invocación de la línea de comando nodemon server.js , que ejecuta server.js en un entorno de nodo. nodemon se usa en lugar de node para que cualquier cambio en el archivo haga que se reinicie. De forma predeterminada, nodemon reiniciaría el proceso en ejecución en cualquier cambio de archivo JS, por lo que es importante incluir un archivo nodemon.json para limitar su alcance:

 { "watch": "server.js" }

Revisemos nuestro código de servidor.

servidor.js

 const baseDir = process.env.NODE_ENV === 'production' ? 'build' : 'dist'; const port = process.env.NODE_ENV === 'production' ? 8080: 3000; const app = express();

Esto establece el directorio base del servidor y el puerto según el entorno del nodo y crea una instancia de express.

 app.use(require('connect-livereload')({port: 35729})); app.use(express.static(path.join(__dirname, baseDir)));

Esto agrega middleware connect-livereload (necesario para nuestra configuración de recarga en vivo) y middleware estático (necesario para manejar nuestros activos estáticos).

 app.get('/api/sample-route', (req, res) => { res.send({ website: 'Toptal', blogPost: true }); });

Esta es solo una ruta API simple. Si navega a localhost:3000/api/sample-route en el navegador, verá:

 { website: "Toptal", blogPost: true }

En un backend real, tendría una carpeta completa dedicada a las rutas API, archivos separados para establecer conexiones DB, etc. Esta ruta de muestra simplemente se incluyó para mostrar que podemos construir fácilmente un backend sobre el frontend que hemos configurado.

 app.get('*', (req, res) => { res.sendFile(path.join(__dirname, './', baseDir ,'/index.html')); });

Esta es una ruta general, lo que significa que no importa qué URL ingrese en el navegador, el servidor devolverá nuestra única página index.html . Entonces es responsabilidad del React Router resolver nuestras rutas en el lado del cliente.

 app.listen(port, () => { open(`http://localhost:${port}`); });

Esto le dice a nuestra instancia express que escuche el puerto que especificamos y abra el navegador en una nueva pestaña en la URL especificada.

Hasta ahora, lo único que no me gusta de la configuración del servidor es:

 app.use(require('connect-livereload')({port: 35729}));

Dado que ya estamos usando gulp-livereload en nuestro archivo gulp, esto crea dos lugares separados donde se debe usar livereload.

Ahora, por último pero no menos importante:

Tarea predeterminada

 gulp.task('default', (cb) => { runSequence('clean', 'lint', 'test', 'html', 'css', 'js', 'fonts', 'server', 'watch', cb); });

Esta es la tarea que se ejecuta simplemente escribiendo gulp en la terminal. Una rareza es la necesidad de usar runSequence para que las tareas se ejecuten secuencialmente. Normalmente, una serie de tareas se ejecutan en paralelo, pero este no siempre es el comportamiento deseado. Por ejemplo, necesitamos que la tarea de clean se ejecute antes que html para asegurarnos de que nuestras carpetas de destino estén vacías antes de mover archivos a ellas. Cuando se lance gulp 4, admitirá los métodos gulp.series y gulp.parallel forma nativa, pero por ahora tenemos que dejar esta pequeña peculiaridad en nuestra configuración.

Más allá de eso, esto es bastante elegante. Toda la creación y el alojamiento de nuestra aplicación se realizan con un solo comando, y comprender cualquier parte del flujo de trabajo es tan simple como examinar una tarea individual en la secuencia de ejecución. Además, podemos dividir toda la secuencia en partes más pequeñas para lograr un enfoque más granular para crear y alojar la aplicación. Por ejemplo, podríamos configurar una tarea separada llamada validate que ejecute las tareas de test y lint . O podríamos tener una tarea de host que ejecute el server y watch . Esta capacidad de orquestar tareas es muy poderosa, especialmente a medida que su aplicación escala y requiere tareas más automatizadas.

Compilaciones de desarrollo vs producción

 if (argv.prod) { process.env.NODE_ENV = 'production'; } let PROD = process.env.NODE_ENV === 'production';

Usando la biblioteca NPM de yargs , podemos proporcionar indicadores de línea de comando a Gulp. Aquí instruyo al archivo gulp para que establezca el entorno del nodo en producción si --prod se pasa a gulp en la terminal. Nuestra variable PROD se usa luego como condicional para diferenciar el comportamiento de desarrollo y producción en nuestro archivo gulp. Por ejemplo, una de las opciones que pasamos a nuestra configuración browserify es:

 plugin: PROD ? [] : [hmr, watchify]

Esto le dice a browserify que no use ningún complemento en el modo de producción, y que use hmr y watchify en otros entornos.

Este condicional PROD es muy útil porque nos evita tener que escribir un archivo gulp separado para producción y desarrollo, que finalmente contendría una gran cantidad de repetición de código. En cambio, podemos hacer cosas como gulp --prod para ejecutar la tarea predeterminada en producción, o gulp html --prod para ejecutar solo la tarea html en producción. Por otro lado, vimos anteriormente que ensuciar nuestras canalizaciones Gulp con declaraciones como .pipe(cond(!PROD, livereload())) no son las más legibles. Al final, es una cuestión de preferencia si desea utilizar el enfoque de variable booleana o configurar dos archivos gulp separados.

Ahora veamos qué sucede cuando seguimos usando Gulp como nuestro ejecutor de tareas pero reemplazamos Browserify con Webpack.

Configuración de Gulp + Webpack

De repente, nuestro archivo gulp tiene solo 99 líneas con 12 importaciones, ¡una gran reducción con respecto a nuestra configuración anterior! Si marcamos la tarea por defecto:

 gulp.task('default', (cb) => { runSequence('lint', 'test', 'build', 'server', 'watch', cb); });

Ahora, la configuración completa de nuestra aplicación web solo requiere cinco tareas en lugar de nueve, una mejora espectacular.

Además, hemos eliminado la necesidad de livereload . Nuestra tarea de watch ahora es simplemente:

 gulp.task('watch', () => { gulp.watch(config.paths.js, () => { runSequence('lint', 'test'); }); });

Esto significa que nuestro observador de tragos no activa ningún tipo de comportamiento de reagrupación. Como beneficio adicional, ya no necesitamos transferir index.html de la app a dist o build .

Volviendo nuestro enfoque a la reducción de tareas, nuestras tareas de html , css , js y fonts han sido reemplazadas por una única tarea de build :

 gulp.task('build', () => { runSequence('clean', 'html'); return gulp.src(config.paths.entry) .pipe(webpack(require('./webpack.config'))) .pipe(gulp.dest(config.paths.baseDir)); });

Suficientemente simple. Ejecute las tareas de clean y html en secuencia. Una vez que estén completos, toma nuestro punto de entrada, canalízalo a través de Webpack, pasa un archivo webpack.config.js para configurarlo y envía el paquete resultante a nuestro baseDir (ya sea dist o build , según el nodo env).

Echemos un vistazo al archivo de configuración de Webpack:

webpack.config.js

Este es un archivo de configuración bastante grande e intimidante, así que expliquemos algunas de las propiedades importantes que se establecen en nuestro objeto module.exports .

 devtool: PROD ? 'source-map' : 'eval-source-map',

Esto establece el tipo de mapas de origen que utilizará Webpack. Webpack no solo admite mapas de origen listos para usar, sino que también admite una amplia gama de opciones de mapas de origen. Cada opción proporciona un equilibrio diferente entre los detalles del mapa de origen y la velocidad de reconstrucción (el tiempo que se tarda en volver a agrupar los cambios). Esto significa que podemos usar una opción de mapa fuente "barata" para el desarrollo para lograr recargas rápidas y una opción de mapa fuente más costosa en producción.

 entry: PROD ? './app/index' : [ 'webpack-hot-middleware/client?reload=true', // reloads the page if hot module reloading fails. './app/index' ]

Este es nuestro punto de entrada del paquete. Observe que se pasa una matriz, lo que significa que es posible tener varios puntos de entrada. En este caso, tenemos nuestro punto de entrada esperado app/index.js , así como el punto de entrada webpack-hot-middleware que se usa como parte de nuestra configuración de recarga de módulos activos.

 output: { path: PROD ? __dirname + '/build' : __dirname + '/dist', publicPath: '/', filename: 'bundle.js' },

Aquí es donde se generará el paquete compilado. La opción más confusa es publicPath . Establece la URL base donde se alojará su paquete en el servidor. Entonces, por ejemplo, si su publicPath es /public/assets , el paquete aparecerá en /public/assets/bundle.js en el servidor.

 devServer: { contentBase: PROD ? './build' : './app' }

Esto le dice al servidor qué carpeta en su proyecto usar como directorio raíz del servidor.

Si alguna vez se confunde acerca de cómo Webpack asigna el paquete creado en su proyecto al paquete en el servidor, simplemente recuerde lo siguiente:

  • path + nombre de filename : la ubicación exacta del paquete en el código fuente de su proyecto
  • contentBase (como raíz, / ) + publicPath : la ubicación del paquete en el servidor
 plugins: PROD ? [ new webpack.optimize.OccurenceOrderPlugin(), new webpack.DefinePlugin(GLOBALS), new ExtractTextPlugin('bundle.css'), new webpack.optimize.DedupePlugin(), new webpack.optimize.UglifyJsPlugin({compress: {warnings: false}}) ] : [ new webpack.HotModuleReplacementPlugin(), new webpack.NoErrorsPlugin() ],

Estos son complementos que mejoran la funcionalidad de Webpack de alguna manera. Por ejemplo, webpack.optimize.UglifyJsPlugin es responsable de la minificación.

 loaders: [ {test: /\.js$/, include: path.join(__dirname, 'app'), loaders: ['babel']}, { test: /\.css$/, loader: PROD ? ExtractTextPlugin.extract('style', 'css?sourceMap'): 'style!css?sourceMap' }, { test: /\.scss$/, loader: PROD ? ExtractTextPlugin.extract('style', 'css?sourceMap!resolve-url!sass?sourceMap') : 'style!css?sourceMap!resolve-url!sass?sourceMap' }, {test: /\.(svg|png|jpe?g|gif)(\?\S*)?$/, loader: 'url?limit=100000&name=img/[name].[ext]'}, {test: /\.(eot|woff|woff2|ttf)(\?\S*)?$/, loader: 'url?limit=100000&name=fonts/[name].[ext]'} ]

Estos son cargadores. Esencialmente, procesan previamente los archivos que se cargan a través de declaraciones require() . Son algo similares a las tuberías Gulp en el sentido de que puede encadenar cargadores juntos.

Examinemos uno de nuestros objetos del cargador:

 {test: /\.scss$/, loader: 'style!css?sourceMap!resolve-url!sass?sourceMap'}

La propiedad de test le dice a Webpack que el cargador dado se aplica si un archivo coincide con el patrón de expresión regular provisto, en este caso /\.scss$/ . La propiedad del loader corresponde a la acción que realiza el cargador. Aquí estamos encadenando los cargadores style , css , resolve-url y sass , que se ejecutan en orden inverso.

Debo admitir que no encuentro la sintaxis de loader3!loader2!loader1 muy elegante. Después de todo, ¿cuándo tienes que leer algo en un programa de derecha a izquierda? A pesar de esto, los cargadores son una característica muy poderosa de webpack. De hecho, el cargador que acabo de mencionar nos permite importar archivos SASS directamente a nuestro JavaScript. Por ejemplo, podemos importar nuestras hojas de estilo globales y de proveedor en nuestro archivo de punto de entrada:

índice.js

 import React from 'react'; import {render} from 'react-dom'; import {Router, browserHistory} from 'react-router'; import routes from './routes'; // CSS imports import '../node_modules/bootstrap/dist/css/bootstrap.css'; import '../node_modules/font-awesome/css/font-awesome.css'; import './styles/global.scss'; render(<Router history={browserHistory} routes={routes} />, document.getElementById('app'));

De manera similar, en nuestro componente de encabezado podemos agregar import './Header.scss' para importar la hoja de estilo asociada del componente. Esto también se aplica a todos nuestros otros componentes.

En mi opinión, esto casi puede considerarse un cambio revolucionario en el mundo del desarrollo de JavaScript. No hay necesidad de preocuparse por la agrupación de CSS, la minificación o los mapas fuente, ya que nuestro cargador maneja todo esto por nosotros. Incluso la recarga de módulos en caliente funciona para nuestros archivos CSS. Luego, poder manejar las importaciones de JS y CSS en el mismo archivo hace que el desarrollo sea conceptualmente más simple: más coherencia, menos cambios de contexto y más fácil razonar.

Para dar un breve resumen de cómo funciona esta función: Webpack inserta el CSS en nuestro paquete JS. De hecho, Webpack también puede hacer esto con imágenes y fuentes:

 {test: /\.(svg|png|jpe?g|gif)(\?\S*)?$/, loader: 'url?limit=100000&name=img/[name].[ext]'}, {test: /\.(eot|woff|woff2|ttf)(\?\S*)?$/, loader: 'url?limit=100000&name=fonts/[name].[ext]'}

El cargador de URL le indica a Webpack que alinee nuestras imágenes y fuentes como direcciones URL de datos si tienen menos de 100 KB; de lo contrario, las servirá como archivos separados. Por supuesto, también podemos configurar el tamaño de corte a un valor diferente, como 10 KB.

Y esa es la configuración de Webpack en pocas palabras. Admito que hay una buena cantidad de configuración, pero los beneficios de usarlo son simplemente fenomenales. Aunque Browserify tiene complementos y transformaciones, simplemente no se pueden comparar con los cargadores de paquetes web en términos de funcionalidad adicional.

Configuración de Webpack + NPM Scripts

En esta configuración, usamos scripts npm directamente en lugar de depender de un archivo gulp para automatizar nuestras tareas.

paquete.json

 "scripts": { "start": "npm-run-all --parallel lint:watch test:watch build", "start:prod": "npm-run-all --parallel lint test build:prod", "clean-dist": "rimraf ./dist && mkdir dist", "clean-build": "rimraf ./build && mkdir build", "clean": "npm-run-all clean-dist clean-build", "test": "mocha ./app/**/*.test.js --compilers js:babel-core/register", "test:watch": "npm run test -- --watch", "lint": "esw ./app/**/*.js", "lint:watch": "npm run lint -- --watch", "server": "nodemon server.js", "server:prod": "cross-env NODE_ENV=production nodemon server.js", "build-html": "node tools/buildHtml.js", "build-html:prod": "cross-env NODE_ENV=production node tools/buildHtml.js", "prebuild": "npm-run-all clean-dist build-html", "build": "webpack", "postbuild": "npm run server", "prebuild:prod": "npm-run-all clean-build build-html:prod", "build:prod": "cross-env NODE_ENV=production webpack", "postbuild:prod": "npm run server:prod" }

Para ejecutar compilaciones de desarrollo y producción, ingrese npm start y npm run start:prod , respectivamente.

Esto es ciertamente más compacto que nuestro archivo gulp, dado que hemos reducido de 99 a 150 líneas de código a 19 scripts NPM, o 12 si excluimos los scripts de producción (la mayoría de los cuales solo reflejan los scripts de desarrollo con el entorno del nodo establecido en producción ). El inconveniente es que estos comandos son algo crípticos en comparación con nuestras contrapartes de tareas Gulp, y no tan expresivos. Por ejemplo, no hay forma (al menos que yo sepa) de que un solo script npm ejecute ciertos comandos en serie y otros en paralelo. Es uno o el otro.

Sin embargo, hay una gran ventaja en este enfoque. Al usar bibliotecas NPM como mocha directamente desde la línea de comando, no necesita instalar un contenedor Gulp equivalente para cada una (en este caso, gulp-mocha ).

En lugar de instalar NPM

  • trago-eslint
  • trago moka
  • trago-nodemon
  • etc.

Instalamos los siguientes paquetes:

  • eslint
  • moca
  • nodemonio
  • etc.

Citando la publicación de Cory House, Why I Left Gulp and Grunt for NPM Scripts :

Yo era un gran fan de Gulp. Pero en mi último proyecto, terminé con cientos de líneas en mi archivo gulp y alrededor de una docena de complementos de Gulp. Estaba luchando por integrar Webpack, Browsersync, hot reloading, Mocha y mucho más usando Gulp. ¿Por qué? Bueno, algunos complementos tenían documentación insuficiente para mi caso de uso. Algunos complementos solo expusieron parte de la API que necesitaba. Uno tenía un error extraño en el que solo veía una pequeña cantidad de archivos. Otros colores eliminados al enviar a la línea de comando.

Él especifica tres problemas centrales con Gulp:

  1. Dependencia de los autores de complementos
  2. Frustrante para depurar
  3. Documentación inconexa

Yo tendería a estar de acuerdo con todo esto.

1. Dependencia de los autores de complementos

Cada vez que se actualiza una biblioteca como eslint , la biblioteca gulp-eslint asociada necesita una actualización correspondiente. Si el mantenedor de la biblioteca pierde interés, la versión trago de la biblioteca pierde la sincronización con la nativa. Lo mismo ocurre cuando se crea una nueva biblioteca. Si alguien crea una biblioteca xyz y se pone de moda, de repente necesita una biblioteca gulp-xyz correspondiente para usarla en sus tareas de gulp.

En cierto sentido, este enfoque simplemente no escala. Idealmente, nos gustaría un enfoque como Gulp que pueda usar las bibliotecas nativas.

2. Frustrante para depurar

Aunque las bibliotecas como gulp-plumber ayudan a aliviar este problema considerablemente, es cierto que el informe de errores en gulp no es muy útil. Si incluso una canalización arroja una excepción no controlada, obtiene un seguimiento de la pila para un problema que parece completamente ajeno a lo que está causando el problema en su código fuente. Esto puede hacer que la depuración sea una pesadilla en algunos casos. Ninguna cantidad de búsquedas en Google o Stack Overflow realmente puede ayudarlo si el error es lo suficientemente críptico o engañoso.

3. Documentación Desarticulada

A menudo encuentro que las bibliotecas pequeñas de gulp tienden a tener una documentación muy limitada. Sospecho que esto se debe a que el autor generalmente crea la biblioteca principalmente para su propio uso. Además, es común tener que consultar la documentación tanto del complemento Gulp como de la propia biblioteca nativa, lo que significa muchos cambios de contexto y el doble de lectura.

Conclusión

Me parece bastante claro que Webpack es preferible a Browserify y los scripts NPM son preferibles a Gulp, aunque cada opción tiene sus ventajas y desventajas. Gulp es ciertamente más expresivo y conveniente de usar que los scripts de NPM, pero paga el precio en toda la abstracción adicional.

No todas las combinaciones pueden ser perfectas para su aplicación, pero si desea evitar una cantidad abrumadora de dependencias de desarrollo y una experiencia de depuración frustrante, Webpack con scripts NPM es el camino a seguir. Espero que este artículo le resulte útil para elegir las herramientas adecuadas para su próximo proyecto.

Relacionados:
  • Mantener el control: una guía para Webpack y React, pt. 1
  • Gulp Under the Hood: creación de una herramienta de automatización de tareas basada en secuencias