Retomamos esta saga sobre Babel y Webpack. Si acabas de aterrizar aquí, te recomiendo que veas la parte #1 primero, donde explicamos qué es Babel, para qué sirve y cómo instalarlo, entre otras muchas cosas.
En esta segunda parte vamos a hablar fundamentalmente sobre Webpack. ¡Vamos al tema! ??
¿Qué es Webpack?
Webpack es un herramienta que nos permite agrupar nuestros módulos de JavaScript (en adelante, JS). Por "módulos" entendemos los diversos bloques de código que se encuentran en los archivos JS de los que se compone una app. Webpack los funde en un sólo archivo JS, llamado comúnmente bundle.js.
Efectivamente, en la parte #1 hemos usado un archivo con el mismo nombre, pero en esa fase no tenía todavía mucho sentido, porque no estábamos "empaquetando" nada (bundle es la palabra inglesa para "empaquetar" o "paquete").
Ejemplo de cómo funciona Webpack, comprimiendo varios archivos y fusionándolos en uno solo.
Pero a partir de ahora cobrará más sentido, porque utilizaremos Webpack junto con Babel para convertir y fusionar archivos y acabar únicamente con un sólo archivo JS que los conjugue todos.
Webpack también sirve para establecer un servidor local de desarrollo (en inglés, local development server), pero más sobre esto en el siguiente episodio de esta serie ?.
Al dividir nuestro código en diferentes archivos, Webpack nos proporciona una manera mucho más ordenada de escribir y mantener el código de un proyecto.
El funcionamiento sería el siguiente. Supongamos que tenemos una estructura de archivos tal y como la hemos dejado en la parte #1, a saber:
- ?dist
- ?assets
- ? bundle.js
- ? index.html
- ?node_modules
- ?src
- ? index.js
- ? .babelrc
- ? package-lock.json
- ? package.json
Y llega un momento en el que necesitamos crear más archivos JS dentro de src, por ejemplo, uno para interactuar con una database y otro para interactuar con nuestro UI. Para poder comunicarse entre sí, estos archivos necesitarán importar y exportar funcionalidades entre ellos (en inglés, import y export). Webpack será quien nos permita hacer eso. ?
A esos bloques de código (o funcionalidades) que necesitemos importar se les llama módulos (en inglés, JS modules). Pero más sobre esto luego.
Cómo hacer la configuración inicial de Webpack
Para empezar a utilizar Webpack, lo primero que debemos hacer es crear un archivo de configuración (en inglés, un webpack config file) en la raíz de nuestro proyecto. O sea, a la altura de dist, node_modules, etc. En ese config file es donde establecemos el comportamiento de Webpack, como si fuera un panel de mandos o centro de control.
El config file recogerá, entre otras cosas, dónde deben estar nuestro archivos fuente o el archivo JS final que será el que se ejecute en el navegador (el bundle.js).
Recordemos que con Babel hacíamos esta configuración en el package.json:
"scripts": { "babel": "./node_modules/.bin/babel.cmd ./src/index.js -w -o ./dist/assets/bundle.js" },
Pero con Webpack podemos hacer mucho más, utilizando un archivo dedicado a almacenar todas nuestras opciones de configuración. ??
1. Creamos un archivo nuevo a la altura de dist, llamado webpack.config.js. Aquí es donde configuraremos todas las opciones de Webpack.
2. Dentro del archivo, creamos un objeto que almacenará todas las opciones de Webpack.
module.exports = {};
Esta sintaxis sólo puede ejecutarse en nuestro ordenador gracias a NodeJS, así que no podrá ejecutarse en el navegador así directamente, sino que pasará un proceso de compresión y quedará comprimido en el bundle.js, archivo que sí será ejecutable en el navegador.
Dentro del objeto especificamos dos propiedades:
- una propiedad de entrada (en inglés, entry property), que es la ruta (en inglés, path) donde vive nuestro archivo con el código fuente, como podría ser el index.js.
- debe de ser un path relativo.
- una propiedad de salida (en inglés, output property) que es el path donde estará el archivo JS con el resultado final, en formato de código comprimido.
- es un objeto con una ruta absoluta (en inglés, absolute path) que contiene dos propiedades. Un absolute path define el path en relación a la raíz de mi ordenador, así que será distinto para cada persona. Es decir, en mi caso sería: C:\Users\Ro\workspace\babel-webpack-project> , quedando así el código:
module.exports = { entry: './src/index.js', output: { path: 'C:/Users/Ro/workspace/babel-webpack-project' } };
Pero esto no es muy práctico de cara a compartir nuestro código con otras personas, porque tendrían que adaptar ese path. Para solucionar esto, usamos otra característica (en inglés, feature) de NodeJS, la propiedad __dirname. Esto nos da el absolute path completo desde la raíz de nuestro pc hasta la raíz de nuestro proyecto. Así que se adapta automáticamente a cada pc.
3. Para llegar hasta el archivo de salida (el bundle.js), usamos un método de NodeJS llamado path.resolve(). Para eso debemos solicitar el módulo path que viene dentro de NodeJS.
El método path.resolve() admite dos parámetros, que serían los dos paths que queremos unir para llegar hasta bundle.js. Es decir, __dirname + el path relativo hasta llegar a ese archivo. Fíjate que no añadimos el nombre del archivo, sino que nos quedamos a las puertas, en la carpeta que lo contiene. Esto es así porque la segunda propiedad del output es precisamente filename, teniendo ahí la oportunidad de especificarlo.
const path = require('path'); module.exports = { entry: './src/index.js', output: { path: path.resolve(__dirname, 'dist/assets'), filename: 'bundle.js' } };
A partir de ahora, Webpack sabrá por dónde empezar y con qué archivo terminar. ?
Cómo ejecutar Webpack con la ayuda de Webpack CLI
Para ejecutar Webpack necesitamos instalar dos cosas: un paquete con las funcionalidades esenciales (en inglés, core) de Webpack y la CLI de Webpack. Es un paso prácticamente idéntico al que hemos hecho antes con Babel.
1. Desde la terminal, navegamos hasta la carpeta raíz de nuestro proyecto, instalamos los paquetes y los guardamos como dev dependencies.
npm install webpack webpack-cli --save-dev
2. Ejecutamos Webpack para que pueda llevar a cabo el código que le hemos indicado en el webpack.config.js. Para eso, al igual que con Babel, el archivo binario de Webpack está en node_modules/.bin. Así que navegamos hasta la CLI de Webpack y le damos a enter.
.\node_modules\.bin\webpack.cmd
Si todo ha ido bien, deberías ver un mensaje parecido a este:
Time: 267ms
Built at: 12/15/2019 4:55:23 PM
Asset Size Chunks Chunk Names
bundle.js 999 bytes 0 [emitted] main
Entrypoint main = bundle.js
[0] ./src/index.js 113 bytes {0} [built]
WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/configuration/mode/
Si vas ahora al archivo bundle.js, verás un formato distinto a cuando Babel cogía el index.js y lo traducía a código antiguo. Efectivamente, verás una "tira de código" como esta:
!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=0)}([function(e,t){const n=e=>{console.log(`hey ${e}`)};n("mike"),n("dustin"),n("nancy")}]);
? Que no cunda el pánico. Lo que ha hecho Webpack es minimizar el código del index.js y transformarlo en una versión comprimida para mejorar el rendimiento de nuestra app. Esto cobrará más importancia cuando trabajemos con JS modules.
Ten en cuenta que por el momento Webpack no está convirtiendo nuestro código moderno a una versión compatible, porque no le hemos indicado que use Babel. Así que esto ha sido como dar un paso hacia delante y un pasito hacia atrás ?♀️. Pero posteriormente vamos a integrar Babel con Webpack, no sufras.
3. Al igual que con Babel, podemos crear un npm script para no tener que escribir
.\node_modules\.bin\webpack.cmd
cada vez que queramos comprimir nuestro código. Lo creamos en el package.json. Le añadimos el banderín -w para que Webpack se ponga en modo "watching for changes", igual que hacíamos con Babel. Así sólo tendremos que ejecutarlo una vez y luego ya estará pendiente de cualquier cambio que guardemos para volver a ejecutarse automáticamente. ?
"scripts": { "babel": "./node_modules/.bin/babel.cmd ./src/index.js -w -o ./dist/assets/bundle.js", "webpack": "./node_modules/.bin/webpack.cmd -w" },
Ahora en lugar del comando largo, sólo tendremos que hacer
npm run webpack
Prueba a añadir algo de código a tu index.js y luego hacer npm run webpack en tu terminal. Verás que en el bundle.js ahora está esa última adición que hayas hecho (al final del código seguramente).
Introducción a los módulos de JavaScript
Vamos a empezar a ver, por fin, el potencial que tiene Webpack y cómo usarlo. Como adelanté antes, Webpack tiene la capacidad de conectar diferentes archivos JS entre sí para así generar una sola fuente sobre la que hacer su compresión (el index.js) y tener un solo archivo final minimizado (el bundle.js).
La forma en la que Webpack conecta archivos JS entre sí es permitiendo hacer imports y exports de los bloques de código que necesitemos usar en el archivo index.js. Esos bloques de código, al ser importados y exportados, adquieren el nombre de JS modules. Por eso se dice que Webpack es un module bundler ?. Vamos a aprender a utilizarlos.
1. Borramos el contenido del index.js y creamos un archivo dentro de src llamado dom.js. Este archivo dom.js será el encargado de hacer toda la DOM manipulation.
2. En el dom.js, hacemos un simple console.log para que quede claro de dónde viene ese código cuando lo veamos en la consola.
console.log('I am coming from the dom.js file');
3. Dentro del mismo archivo, hacemos unos cambios en el DOM, a saber:
- obtenemos un referencia al <body> y le cambiamos el background color usando una función llamada styleBody(), por ejemplo.
- creamos una función llamada addTitle() que tomará un string y lo convertirá en un elemento <h2>.
- inyectamos ese string en el <body>.
- llamamos a ambas funciones.
console.log('I am coming from the dom.js file'); const body = document.querySelector('body'); const styleBody = () => { body.style.background = 'antiquewhite'; }; const addTitle = text => { const title = document.createElement('h2'); title.textContent = text; body.appendChild(title); }; styleBody(); addTitle('hi, I am a title coming from dom.js file');
4. Vamos al index.js e importamos el archivo dom.js ahí. Para eso debemos usar la palabra clave import y decirle a JS dónde está el archivo que queremos importar.
import './dom';
No necesitamos escribir la extensión .js del archivo, porque Webpack es muy listo y ya lo reconoce como un archivo JS. ?
5. Hacemos un console.log para ver de dónde viene cada cosa en la consola.
console.log('I am coming from the index.js');
Con esto verás que se han aplicado todos los cambios que hemos escrito en el dom.js, empezando a ejecutarse, primero ese archivo y luego el console.log del index.js, porque ese es el orden que hemos especificado. También podrás comprobar que se sigue el mismo orden en tu consola.
| Elements Console Sources Performance Network ...
I am coming from the dom.js file
I am coming from the index.js
Eso se explica porque cuando importamos archivos de esa manera, importamos el archivo entero, pero no podemos usar ni acceder a ningún bloque de código suelto automáticamente. Esto así porque cuando usamos JS modules, cada archivo tiene su propio ámbito de aplicación (en inglés, scope). Así que para poder usar bloques de código de un archivo en el index.js, debemos exportar manualmente esos bloques.
Para eso debemos escribir export delante del bloque que queramos exportar, por ejemplo:
export const styleBody = () => { body.style.background = 'antiquewhite'; }; export const addTitle = text => { const title = document.createElement('h2'); title.textContent = text; body.appendChild(title); };
e importarlo explícitamente en el index.js. Ten en cuenta que debemos usar exactamente el mismo nombre en el import que el que le hemos dado en la función.
import { styleBody, addTitle } from './dom'; console.log('I am coming from the index.js'); addTitle('hellooo')
Si ahora guardamos, verás en tu navegador que la función addTitle('hellooo') se ha ejecutado correctamente. ¡Genial! ?
6. Si ahora borramos las llamadas a las funciones desde el dom.js y las invocamos únicamente desde el index.js, todo debería seguir funcionando igual.
import { styleBody, addTitle } from './dom'; console.log('I am coming from the index.js'); addTitle('hellooo') styleBody();
? Podemos exportar cualquier cosa desde un archivo, no sólo funciones (strings, JS classes, etc). También podemos definir qué queremos exportar al final del archivo, en un solo statement, en lugar de usar la palabra export delante de cada bloque queramos exportar.
console.log('I am coming from the dom.js file'); const body = document.querySelector('body'); const styleBody = () => { body.style.background = 'antiquewhite'; }; const addTitle = text => { const title = document.createElement('h2'); title.textContent = text; body.appendChild(title); }; export { styleBody, addTitle };
Escoge la forma que más te guste. ?
Qué son los default exports y cómo usarlos
Vamos a crear un tercer archivo JS en src llamado data.js, que en el futuro podría ser responsable de lidiar con algún tipo de datos. Añadimos un array que contiene usuarios y su suscripción a spotify premium.
const users = [ { name: 'Monica', spotifyPremium: true }, { name: 'Joey', spotifyPremium: false }, { name: 'Phoebe', spotifyPremium: false }, { name: 'Ross', spotifyPremium: true }, { name: 'Rachel', spotifyPremium: false }, { name: 'Chadler', spotifyPremium: true }, ];
Si queremos exportar ese bloque, podemos hacerlo de las maneras que hemos aprendido arriba o usando un default export.
Un default export es el bloque principal de código que queremos exportar de nuestro archivo JS. Sólo está permitido tener un default export por archivo.
1. Para usarlo, escribimos las palabras clave export default seguidas del nombre del bloque que queremos exportar, en este caso users.
export default users;
2. Lo importamos en el index.js sin necesidad de usar llaves ni tan siquiera el mismo nombre (users), porque Webpack ya sabe que, como es un default export, debe exportar cualquier cosa que lleve esas keywords desde el archivo JS que le indiquemos.
Pero por consistencia, vamos a darle el mismo nombre en el export que en el import. Hacemos un console.log de users para ver el resultado en la consola.
import { styleBody, addTitle } from './dom'; import users from './data'; console.log('I am coming from the index.js'); addTitle('hellooo') styleBody(); console.log(users);
Con estos cambios ya deberías ver el array de users en tu consola. ?
Aunque sólo podamos exportar un único default value por archivo, en ese mismo archivo podemos exportar lo que queramos de la primera forma que hemos aprendido. Así que vamos a probar eso creando una función en el data.js que filtre el array users y nos devuelva únicamente los usuarios que tienen Spotify Premium.
export const getSpotiPremUsers = users => { return users.filter(user => user.spotifyPremium); };
3. Vamos al index.js e importamos ese bloque. Ya que estamos, podemos deshacernos de esta parte
console.log('I am coming from the index.js'); addTitle('hellooo') styleBody();
porque ya no la vamos a utilizar.
Creamos una variable para almacenar el nuevo array con los usuarios Premium filtrados y hacemos un console.log de esa variable. Al guardar, verás que en la consola ya aparecen los dos arrays, el normal y el filtrado. ¡Estupendo!?
import { styleBody, addTitle } from './dom'; import users, { getSpotiPremUsers } from './data'; const premiumUsers = getSpotiPremUsers(users); console.log(users, premiumUsers);
Alternativamente, podemos volver al data.js y hacer los exports al final del archivo, con la particularidad de que para los default exports debemos usar las keywords "as default".
const users = [ { name: 'Monica', spotifyPremium: true }, { name: 'Joey', spotifyPremium: false }, { name: 'Phoebe', spotifyPremium: false }, { name: 'Ross', spotifyPremium: true }, { name: 'Rachel', spotifyPremium: false }, { name: 'Chadler', spotifyPremium: true }, ]; const getSpotiPremUsers = users => { return users.filter(user => user.spotifyPremium); }; export { getSpotiPremUsers, users as default };
THE END!
¡Y así terminamos la segunda parte de esta guía sobre Babel y Webpack! Espero que hayas aprendido algo nuevo ?. Si te queda alguna duda, ¡nos vemos en los comentarios!
Si quieres seguir aprendiendo, aquí tienes la parte #3.
Y si crees que este post puede serle útil a alguien, ¡compártelo!
Otros artículos que pueden interesarte