Una guía para administrar dependencias de paquetes web
Publicado: 2022-03-11El concepto de modularización es una parte inherente de la mayoría de los lenguajes de programación modernos. Sin embargo, JavaScript ha carecido de un enfoque formal para la modularización hasta la llegada de la última versión de ECMAScript ES6.
En Node.js, uno de los marcos JavaScript más populares de la actualidad, los paquetes de módulos permiten cargar módulos NPM en navegadores web y las bibliotecas orientadas a componentes (como React) fomentan y facilitan la modularización del código JavaScript.
Webpack es uno de los paquetes de módulos disponibles que procesa el código JavaScript, así como todos los activos estáticos, como hojas de estilo, imágenes y fuentes, en un archivo incluido. El procesamiento puede incluir todas las tareas necesarias para administrar y optimizar las dependencias del código, como compilación, concatenación, minificación y compresión.
Sin embargo, configurar Webpack y sus dependencias puede ser estresante y no siempre es un proceso sencillo, especialmente para principiantes.
Esta publicación de blog proporciona pautas, con ejemplos, sobre cómo configurar Webpack para diferentes escenarios y señala los errores más comunes relacionados con la agrupación de dependencias de proyectos mediante Webpack.
La primera parte de esta publicación de blog explica cómo simplificar la definición de dependencias en un proyecto. A continuación, analizamos y demostramos la configuración para la división de código de aplicaciones de una sola página y múltiples. Finalmente, discutimos cómo configurar Webpack, si queremos incluir bibliotecas de terceros en nuestro proyecto.
Configuración de alias y rutas relativas
Las rutas relativas no están directamente relacionadas con las dependencias, pero las usamos cuando definimos dependencias. Si la estructura de un archivo de proyecto es compleja, puede ser difícil resolver las rutas de los módulos relevantes. Uno de los beneficios más fundamentales de la configuración de Webpack es que ayuda a simplificar la definición de rutas relativas en un proyecto.
Digamos que tenemos la siguiente estructura de proyecto:
- Project - node_modules - bower_modules - src - script - components - Modal.js - Navigation.js - containers - Home.js - Admin.js
Podemos hacer referencia a las dependencias mediante rutas relativas a los archivos que necesitamos, y si queremos importar componentes a contenedores en nuestro código fuente, se ve así:
Home.js
Import Modal from '../components/Modal'; Import Navigation from '../components/Navigation';
Modal.js
import {datepicker} from '../../../../bower_modules/datepicker/dist/js/datepicker';
Cada vez que queramos importar un script o un módulo, necesitamos saber la ubicación del directorio actual y encontrar la ruta relativa a lo que queremos importar. Podemos imaginar cómo este problema puede aumentar en complejidad si tenemos un gran proyecto con una estructura de archivos anidada, o si queremos refactorizar algunas partes de una estructura de proyecto compleja.
Podemos manejar este problema fácilmente con la opción resolve.alias de resolve.alias
. Podemos declarar los llamados alias: nombre de un directorio o módulo con su ubicación, y no confiamos en rutas relativas en el código fuente del proyecto.
webpack.config.js
resolve: { alias: { 'node_modules': path.join(__dirname, 'node_modules'), 'bower_modules': path.join(__dirname, 'bower_modules'), } }
En el archivo Modal.js
, ahora podemos importar datepicker mucho más simple:
import {datepicker} from 'bower_modules/datepicker/dist/js/datepicker';
División de código
Podemos tener escenarios en los que necesitemos agregar un script en el paquete final, o dividir el paquete final, o queramos cargar paquetes separados a pedido. Configurar nuestro proyecto y la configuración de Webpack para estos escenarios puede no ser sencillo.
En la configuración de Webpack, la opción Entry
le dice a Webpack dónde está el punto de partida para el paquete final. Un punto de entrada puede tener tres tipos de datos diferentes: cadena, matriz u objeto.
Si tenemos un solo punto de partida, podemos usar cualquiera de estos formatos y obtener el mismo resultado.
Si queremos agregar varios archivos y no dependen unos de otros, podemos usar un formato de matriz. Por ejemplo, podemos agregar analytics.js
al final de bundle.js
:
webpack.config.js
module.exports = { // creates a bundle out of index.js and then append analytics.js entry: ['./src/script/index.jsx', './src/script/analytics.js'], output: { path: './build', filename: bundle.js ' } };
Gestión de múltiples puntos de entrada
Digamos que tenemos una aplicación de varias páginas con varios archivos HTML, como index.html
y admin.html
. Podemos generar múltiples paquetes usando el punto de entrada como un tipo de objeto. La siguiente configuración genera dos paquetes de JavaScript:
webpack.config.js
module.exports = { entry: { index: './src/script/index.jsx', admin: './src/script/admin.jsx' }, output: { path: './build', filename: '[name].js' // template based on keys in entry above (index.js & admin.js) } };
index.html
<script src=”build/index.js”></script>
admin.html
<script src=”build/admin.js”></script>
Ambos paquetes de JavaScript pueden compartir bibliotecas y componentes comunes. Para eso, podemos usar CommonsChunkPlugin
, que encuentra módulos que ocurren en múltiples fragmentos de entrada y crea un paquete compartido que se puede almacenar en caché entre varias páginas.
webpack.config.js
var commonsPlugin = new webpack.optimize.CommonsChunkPlugin('common.js'); module.exports = { entry: { index: './src/script/index.jsx', admin: './src/script/admin.jsx' }, output: { path: './build', filename: '[name].js' // template based on keys in entry above (index.js & admin.js) }, plugins: [commonsPlugin] };
Ahora, no debemos olvidar agregar <script src="build/common.js"></script>
antes de los scripts incluidos.
Habilitación de la carga diferida
Webpack puede dividir activos estáticos en partes más pequeñas y este enfoque es más flexible que la concatenación estándar. Si tenemos una aplicación de una sola página (SPA) grande, la concatenación simple en un paquete no es un buen enfoque porque cargar un paquete enorme puede ser lento y los usuarios generalmente no necesitan todas las dependencias en cada vista.
Anteriormente explicamos cómo dividir una aplicación en varios paquetes, concatenar dependencias comunes y beneficiarse del comportamiento de almacenamiento en caché del navegador. Este enfoque funciona muy bien para aplicaciones de varias páginas, pero no para aplicaciones de una sola página.
Para el SPA, solo debemos proporcionar los activos estáticos que se requieren para representar la vista actual. El enrutador del lado del cliente en la arquitectura SPA es un lugar perfecto para manejar la división de código. Cuando el usuario ingresa una ruta, podemos cargar solo las dependencias necesarias para la vista resultante. Alternativamente, podemos cargar dependencias a medida que el usuario se desplaza hacia abajo en una página.
Para ello, podemos utilizar las funciones require.ensure
o System.import
, que Webpack puede detectar de forma estática. Webpack puede generar un paquete separado basado en este punto de división y llamarlo a pedido.
En este ejemplo, tenemos dos contenedores React; una vista de administrador y una vista de tablero.
admin.jsx
import React, {Component} from 'react'; export default class Admin extends Component { render() { return <div > Admin < /div>; } }
dashboard.jsx
import React, {Component} from 'react'; export default class Dashboard extends Component { render() { return <div > Dashboard < /div>; } }
Si el usuario ingresa la URL /dashboard
o /admin
, solo se carga el paquete de JavaScript requerido correspondiente. A continuación podemos ver ejemplos con y sin el enrutador del lado del cliente.
index.jsx
if (window.location.pathname === '/dashboard') { require.ensure([], function() { require('./containers/dashboard').default; }); } else if (window.location.pathname === '/admin') { require.ensure([], function() { require('./containers/admin').default; }); }
index.jsx
ReactDOM.render( <Router> <Route path="/" component={props => <div>{props.children}</div>}> <IndexRoute component={Home} /> <Route path="dashboard" getComponent={(nextState, cb) => { require.ensure([], function (require) { cb(null, require('./containers/dashboard').default) }, "dashboard")}} /> <Route path="admin" getComponent={(nextState, cb) => { require.ensure([], function (require) { cb(null, require('./containers/admin').default) }, "admin")}} /> </Route> </Router> , document.getElementById('content') );
Extracción de estilos en paquetes separados
En Webpack, los cargadores, como style-loader
y css-loader
, preprocesan las hojas de estilo y las incrustan en el paquete de JavaScript de salida, pero en algunos casos, pueden provocar el Flash de contenido sin estilo (FOUC).

Podemos evitar el FOUC con ExtractTextWebpackPlugin
que permite generar todos los estilos en paquetes CSS separados en lugar de tenerlos incrustados en el paquete JavaScript final.
webpack.config.js
var ExtractTextPlugin = require('extract-text-webpack-plugin'); module.exports = { module: { loaders: [{ test: /\.css/, loader: ExtractTextPlugin.extract('style', 'css')' }], }, plugins: [ // output extracted CSS to a file new ExtractTextPlugin('[name].[chunkhash].css') ] }
Manejo de bibliotecas y complementos de terceros
Muchas veces, necesitamos usar bibliotecas de terceros, varios complementos o scripts adicionales, porque no queremos perder tiempo desarrollando los mismos componentes desde cero. Hay muchas bibliotecas heredadas y complementos disponibles que no se mantienen activamente, no comprenden los módulos de JavaScript y asumen la presencia de dependencias globalmente bajo nombres predefinidos.
A continuación se muestran algunos ejemplos con complementos de jQuery, con una explicación de cómo configurar Webpack correctamente para poder generar el paquete final.
Proporcionar complemento
La mayoría de los complementos de terceros se basan en la presencia de dependencias globales específicas. En el caso de jQuery, los complementos se basan en la definición de la variable $
o jQuery
, y podemos usar los complementos de jQuery llamando a $('div.content').pluginFunc()
en nuestro código.
Podemos usar el complemento ProvidePlugin
de Webpack para anteponer var $ = require("jquery")
cada vez que encuentre el identificador $
global.
webpack.config.js
webpack.ProvidePlugin({ '$': 'jquery', })
Cuando Webpack procesa el código, busca la presencia $
y proporciona una referencia a las dependencias globales sin importar el módulo especificado por la función require
.
cargador de importaciones
Algunos complementos de jQuery asumen $
en el espacio de nombres global o confían en que this
sea el objeto de la window
. Para este propósito, podemos usar imports-loader
que inyecta variables globales en los módulos.
example.js
$('div.content').pluginFunc();
Luego, podemos inyectar la variable $
en el módulo configurando el imports-loader
:
require("imports?$=jquery!./example.js");
Esto simplemente antepone var $ = require("jquery");
a example.js
.
En el segundo caso de uso:
webpack.config.js
module: { loaders: [{ test: /jquery-plugin/, loader: 'imports?jQuery=jquery,$=jquery,this=>window' }] }
Al usar el símbolo =>
(que no debe confundirse con las funciones de flecha de ES6), podemos establecer variables arbitrarias. El último valor redefine la variable global this
para apuntar al objeto de la window
. Es lo mismo que envolver todo el contenido del archivo con la (function () { ... }).call(window);
y llamando a this
función con window
como argumento.
También podemos requerir bibliotecas usando el formato de módulo CommonJS o AMD:
// CommonJS var $ = require("jquery"); // jquery is available // AMD define(['jquery'], function($) { // jquery is available });
Algunas bibliotecas y módulos pueden admitir diferentes formatos de módulos.
En el siguiente ejemplo, tenemos un complemento de jQuery que usa el formato de módulo AMD y CommonJS y tiene una dependencia de jQuery:
jquery-plugin.js
(function(factory) { if (typeof define === 'function' && define.amd) { // AMD format is used define(['jquery'], factory); } else if (typeof exports === 'object') { // CommonJS format is used module.exports = factory(require('jquery')); } else { // Neither AMD nor CommonJS used. Use global variables. } });
webpack.config.js
module: { loaders: [{ test: /jquery-plugin/, loader: "imports?define=>false,exports=>false" }] }
Podemos elegir qué formato de módulo queremos usar para la biblioteca específica. Si declaramos define
to equal false
, Webpack no analiza el módulo en el formato del módulo AMD, y si declaramos las exports
de variables a equal false
, Webpack no analiza el módulo en el formato del módulo CommonJS.
Expose-cargador
Si necesitamos exponer un módulo al contexto global, podemos usar expose-loader
. Esto puede ser útil, por ejemplo, si tenemos scripts externos que no forman parte de la configuración de Webpack y dependen del símbolo en el espacio de nombres global, o si usamos complementos del navegador que necesitan acceder a un símbolo en la consola del navegador.
webpack.config.js
module: { loaders: [ test: require.resolve('jquery'), loader: 'expose-loader?jQuery!expose-loader?$' ] }
La biblioteca jQuery ahora está disponible en el espacio de nombres global para otros scripts en la página web.
window.$ window.jQuery
Configuración de dependencias externas
Si queremos incluir módulos de scripts alojados externamente, debemos definirlos en la configuración. De lo contrario, Webpack no puede generar el paquete final.
Podemos configurar scripts externos usando la opción externals
en la configuración de Webpack. Por ejemplo, podemos usar una biblioteca de un CDN a través de una etiqueta <script>
separada, y al mismo tiempo declararla explícitamente como una dependencia de módulo en nuestro proyecto.
webpack.config.js
externals: { react: 'React', 'react-dom': 'ReactDOM' }
Compatibilidad con varias instancias de una biblioteca
Es genial usar el administrador de paquetes NPM en el desarrollo front-end para administrar bibliotecas y dependencias de terceros. Sin embargo, a veces podemos tener varias instancias de la misma biblioteca con diferentes versiones y no funcionan bien juntas en un entorno.
Esto podría suceder, por ejemplo, con la biblioteca React, donde podemos instalar React desde NPM y luego una versión diferente de React puede estar disponible con algún paquete o complemento adicional. La estructura de nuestro proyecto puede ser similar a la siguiente:
project | |-- node_modules | |-- react |-- react-plugin | |--node_modules | |--react
Los componentes que provienen del react-plugin
tienen una instancia de React diferente al resto de los componentes del proyecto. Ahora tenemos dos copias separadas de React, y pueden ser versiones diferentes. En nuestra aplicación, este escenario puede alterar nuestro DOM mutable global y podemos ver mensajes de error en el registro de la consola web. La solución a este problema es tener la misma versión de React durante todo el proyecto. Podemos resolverlo mediante alias de Webpack.
webpack.config.js
module.exports = { resolve: { alias: { 'react': path.join(__dirname, './node_modules/react'), 'react/addons': path.join(__dirname, '/node_modules/react/addons'), } } }
Cuando react-plugin
intenta requerir React, usa la versión en node_modules
del proyecto. Si queremos saber qué versión de React usamos, podemos agregar console.log(React.version)
en el código fuente.
Concéntrese en el desarrollo, no en la configuración del paquete web
Esta publicación solo rasca la superficie del poder y la utilidad de Webpack.
Hay muchos otros complementos y cargadores de paquetes web que lo ayudarán a optimizar y agilizar la agrupación de JavaScript.
Incluso si es un principiante, esta guía le brinda una base sólida para comenzar a usar Webpack, lo que le permitirá concentrarse más en el desarrollo y menos en la configuración de paquetes.