Original article: How to use Webpack with React: an in-depth tutorial

Actualizado a Babel 7

En este tutorial veremos las bases de Webpack para React para ayudarte a empezar, incluyendo React Router, Reemplazo de Módulo Caliente (HMR), Separación de Código por Route y Proveedor, configuración de producción y más.

Antes de que empecemos, aquí hay una lista completa de las características que estaremos configurando juntos en este tutorial:

  • React 16
  • React Router 5
  • Semantic UI como el Framework de CSS
  • Reemplazo de Módulo Caliente (HMR)
  • Autoprefijo de CSS
  • Módulos de CSS
  • @babel/plugin-proposal-class-properties
  • @babel/plugin-syntax-dynamic-import
  • Webpack 4
  • Separación de Código por Ruta y Proveedor
  • Analizador de Paquetes de Webpack

Pre-requisitos

Tener lo siguiente pre-instalado:

Y deberías tener al menos algún conocimiento básico de React y React Router.

Nota: Puedes usar npm si lo deseas, aunque los comandos variarán ligeramente.

Dependencias Iniciales

Empecemos creando nuestro directorio y package.json.

En tu terminal escribe lo siguiente:

mkdir webpack-para-react && cd $_
yarn init -y

Este primer comando creará nuestro directorio y nos metemos dentro, luego inicializamos un package.json aceptando las configuraciones por defecto.

Si lo inspeccionas verás las configuraciones básicas:

{
  "name": "webpack-para-react",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT"
}

Ahora instalamos nuestras dependencias iniciales (producción) y dependencias de desarrollo.

En tu terminal escribe lo siguiente:

yarn add react react-dom prop-types react-router-dom semantic-ui-react
yarn add @babel/core babel-loader @babel/preset-env @babel/preset-react @babel/plugin-proposal-class-properties @babel/plugin-syntax-dynamic-import css-loader style-loader html-webpack-plugin webpack webpack-dev-server webpack-cli -D

Las dependencias de desarrollo serán usadas solamente, como su nombre lo dice, durante la fase de desarrollo, y las dependencias (producción) es lo que nuestra aplicación necesita en producción.

{
  "name": "webpack-para-react",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "dependencies": {
    "react": "^16.2.0",
    "react-dom": "^16.2.0",
    "prop-types": "^15.6.2",
    "react-router-dom": "^4.2.2",
    "semantic-ui-react": "^0.77.1"
  },
  "devDependencies": {
    "@babel/core": "^7.0.0",
    "@babel/plugin-proposal-class-properties": "^7.0.0",
    "@babel/plugin-syntax-dynamic-import": "^7.0.0",
    "@babel/preset-env": "^7.0.0",
    "@babel/preset-react": "^7.0.0", 
    "babel-loader": "^8.0.1",
    "css-loader": "^0.28.10",
    "html-webpack-plugin": "^3.0.4",
    "style-loader": "^0.19.1",
    "webpack": "^4.0.0",
    "webpack-cli": "^2.0.14",
    "webpack-dev-server": "^3.0.0"
  }
}

Nota: Cambios a los archivos creados previamente serán puestos en negrita.

Nota: Las versiones de Dependencias podrían ser distintos a los tuyos desde el momento de éste escrito.

  • react — Estoy seguro que sabes qué es React
  • react-dom — Provee métodos específicos del DOM para el navegador
  • prop-types — Verificación de tipo de tiempo de ejecución para las propiedades de React
  • react-router-dom — Provee las capacidades de enrutamiento a React para el navegador
  • semantic-ui-react — Framework de CSS
  • @babel/core — Dependencias principales para Babel
  • Babel es un transpilador que compila JavaScript ES6 a JavaScript ES5 permitiéndote escribir JavaScript "del futuro" de manera que los navegadores actuales lo entenderán
  • babel-loader — Este paquete permite transpilar archivos de JavaScript usando Babel y webpack
  • @babel/preset-env — Con este no tienes que especificar si estarás escribiendo ES2015, ES2016 o ES2017. Babel automáticamente lo detecterá y lo transpilará respectivamente
  • @babel/preset-react — Le dice a Babel que estaremos usando React
  • @babel/plugin-proposal-class-properties — Usa propiedades de clase. No usamos Propiedades de Clase en este proyecto, pero muy probablemente tú los utilizarás en tu proyecto
  • @babel/plugin-syntax-dynamic-import — Ser capaz de usar imports dinámicos
  • css-loader — Interpreta @import y url() como import/require() y los resolverá
  • html-webpack-plugin — Puede generar un archivo HTML para tu aplicación, o puedes proveer una plantilla
  • style-loader — Agrega CSS al DOM inyectando una etiqueta <style>
  • webpack — Empaquetador de Módulos
  • webpack-cli — Interfaz de Línea de Comando, necesario para Webpack 4.0.1 y y los últimos
  • webpack-dev-server — Provee un servidor de desarrollo para tu aplicación

Configurar Babel

En el directorio raíz (webpack-para-react) creamos el archivo de configuración de Babel.

touch .babelrc

En este punto puedes abrir tu editor favorito (el mío es VS CODE por cierto), luego apunta el editor a la ruta de este proyecto y abre el archivo .babelrc y copia lo siguiente:

{
  "presets": ["@babel/preset-env", "@babel/preset-react"],
  "plugins": [
    "@babel/plugin-syntax-dynamic-import",
    "@babel/plugin-proposal-class-properties"
  ]
}

Esto le dice a Babel para usar los preajustes (plugins) que anteriormente instalamos. Luego cuando llamamos babel-loader de Webpack, aquí es donde verá qué hacer.

Configurar Webpack

¡Ahora la diversión comienza! Vamos a crear el archivo de configuración de Webpack.

En tu terminal escribe lo siguiente:

touch webpack.config.js

Abre webpack.config.js y copia lo siguiente:

const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');

const port = process.env.PORT || 3000;

module.exports = {
  // La configuracion de Webpack va aquí
};

Este es el shell básico para Webpack. Requerimos webpack y html-wepback-plugin. Provee un puerto por defecto si la variable de entorno PORT no existe y exporta el módulo.

Lo siguiente serán adiciones para weback.config.js (uno después del otro)

...
module.exports = {
  mode: 'development',
};

mode le dice a Webpack que esta configuración será o para development o para production. "Modo de Desarrollo [está] optimizado para la velocidad y la experiencia del desarrollador... Las configuraciones de Producción por defecto te darán un conjunto de configuraciones preestablecidas útiles para desplegar tu aplicación".

...
module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.[hash].js'
  },
};

Para tener una instancia de Webpack ejecutando necesitamos:

  • entry — Especifica el punto de entrada de tu aplicación; aquí es donde tu app de React vive y donde el proceso de empaquetamiento comenzará (Docs)

Webpack 4 introdujo algunas configuraciones por defecto, así que si no incluyes entry en tu configuración, entonces Webpack asumirá que tu punto de entrada está localizado en el directorio ./src, haciendo entry opcional en contraposición a Webpack 3. Para este tutorial he decidido dejar entry ya que hace obvio donde estará nuestro punto de entrada, pero eres más que bienvenido en removerlo si así lo deseas.

  • output — Le dice a Webpack cómo escribir los archivos compilados al disco (Docs)
  • filename — Éste será el nombre de archivo de la aplicación empaquetada. La porción [hash] del nombre de archivo será reemplazado por un hash generado por Webpack cada vez que tu aplicación cambie y es recompilado (ayuda con caching).
...
module.exports = {
  ...
  devtool: 'inline-source-map',
};

devtool creará mapas de fuente para ayudarte en debuguear tu aplicación. Hay muchos tipos de mapas de fuente y este mapa particular (inline-source-map) es para ser usado solamente en desarrollo. (Docs para más opciones).

...
module.exports = {
  ...
  module: {
    // Reglas
    rules: [

      // Primer Regla
      {
        test: /\.(js)$/,
        exclude: /node_modules/,
        use: ['babel-loader']
      },

      // segunda Regla
      {
        test: /\.css$/,
        use: [
          {
            loader: 'style-loader'
          },
          {
            loader: 'css-loader',
            options: {
              modules: true,
              localsConvention: 'camelCase',
              sourceMap: true
            }
          }
        ]
      }
    ]
  },
};
  • module — Qué tipos de módulos incluirá tu aplicación, en nuestro caso soportaremos ESNext (Babel) y Módulos de CSS
  • rules — Cómo manejamos cada distinto tipo de módulo

Primer Regla

Probamos archivos con una extensión .js excluyendo el directorio node_modules y usamos Babel, mediante babel-loader, transpilarlos a vanilla JavaScript (básicamente, buscando nuestros archivos de React).

¿Recuerdas nuestra configuración en .babelrc? Aquí es donde Babel mira ese archivo.

Segunda Regla

Testeamos archivos CSS con una extensión .css. Aquí usamos dos cargadores, style-loader y css-loader, para manejar nuestros archivos CSS. Luego instruimos a css-loader para usar Módulos de CSS, camel case y crear mapeos de fuente.

Módulos de CSS y Camel Case

Esto nos da la habilidad de usar la sintaxis  import Styles from './styles.css' (o destructurar así import { style1, style2 } from './styles.css').

Después podemos usarlo de esta manera en una app de React:

...
<div className={Style.style1}>Hola Mundo</div>
// o con la sintaxis de destructuración
<div className={style1}>Hola Mundo</div>
...

Camel case nos da la habilidad de escribir nuestras reglas de CSS así:

.home-button {...}

Y usarlo en nuestros archivos de React de esta forma:

...
import { homeButton } from './styles.css'
...
...
module.exports = {
  ...
  plugins: [
    new HtmlWebpackPlugin({
      template: 'public/index.html',
      favicon: 'public/favicon.ico'
    })
  ],
};

En esta sección es donde configuramos (como el nombre implica) plugins.

html-webpack-plugin acepta un objeto con diferentes opciones. En nuestro caso especificamos la plantilla de HTML que estaremos usando y el favicon. (Referirse a la docs para más opciones).

Luego estaremos agregando otros plugin para el analizador de Empaquetamientos y HMR.

...
module.exports = {
  ...
  devServer: {
    host: 'localhost',
    port: port,
    historyApiFallback: true,
    open: true
  }
};

Finalmente, configuramos el servidor de desarrollo. Especificamos localhost como el alojamiento y asignamos la variable port como el puerto (si recuerdas, asignamos el puerto 3000 a esta variable). Ponemos historyApiFallback a true y open a true. Esto abrirá el navegador automáticamente y lanzará tu aplicación en http://localhost:3000. (Docs).

Ahora, debajo está la configuración de Webpack completa (webpack.config.js):

const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');

const port = process.env.PORT || 3000;

module.exports = {
  mode: 'development',  
  entry: './src/index.js',
  output: {
    filename: 'bundle.[hash].js'
  },
  devtool: 'inline-source-map',
  module: {
    rules: [
      {
        test: /\.(js)$/,
        exclude: /node_modules/,
        use: ['babel-loader']
      },
      {
        test: /\.css$/,
        use: [
          {
            loader: 'style-loader'
          },
          {
            loader: 'css-loader',
            options: {
              modules: true,
              localsConvention: 'camelCase',
              sourceMap: true
            }
          }
        ]
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: 'public/index.html',
      favicon: 'public/favicon.ico'
    })
  ],
  devServer: {
    host: 'localhost',
    port: port,
    historyApiFallback: true,
    open: true
  }
};

Creando la App de React

Estaremos creando una app de Hola Mundo sencilla con tres rutas: un inicio, una página no encontrada y una página dinámica que estaremos cargando de manera asíncrona cuando implementemos separación de código después.

Nota: Asumiendo que tienes un conocimiento básico de React y React Router, no iré en muchos detalles y solamente resaltaré lo que es más relevante para este tutorial.

Actualmente tenemos la siguiente estructura de proyecto:

|-- node_modules
|-- .babelrc
|-- package.json
|-- webpack.config.js
|-- yarn.lock

En tu terminal escribe lo siguiente:

mkdir public && cd $_
touch index.html

Creamos un directorio public, lo movemos dentro y también creamos un archivo index.html. Aquí es donde también tenemos el favicon. Puedes tomarlo desde aquí y copiarlo dentro del directorio public.

Abrimos el archivo index.html y copiamos lo siguiente:

<!DOCTYPE html>
<html lang="es">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.2.13/semantic.min.css"></link>
  <title>webpack-para-react</title>
</head>

<body>
  <div id="root"></div>
</body>

</html>

No hay mucho aquí (solo una plantilla de HTML estándar), estamos agregando la hoja de estilos de Semantic UI y también creamos un div con un ID de root. Aquí es donde nuestra app de React renderizará.

De nuevo en tu terminal escribe lo siguiente:

cd ..
mkdir src && cd $_
touch index.js

Abre index.js y copia lo siguiente:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/App';

ReactDOM.render(<App />, document.getElementById('root'));

En tu terminal escribe lo siguiente:

mkdir components && cd $_
touch App.js Layout.js Layout.css Home.js DynamicPage.js NoMatch.js

Después de crear los archivos componentes de React, tenemos la siguiente estructura del proyecto:

|-- node_modules
|-- public
    |-- index.html
    |-- favicon.ico
|-- src
    |-- components
        |-- App.js
        |-- DynamicPage.js
        |-- Home.js
        |-- Layout.css
        |-- Layout.js
        |-- NoMatch.js
    |-- index.js
|-- .babelrc
|-- package.json
|-- webpack.config.js
|-- yarn.lock

Abre App.js y copia lo siguiente:

import React from 'react';
import { Switch, BrowserRouter as Router, Route } from 'react-router-dom';

import Home from './Home';
import DynamicPage from './DynamicPage';
import NoMatch from './NoMatch';

const App = () => {
  return (
    <Router>
      <div>
        <Switch>
          <Route exact path="/" component={Home} />
          <Route exact path="/dynamic" component={DynamicPage} />
          <Route component={NoMatch} />
        </Switch>
      </div>
    </Router>
  );
};

export default App;

Creamos nuestro "shell" básico con React Router y tiene una página inicio, una página dinámica y una página no encontrada

Abre Layout.css y copia lo siguiente:

.pull-right {
  display: flex;
  justify-content: flex-end;
}

.h1 {
  margin-top: 10px !important;
  margin-bottom: 20px !important;
}

Abre Layout.js y copia lo siguiente:

import React from 'react';
import { Link } from 'react-router-dom';
import { Header, Container, Divider, Icon } from 'semantic-ui-react';

import { pullRight, h1 } from './layout.css';

const Layout = ({ children }) => {
  return (
    <Container>
      <Link to="/">
        <Header as="h1" className={h1}>
          webpack-para-react
        </Header>
      </Link>
      {children}
      <Divider />
      <p className={pullRight}>
        Made with <Icon name="heart" color="red" /> by Esau Silva
      </p>
    </Container>
  );
};

export default Layout;

Este es nuestro componente contenedor donde definimos el diseño del sitio. Haciendo uso de los Módulos de CSS, estamos importando dos reglas de CSS desde layout.css. También nota cómo estamos usando camel case para pullRight.

Abre Home.js y copia lo siguiente:

import React from 'react';
import { Link } from 'react-router-dom';

import Layout from './Layout';

const Home = () => {
  return (
    <Layout>
      <p>Hola Mundo de React y Webpack!</p>
      <p>
        <Link to="/dynamic">Navegar a Página Dinámica</Link>
      </p>
    </Layout>
  );
};

export default Home;

Abre DynamicPage.js y copia lo siguiente:

import React from 'react';
import { Header } from 'semantic-ui-react';

import Layout from './Layout';

const DynamicPage = () => {
  return (
    <Layout>
      <Header as="h2">Página Dinámica</Header>
      <p>Esta página fue cargada asincrónicamente!!!</p>
    </Layout>
  );
};

export default DynamicPage;

Abre NoMatch.js y copia lo siguiente:

import React from 'react';
import { Icon, Header } from 'semantic-ui-react';

import Layout from './Layout';

const NoMatch = () => {
  return (
    <Layout>
      <Icon name="minus circle" size="big" />
      <strong>Página No Encontrada!</strong>
    </Layout>
  );
};

export default NoMatch;

Ya terminamos de crear los componentes de React. Para un paso final antes de correr nuestra aplicación, abre package.json y agrega las líneas en negritas:

{
  "name": "webpack-for-react",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "start": "webpack-dev-server"
  },
  "dependencies": {
    "react": "^16.2.0",
    "react-dom": "^16.2.0",
    "prop-types": "^0.4.0",
    "react-router-dom": "^4.2.2",
    "semantic-ui-react": "^0.77.1"
  },
  "devDependencies": {
    "@babel/core": "^7.0.0",
    "@babel/plugin-proposal-class-properties": "^7.0.0",
    "@babel/plugin-syntax-dynamic-import": "^7.0.0",
    "@babel/preset-env": "^7.0.0",
    "@babel/preset-react": "^7.0.0",
    "babel-loader": "^8.0.1",
    "css-loader": "^0.28.10",
    "html-webpack-plugin": "^3.0.4",
    "style-loader": "^0.19.1",
    "webpack": "^4.0.0",
    "webpack-cli": "^2.0.14",
    "webpack-dev-server": "^3.0.0"
  }
}

Agregamos la clave scripts y también la clave start. Esto nos permitirá correr React con el Servidor de Desarrollo de Webpack. Si no especificas un archivo de configuración, webpack-dev-server buscará por el archivo webpack.config.js como la entrada de configuración por defecto dentro del directorio raíz.

¡Ahora el momento de la verdad! Escribe lo siguiente en tu terminal (recuerda estar en el directorio raíz) y Yarn llamará nuestro script start.

yarn start
iHJNOlQ4O11i7ONPbmXDRkbKF7WAGqZRFcCY
Corriendo React

Ahora tenemos una app de React funcionando impulsado por nuestra propia configuración de Webpack. Nota que al final del GIF resalto el archivo de JavaScript empaquetado que Webpack nos generó, y como indicamos en la configuración, el nombre de archivo tiene un hash único, bundle.d505bbab002262a9bc07.js.

Configurando el Hot Module Replacement (HMR)

De vuelta a tu terminal, instala React Hot Loader como una dependencia de desarrollo.

yarn add react-hot-loader @hot-loader/react-dom -D

Abre .babelrc y agregar líneas 3 y 9. No te olvides de incluir la coma (,) al final de la línea 3:  

{
  "presets": [
    ["@babel/preset-env", { "modules": false }],
    "@babel/preset-react"
  ],
  "plugins": [
    "@babel/plugin-syntax-dynamic-import",
    "@babel/plugin-proposal-class-properties",
    "react-hot-loader/babel"
  ]
}

Abre webpack.config.js y modíficalo como abajo.

Estoy incluyendo solamente el código relevante y omitiendo código que permaneció igual por la brevedad.

...
module.exports = {
  entry: './src/index.js',
  output: {
    ...
    publicPath: '/'
  },
  resolve: {
    alias: {
      "react-dom": "@hot-loader/react-dom",
    },
  },
  ...
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    ...
  ],
  devServer: {
    ...
    hot: true
  }
};
  • publicPath: ‘/’ — La Recarga rápida  no funcionará como se espera para las rutas anidadas sin él
  • webpack.HotModuleReplacementPlugin — Imprime más nombres de módulo legibles en la terminal del navegador en las actualizaciones de HMR
  • hot: true — Permite HMR en el servidor
  • resolve: alias — reemplaza react-dom con el react-dom personalizado de hot-loader

Abre index.js y cámbialo a lo siguiente.

import { hot } from "react-hot-loader/root";
import React from "react";
import ReactDOM from "react-dom";
import App from "./components/App";

import "./index.css";

const render = (Component) =>
  ReactDOM.render(<Component />, document.getElementById("root"));

render(hot(App));

Ahora estamos listos para probar HMR! Devuelta en la terminal, ejecuta tu app, haz un cambio, y observa como la app se actualiza sin un refresco de toda la página.

yarn start
tBaz3tT6Vsnxk7ijy1xFl03YFWmktaaktUWH
HMR en acción

Después de actualizar el archivo, la página cambia sin un refreso completo. Para mostrar este cambio en el navegador selecciono Rendering -> Paint flashing en el DevTools de Chrome, el cual resalta las áreas de la página, en verde, que cambiaron. También resalto en la terminal el cambio que Webpack envió al navegador para que esto suceda.

Separación de Código

Con separación de código, en vez de tener tu aplicación en un sólo paquete grande, puedes tener múltiples paquetes cada uno cargando asincrónicamente o en paralelo. También puedes separar código de proveedor del código de tu aplicación que puede decrecer potencialmente el tiempo de carga.

Por Ruta

Hay muchas maneras que podemos lograr separación de código por ruta, sin embargo en nuestro caso estaremos usando react-imported-component.

También nos gustaría mostrar un carga giratoria cuando el usuario navegue a una ruta diferente. Este es una buena práctica ya que no queremos que el usuario sólo se quede mirando una pantalla en blanco mientras él/ella espera que la nueva página se cargue. Así que, estaremos creando a componente Loading.

Sin embargo, si la nueva página carga realmente rápido, no queremos que el usuario vea una carga giratoria parpadeante por un par de milisegundos, así que retrasaremos el componente Loading por 300 milisegundos. Para lograr esto, estaremos usando React-Delay-Render.

Empieza por intsalar las dos dependencias adicionales.

En tu terminal escribe lo siguiente:

yarn add react-imported-component react-delay-render

Ahora vamos a crear el componente Loading.

En tu terminal escribe lo siguiente:

touch ./src/components/Loading.js

Abre Loading.js y copia lo siguiente:

import React from 'react';
import { Loader } from 'semantic-ui-react';
import ReactDelayRender from 'react-delay-render';

const Loading = () => <Loader active size="massive" />;

export default ReactDelayRender({ delay: 300 })(Loading);

Ahora que tenemos el componente Loading, abre App.js y modifícalo como sigue:

import React from 'react';
import { Switch, BrowserRouter as Router, Route } from 'react-router-dom';
import importedComponent from 'react-imported-component';

import Home from './Home';
import Loading from './Loading';

const AsyncDynamicPAge = importedComponent(
  () => import(/* webpackChunkName:'DynamicPage' */ './DynamicPage'),
  {
    LoadingComponent: Loading
  }
);
const AsyncNoMatch = importedComponent(
  () => import(/* webpackChunkName:'NoMatch' */ './NoMatch'),
  {
    LoadingComponent: Loading
  }
);

const App = () => {
  return (
    <Router>
      <div>
        <Switch>
          <Route exact path="/" component={Home} />
          <Route exact path="/dynamic" component={AsyncDynamicPAge} />
          <Route component={AsyncNoMatch} />
        </Switch>
      </div>
    </Router>
  );
};

export default App;

Esto creará tres paquetes, o trozos, uno para el componente DynamicPage, uno para el componente NoMatch, y uno para la app principal.

También cambiemos el nombre del archivo del paquete. Abre webpack.config.js y cámbialo como sigue:

...
module.exports = {
  ...
  output: {
    filename: '[name].[hash].js',
    ...
  },
}

Es tiempo de ejecutar la app y ver la separación de código por ruta en acción.

yarn start
dNGa5cqDKitNHQAKtH7ZDce3fvrCCWuE1zYh
Separación de Código por Ruta

En el GIF, primero resalto los tres diferentes pedazos creados por Webpack en la terminal. Luego resalto que sobre el lanzamiento de la app, solamente el pedazo principal fue cargado. Finalmente, vemos que sobre clickear Navigate a Dynamic Page el pedazo correspondiente a esta página fue cargada asincrónicamente.

También vemos que el pedazo correspondiente a la página no encontrada nunca fue cargado, ahorrando el ancho de banda del usuario.

Por proveedor

Ahora separemos la aplicación por proveedor. Abre webpack.config.js y haz los siguientes cambios:

...
module.exports = {
  entry: {
    vendor: ['semantic-ui-react'],
    app: './src/index.js'
  },
  ...
  optimization: {
    splitChunks: {
      cacheGroups: {
        styles: {
          name: 'styles',
          test: /\.css$/,
          chunks: 'all',
          enforce: true
        },
        vendor: {
          chunks: 'initial',
          test: 'vendor',
          name: 'vendor',
          enforce: true
        }
      }
    }
  },
  ...
};
  • entry.vendor: [‘semantic-ui-react’] — Especifica qué librería queremos extraer de nuestra app principal
  • optimization — si dejas esta entrada en blanco, Webpack aún separará tu aplicación por proveedor (vendor), sin embargo he notado que los tamaños de los empaquetados eran grandes y después de agregar esta entrada, los tamaños de los empaquetados redujeron significativamente. (Lo conseguí de Webpack 4 migration draft CommonsChunkPlugin -> Initial vendor chunk)

Nota: Previamente Webpack 3 hizo uso de CommonsChunkPlugin para separar el código por vendor y/o comunes, sin embargo quedó obsoleto en Webpack 4 y muchas de sus características ahora están disponibles por defecto. Con el retiro de CommonsChunkPlugin han agregado optimization.splitChunks para aquellos que necesitan control detallado sobre su estrategia de caching (Ver esto para una explicación en profundidad).

En la terminal, lanza la app:

yarn start
L5D5dDH3r5DkNfhN2oilt1BaejnhCYfpdnQw
Separación de Código por Vendor

En la terminal, resalto los tres pedazos previos mas el nuevo pedazo vendor. Después cuando inspeccionamos el HTML vemos que tanto como los pedazos vendor y app fueron cargados.

Siendo que hemos hechos varias actualizaciones a nuestra configuración de Webpack, debajo encontrarás el archivo webpack.config.js completo.

const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const port = process.env.PORT || 3000;
module.exports = {
  mode: 'development',
  entry: {
    vendor: ['semantic-ui-react'],
    app: './src/index.js'
  },
  output: {
    filename: '[name].[hash].js'
  },
  resolve: {
    alias: {
      "react-dom": "@hot-loader/react-dom",
    },
  },
  devtool: 'inline-source-map',
  module: {
    rules: [
      {
        test: /\.(js)$/,
        exclude: /node_modules/,
        use: ['babel-loader']
      },
      {
        test: /\.css$/,
        use: [
          {
            loader: 'style-loader'
          },
          {
            loader: 'css-loader',
            options: {
              modules: true,
              localsConvention: 'camelCase',
              sourceMap: true
            }
          }
        ]
      }
    ]
  },
  optimization: {
    splitChunks: {
      cacheGroups: {
        styles: {
          name: 'styles',
          test: /\.css$/,
          chunks: 'all',
          enforce: true
        },
        vendor: {
          chunks: 'initial',
          test: 'vendor',
          name: 'vendor',
          enforce: true
        }
      }
    }
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    new HtmlWebpackPlugin({
      template: 'public/index.html',
      favicon: 'public/favicon.ico'
    })
  ],
  devServer: {
    host: 'localhost',
    port: port,
    historyApiFallback: true,
    open: true,
    hot: true
  }
};

Configuración de Producción

Renombrar la configuración de Webpack de webpack.config.js a webpack.config.development.js. Luego haz una copia y nómbralo webpack.config.production.js.

En tu terminal escribe lo siguiente:

mv webpack.config.js webpack.config.development.js
cp webpack.config.development.js webpack.config.production.js

Necesitaremos una dependencia de desarrollo, el Plugin de Extracción de Texto. De su documentación: "Mueve todos los módulos *.css requeridos en pedazos de entrada en un archivo de CSS aparte. Así que, tus estilos ya no están alineados en el empaquetado de JS, sino que están en un archivo de CSS aparte (styles.css). Si el volumen de tu hoja de estilos es grande, será más rápido porque el empaquetado de CSS se carga en paralelo junto al empaquetado de JS."

En tu terminal escribe lo siguiente:

yarn add mini-css-extract-plugin -D

Abre webpack.config.production.js y haz los siguientes cambios en negrita:

Haciendo algo diferente acá Agregaré explicaciones con comentarios en línea.

const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
  mode: 'production',
  entry: {
    vendor: ['semantic-ui-react'],
    app: './src/index.js'
  },
  output: {
    // Queremos crear los empaquetados de JavaScript en un 
    // directorio 'static'
    filename: 'static/[name].[hash].js',
    // El Absolute path al directorio de salida deseado. En nuestro 
    // caso un directorio llamado 'dist'
    // '__dirname' es una variable de Node que nos da el camino
    // absoluto a nuestro directorio actual. Luego con 'path.resolve'
    // unimos los directorios
    // Webpack 4 asume que tu camino de salida será './dist' así que
    // puedes dejar esta entrada como está.
    path: path.resolve(__dirname, 'dist'),
    publicPath: '/'
  },
  // Cambia a mapeos de fuente de producción
  devtool: 'source-map',
  module: {
    rules: [
      {
        test: /\.(js)$/,
        exclude: /node_modules/,
        use: ['babel-loader']
      },
      {
        test: /\.css$/,
          use: [
            {
              // Configuramos 'MiniCssExtractPlugin'              
              loader: MiniCssExtractPlugin.loader,
            }, 
            {
              loader: 'css-loader',
              options: {
                modules: true,
                // Permite configurar cuántos loaders 
                // deberían ser aplicados antes de css-loader
                // a los recursos @import(ados)
                importLoaders: 1,
                localsConvention: 'camelCase',
                // Crear los mapeos de fuente para los archivos de CSS
                sourceMap: true
              }
            },
            {
              // PostCSS se ejecutará antes del css-loader y
              // minificará y autoprefijará nuestras reglas de CSS.
              loader: 'postcss-loader',
            }
          ]
      }
    ]
  },
  optimization: {
    splitChunks: {
      cacheGroups: {
        styles: {
          name: 'styles',
          test: /\.css$/,
          chunks: 'all',
          enforce: true
        },
        vendor: {
          chunks: 'initial',
          test: 'vendor',
          name: 'vendor',
          enforce: true
        }
      }
    }
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: 'public/index.html',
      favicon: 'public/favicon.ico'
    }),
    
    // Crea la hoja de estilo en el directorio 'styles'
    new MiniCssExtractPlugin({
      filename: 'styles/styles.[hash].css'
    })
  ]
};

Nota que removimos la variable port, los plugins relacionados a HMR y la entrada devServer.

También, desde que agregamos PostCSS a la configuración de producción, necesitamos instalarlo y crearle un archivo de configuración.

En tu terminal escribe lo siguiente:

yarn add postcss-loader autoprefixer cssnano postcss-preset-env -D
touch postcss.config.js

Abre postcss.config.js y copia lo siguiente:

const postcssPresetEnv = require('postcss-preset-env');
module.exports = {
  plugins: [
    postcssPresetEnv({
      browsers: ['>0.25%', 'not ie 11', 'not op_mini all']
    }),
    require('cssnano')
  ]
};

Aquí estamos especificando qué navegadores queremos que autoprefixer (referirse a las docs para más opciones) soporte y minificar la salida de CSS.

Ahora, para el último paso, antes de crear nuestra construcción de producción, necesitamos crear un script build en el package.json.

Abre el archivo y haz los siguientes cambios a la sección scripts:

...
"scripts": {
  "dev":"webpack-dev-server --config webpack.config.development.js",
  "prebuild": "rimraf dist",
  "build": "cross-env NODE_ENV=production webpack -p --config webpack.config.production.js"
},
...

Lo primero en notar aquí es que cambiamos el script de start a dev, luego agregamos dos scripts adicionales, prebuild y build.

Finalmente, estamos indicando qué configuración usar cuando esté en desarrollo o en producción.

  • prebuild — se ejecutará antes del script build y eliminará la carpeta dist creado por nuestra última construcción de producción. Usamos la librería rimraf para esto.
  • build — Primero usamos la librería cross-env sólo en caso de que alguien esté usando Windows. De esta manera, configurar variables de entorno con NODE_ENV funcionará. Después llamamos a Webpack con la bandera -p para decirle que optimice ésta construcción para producción, y finalmente especificamos la configuración de producción.

En tu terminal instala las dos nuevas dependencias que incluimos en el package.json:

yarn add rimraf cross-env -D

Antes de crear la construcción de producción, miremos nuestra nueva estructura del proyecto:

|-- node_modules
|-- public
    |-- index.html
    |-- favicon.ico
|-- src
    |-- components
        |-- App.js
        |-- DynamicPage.js
        |-- Home.js
        |-- Layout.css
        |-- Layout.js
        |-- Loading.js
        |-- NoMatch.js
    |-- index.js
|-- .babelrc
|-- package.json
|-- postcss.config.js
|-- webpack.config.development.js
|-- webpack.config.production.js
|-- yarn.lock

Ahora sí podemos crear nuestro empaquetado de producción.

En tu terminal escribe lo siguiente:

yarn build
T81PYokNYP1m76WiEbGipkFrmGYKee46P4IR
Empaquetado de la construcción a producción

Como haz notado, después que ejecutamos el script build, Webpack creó una carpeta dist conteniendo nuestra app lista para producción. Ahora inspecciona los archivos que fueron creados y nota que están minificados y cada uno tiene un mapeo de fuente correspondiente. También notarás que PostCSS ha agregado autoprefijado al archivo de CSS.

Ahora tomamos nuestros archivos de producción y encendemos un servidor de Node para servir nuestro sitio, y este es el resultado:

A44vaXq-Jc68ClNMgWe5O0o4a3XjIfj3XLrU
Ejecutando la construcción de producción

Nota: Estoy usando este servidor en el GIF de arriba para servir nuestros archivos de producción.

En este punto tenemos dos configuraciones de Webpack funcionando, uno para desarrollo y uno para producción. Sin embargo, ya que ambas configuraciones son muy similares, comparten muchas de las mismas configuraciones. Si queremos agregar algo más, tendríamos que agregarlo a ambos archivos de configuración.

Composición de Webpack

Empecemos instalando webpack-merge y Chalk como dependencias de desarrollo.

En tu terminal escribe lo siguiente:

yarn add webpack-merge chalk -D

También necesitaremos un par de nuevas carpetas y unos pocos archivos nuevos.

En tu terminal escribe lo siguiente:

mkdir -p build-utils/addons
cd build-utils
touch build-validations.js common-paths.js webpack.common.js webpack.dev.js webpack.prod.js

Ahora miremos nuestra nueva estructura de proyecto:

|-- build-utils
    |-- addons
    |-- build-validations.js
    |-- common-paths.js
    |-- webpack.common.js
    |-- webpack.dev.js
    |-- webpack.prod.js
|-- node_modules
|-- public
    |-- index.html
    |-- favicon.ico
|-- src
    |-- components
        |-- App.js
        |-- DynamicPage.js
        |-- Home.js
        |-- Layout.css
        |-- Layout.js
        |-- Loading.js
        |-- NoMatch.js
    |-- index.js
|-- .babelrc
|-- package.json
|-- postcss.config.js
|-- webpack.config.development.js
|-- webpack.config.production.js
|-- yarn.lock

Abre common-paths.js y copia lo siguiente:

const path = require('path');
const PROJECT_ROOT = path.resolve(__dirname, '../');

module.exports = {
  projectRoot: PROJECT_ROOT,
  outputPath: path.join(PROJECT_ROOT, 'dist'),
  appEntry: path.join(PROJECT_ROOT, 'src')
};

Aquí definimos, como el nombre lo implica, los caminos comúnes para nuestras configuraciones de Webpack. PROJECT_ROOT necesita buscar una carpeta ya que estamos trabajando en la carpeta build-utils (un nivel menos del camino raíz actual de nuestro proyecto).

Abre build-validations.js y copia lo siguiente:

const chalk = require('chalk');
const ERR_NO_ENV_FLAG = chalk.red(
  `Debes pasar una bandera --env.env en tu construcción a webpack para que funcione!`
);

module.exports = {
  ERR_NO_ENV_FLAG
};

Luego, cuando modifiquemos nuestro package.json estaremos requiriendo la bandera --env.env en los scripts. Estas validaciones están para verificar que la bandera esté presente; si no, lanzará un error.  

En los tres archivos próximos, estaremos separando las configuraciones de Webpack en configuraciones que son compartidas entre desarrollo y producción, configuraciones que son solamente para desarrollo y configuraciones solamente para producción.

Abre webpack.common.js y copia lo siguiente:

const commonPaths = require('./common-paths');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const config = {
  entry: {
    vendor: ['semantic-ui-react']
  },
  output: {
    path: commonPaths.outputPath,
    publicPath: '/'
  },
  module: {
    rules: [
      {
        test: /\.(js)$/,
        exclude: /node_modules/,
        use: ['babel-loader']
      }
    ]
  },
  optimization: {
    splitChunks: {
      cacheGroups: {
        styles: {
          name: 'styles',
          test: /\.css$/,
          chunks: 'all',
          enforce: true
        },
        vendor: {
          chunks: 'initial',
          test: 'vendor',
          name: 'vendor',
          enforce: true
        }
      }
    }
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: 'public/index.html',
      favicon: 'public/favicon.ico'
    })
  ]
};
module.exports = config;

Básicamente extraímos lo que fue compartido entre webpack.config.development.js y webpack.config.production.js y lo transferimos a este archivo. En la parte de arriba requerimos common-paths.js para establecer el output.path.

Abre webpack.dev.js y copia lo siguiente:

const commonPaths = require('./common-paths');
const webpack = require('webpack');
const port = process.env.PORT || 3000;
const config = {
  mode: 'development',
  entry: {
    app: `${commonPaths.appEntry}/index.js`
  },
  output: {
    filename: '[name].[hash].js'
  },
  resolve: {
    alias: {
      "react-dom": "@hot-loader/react-dom",
    },
  },
  devtool: 'inline-source-map',
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          {
            loader: 'style-loader'
          },
          {
            loader: 'css-loader',
            options: {
              modules: true,
              localsConvention: 'camelCase',
              sourceMap: true
            }
          }
        ]
      }
    ]
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ],
  devServer: {
    host: 'localhost',
    port: port,
    historyApiFallback: true,
    hot: true,
    open: true
  }
};
module.exports = config;

Este es el mismo concepto como en el archivo previo. Aquí extrajimos configuraciones de desarrollo solamente.

Abre webpack.prod.js y copia lo siguiente:

const commonPaths = require('./common-paths');
const webpack = require('webpack');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const config = {
  mode: 'production',
  entry: {
    app: [`${commonPaths.appEntry}/index.js`]
  },
  output: {
    filename: 'static/[name].[hash].js'
  },
  devtool: 'source-map',
  module: {
    rules: [
      {
        test: /\.css$/,
          use: [
            {
              // Configuramos 'MiniCssExtractPlugin'              
              loader: MiniCssExtractPlugin.loader,
            }, 
            {
              loader: 'css-loader',
              options: {
                modules: true,
                importLoaders: 1,
                localsConvention: 'camelCase',
                sourceMap: true
              }
            },
            {
              loader: 'postcss-loader'
            }
          ]
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'styles/styles.[hash].css'
    })
  ]
};
module.exports = config;

Extrajimos las configuraciones de producción solamente.

Ahora que tenemos las configuraciones compartidas y las que son específicas para desarrollo y producción en archivos separados, es tiempo de poner todo junto.

En la terminal, si estás todavía en la carpeta build-utils, ve un nivel arriba a la raíz del proyecto, luego elimina las configuraciones de Webpack previas y crea una nueva configuración de Webpack. Llámalo webpack.config.js.

En tu terminal escribe lo siguiente:

cd ..
rm webpack.config.development.js webpack.config.production.js
touch webpack.config.js

Antes de configurar webpack.config.js, abramos package.json y actualicemos la sección de scripts.

Modificar la sección de la siguiente manera:

...
"scripts": {
  "dev": "webpack-dev-server --env.env=dev",
  "prebuild": "rimraf dist",
  "build": "cross-env NODE_ENV=production webpack -p --env.env=prod"
},
...

Ya que removimos la bandera --config, Webpack ahora estará buscando la configuración por defecto, el cual es webpack.config.js. Ahora usamos la bandera  –env para pasar una variable de entorno a Webpack, env=dev para desarrollo yenv=prod para producción.

Abre webpack.config.js y copia lo siguiente:

Explicaciones con comentarios en línea.

const buildValidations = require('./build-utils/build-validations');
const commonConfig = require('./build-utils/webpack.common');

const webpackMerge = require('webpack-merge');

// Podemos incluir plugins de Webpack, a través de complementos, que
// no necesitan ejecutarse cada vez que estemos desarrollando.
// Veremos un ejemplo cuando configuremos 'Analizador de Paquetes'
const addons = (/* string | string[] */ addonsArg) => {
  
  // Normalizar el arreglo addons (flatten)
  let addons = [...[addonsArg]] 
    .filter(Boolean); // Si los complementos son undefined, los filtramos

  return addons.map(addonName =>
    require(`./build-utils/addons/webpack.${addonName}.js`)
  );
};

// 'env' contendrá la variable de entorno de la sección 'scripts' 
// en el 'package.json'.
// console.log(env); => { env: 'dev' }
module.exports = env => {

  // Usamos 'buildValidations' para verificar la bandera 'env'
  if (!env) {
    throw new Error(buildValidations.ERR_NO_ENV_FLAG);
  }

  // Seleccionar qué configuración de Webpack usar: desarrollo
  // o producción
  // console.log(env.env); => dev
  const envConfig = require(`./build-utils/webpack.${env.env}.js`);
  
  // 'webpack-merge' combinará nuestras configuraciones compartidas, 
  // las configuraciones específicas de entorno y cualquier complemento
  // que estemos incluyendo
  const mergedConfig = webpackMerge(
    commonConfig,
    envConfig,
    ...addons(env.addons)
  );

  // Luego devolver la configuración final para Webpack
  return mergedConfig;
};

Ahora, esto podría parecer un montón de configuraciones, pero a la larga, se volverá práctico.

En este momento, puedes lanzar la aplicación o construir los archivos para producción, y todo funcionará como se espera (disculpa, no hay GIF esta vez).

yarn dev
yarn build

Nota: Esta técnica de “Composición de Webpack” fue tomado de Webpack Academy, un curso gratis por Sean Larkin el cual recomiendo para aprender más sobre Webpack, no específico a React.

BONUS: Configurar el Analizador de Paquetes de Webpack

No necesitas necesariamente Analizador de Paquetes de Webpack, pero viene muy bien cuando intentas optimizar tus construcciones.

Empieza instalando la dependencia y crea el archivo de configuración.

En tu terminal escribe lo siguiente:

yarn add webpack-bundle-analyzer -D
touch build-utils/addons/webpack.bundleanalyzer.js

Abre webpack.bundleanalyzer.js y copia lo siguiente:

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer')
  .BundleAnalyzerPlugin;

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'server'
    })
  ]
};

Estamos sólo exportando la sección de plugins, el cual incluye el Analizador de Paquetes, para Webpack. Luego webpack-merge lo combinará en la configuración de Webpack final. ¿Recuerdas los complementos en webpack.config.js? Bueno, aquí es donde entra en acción.

Para el paso final, abramos package.json e incluyamos los nuevos scripts de la siguiente manera:

"scripts": {
  "dev": "webpack-dev-server --env.env=dev",
  "dev:bundleanalyzer": "yarn dev --env.addons=bundleanalyzer",
  "prebuild": "rimraf dist",
  "build": "cross-env NODE_ENV=production webpack -p --env.env=prod",
  "build:bundleanalyzer": "yarn build --env.addons=bundleanalyzer"
},
  • dev:bundleanalyzer — Llama el script dev y pasa una nueva variable de entorno addons=bundleanalyzer
  • build:bundleanalyzer — Llama el script build y pasa una nueva variable de entorno addons=bundleanalyzer

Es tiempo de correr la app con el complemento de Analizador de Paquetes.

En tu terminal escribe lo siguiente:

yarn dev:bundleanalyzer
EFwowawQtLlHdJQGL9gwuz77O2wfhz784xW2
Analizador de Paquetes de Webpack

La aplicación se lanza junto al Analizador de Paquetes de Webpack.

Incluir complementos con la Composición de Webpack pude ser muy útil, ya que hay muchos plugins que querrías usar solamente en ciertas ocasiones.

Conclusión

Primero de todo, puedes obtener el código completo en el repositorio de Github.

Bueno, llegaste al final. ¡Felicidades! Ahora que sabes las bases (y un poquito más) de Webpack para React, puedes continuar y seguir explorando y aprendiendo características y técnicas más avanzadas.

Gracias por leer y espero que lo hayas disfrutado. Si tienes cualquier pregunta, sugerencia o correcciones, házmelos saber en los comentarios abajo. No te olvides de compartir este artículo y ¿dar algunos aplausos??

Me puedes seguir aquí en Medium, Twitter, GitHub, LinkedIn o todos ellos.

Este artículo fue publicado originalmente en mi sitio web de blog personal.


Actualización 8/25/19: He estado construyendo una aplicación web de oraciones llamado "Mi Tiempo Tranquilo - Una Oración Diara". Si te gustaría estar al tanto por favor regístrate a través del siguiente link: http://b.link/mqt

Mis Mensajes Directos en Twitter están abiertos si tienes alguna pregunta respecto a la app.