React y Babel en Plugins de WordPress (III): Ahora con Webpack

Esta entrada pertenece a la serie React y Babel en Plugins de WordPress. Si te has perdido los anteriores, te recomiendo que les eches un vistazo:

React, Browserify y Babel en Plugins de WordPress (I)

React, Browserify y Babel en Plugins de WordPress (II): Ahora con Gulp

Además puedes ver el repositorio de la serie: https://github.com/igmoweb/react-plugin.git donde hay un branch por cada entrada de la serie.

Tarde, tardísimo llego a la tercera parte de esta serie. Desde Noviembre del año pasado lleva este post en la nevera y muchas cosas han cambiado desde entonces: Webpack sacó su versión 2 y la documentación es ahora mucho más clara. También he aprendido mucho en este tiempo y he refinado mi forma de usar Webpack. ¡Tarde pero aquí vamos!

¿Qué es eso de Webpack?

Webpack es una herramienta para crear bundles. Esto no es más que distintos archivos JavaScript procesados por Webpack y luego dispuestos en alguna parte de nuestra aplicación. Por ejemplo, con Webpack puedes recoger dos o tres ficheros .js y convertirlos a uno sólo. Y pensarás que esto ya lo hace Gulp o Grunt. Y es correcto, no tienes porqué utilizar Webpack en realidad pero es que Webpack llega mucho más lejos, es más eficiente y más rápido al procesar y además su configuración básica es enormemente sencilla. Gulp, aún siendo una gran herramienta, utiliza un sistema de pipes que puede llegar a ser desesperante, con Webpack todo se simplifica pero puedes complicarlo hasta niveles estratosféricos.

Por defecto, Webpack sólo entiende JavaScript y nada más pero gracias a la multitud de extensiones que tiene, puedes procesar Sass, React, ES6 e incluso imágenes. Básicamente todo lo que se te ocurra. Además puedes definir distintos bundles para cargar dependiendo de la página donde estés. Esto hace que sea muy adecuado si quieres afinar mucho el tamaño de descarga de tu página web.

Webpack además está preparado para funcionar en producción incluso con su propio servidor y es capaz de generar HTML estático dependiendo de la configuración. Esto no lo veremos aquí porque es bastante más complicado y prefiero hacer algo más simple. Si quieres volverte muy loco puedes usar la guía de SurviveJS: https://survivejs.com/webpack/developing/

Quizás esta imagen te aclare algo, o no:

Conceptos básicos en Webpack

Los conceptos básicos de Webpack los puedes encontrar en su documentación: https://webpack.js.org/concepts/. Estos son cuatro:

  • Entry: Es el punto de entrada de la aplicación. Éste le dice a Webpack desde dónde empezar a revisar el código a procesar para luego generar el/los bundles. Para Webpack puede haber uno o varios archivos como punto de entrada y con ellos puede generar uno o varios bundles.
  • Output: El punto de salida del proceso de Webpack, es decir, dónde tiene que poner el bundle una vez procesado.
  • Loaders: Webpack trata cada fichero como un módulo, sea .css, .html, .jpg o js pero Webpack sólo entiende JavaScript. Los loaders transforman dichos archivos en módulos que luego son añadidos a la gráfica de dependencias de Webpack (algo que funciona a nivel interno). Los loaders primero identifican qué tipo de archivos deberían ser tratados por dicho loader y luego transforman dicho fichero para ser añadidos al gráfico de dependencias. Un follón, vaya, pero creo que se entenderá un poco mejor con el ejemplo que veremos más adelante.
  • Plugins: Estos son añadidos externos que afectan sólo a ciertas partes del código. Hay plugins que van desde emitir una señal en la consola si hay un error a otros que generan templates de HTML. Usarlos es sencillo en lo básico pero pueden complicarse hasta el extremo.

Una configuración básica

Primero, en la raíz de nuestra aplicación, tendríamos que crear un fichero llamado webpack.config.js e incluir el siguiente código:

Es un ejemplo muy sencillo: Le decimos a Webpack que recoja el fichero foo.js, genere su gráfico de dependencias y una vez procesado, pase el contenido a ./dist/foo.bundle.js. Puede que foo.js sea un código pequeño pero también podría ser que dicho fichero llamara a otros mediante por ejemplo, require( ‘foo2.js’ ). De esta forma, Webpack procesará todos los  ficheros en uno sólo.

Webpack en WordPress

En esta entrada no vamos a complicarnos mucho. Bastará con el mismo código en React que teníamos en los dos posts anteriores y un plugin de WordPress básico. Vamos a olvidarnos de Gulp así que el fichero gulpfile.js lo podemos borrar y crearemos nuestro primer webpack.config.js. Así tengo yo mi estructura antes de empezar:

Es decir, hemos reemplazado gulpfile.js por webpack.config.js. El resto quedaría igual.

Ahora vamos a instalar Webpack como dependencia local:

npm install --save-dev webpack

Y crearemos nuestra primera configuración:

Si te fijas es casi igual que el ejemplo anterior: Un punto de entrada (src/index.js) y un punto de salida (build/app.js).

Recordemos que index.js contiene una aplicación hecha en React que incluye el componente App.js y lo renderiza donde queramos.

Ejecutando Webpack por primera vez

Aunque no te lo creas, estamos muy muy cerca de que Webpack funcione por primera vez y de hecho vamos a ejecutarlo con el siguiente comando:

./node_modules/.bin/webpack

Debería dar un error tal que así:

ERROR in ./src/index.js
Module parse failed: /Users/ignacio/source/vvv/www/wordpress/wp-content/plugins/react-plugin/src/index.js Unexpected token (5:16)
You may need an appropriate loader to handle this file type.
| import App from './App';
| 
| ReactDOM.render(<App />, document.getElementById('app') );
 @ multi ./src/index.js

Eso es porque index.js está escrito en una versión de JavaScript que Webpack no conoce, es decir, está escrito en React. Para que Webpack entienda React y ES6 necesitamos un loader.

Puede que te suene porque hemos trabajado ya con ellos en los dos capítulos anteriores: Vamos a necesitar el loader de Babel pero diciéndole que lo que hay escrito en nuestros .js es ES6 versión 2015 y React:

Lo único que hemos añadido es el objeto module y dentro unas rules. En este caso, le decimos a Webpack que cualquier fichero de tipo .js deberá ser tratado con el loader de Babel y las opciones indican qué tipo de ES6 vamos a usar (es2015) y que está escrito en React. Además le indicamos que esto sólo debe afectar a los ficheros contenidos dentro de ./src.

Por si no los tienes instalados todavía, puedes ejecutar lo siguiente para instalar el loader de Babel y todos los presets que necesitamos. Si ya los tenías es posible que sólo necesites instalar babel-loader pero tampoco pierdes nada ejecutando el comando completo:

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

Y volvemos a ejecutar:

./node_modules/.bin/webpack

Seguidamente veremos un chorizo enorme incomprensible pero si te fijas es fácil de descifrar:

Hash: b1f1b11399e955d5b009
Version: webpack 2.4.1
Time: 3880ms
Asset    Size  Chunks                    Chunk Names
app.js  739 kB       0  [emitted]  [big]  main
   [8] ./~/react-dom/lib/ReactInstrumentation.js 601 bytes {0} [built]
  [10] ./~/react-dom/lib/ReactUpdates.js 9.53 kB {0} [built]
  [19] ./~/react/lib/React.js 3.32 kB {0} [built]
  [80] ./~/react/react.js 56 bytes {0} [built]
  [81] ./src/index.js 466 bytes {0} [built]
  [82] ./src/App.js 2.29 kB {0} [built]
  [99] ./~/react-dom/index.js 59 bytes {0} [built]
 [113] ./~/react-dom/lib/ReactDOM.js 5.14 kB {0} [built]
 [172] ./~/react/lib/ReactChildren.js 6.19 kB {0} [built]
 [173] ./~/react/lib/ReactClass.js 26.9 kB {0} [built]
 [174] ./~/react/lib/ReactDOMFactories.js 5.53 kB {0} [built]
 [175] ./~/react/lib/ReactPropTypes.js 500 bytes {0} [built]
 [177] ./~/react/lib/ReactPureComponent.js 1.32 kB {0} [built]
 [178] ./~/react/lib/ReactVersion.js 350 bytes {0} [built]
 [183] multi ./src/index.js 28 bytes {0} [built]

Lo primero que nos da es un nombre de hash. A nosotros no nos va a importar demasiado en este tutorial. Luego aparece la versión de Webpack, el tiempo de procesado y a continuación la lista de bundles. En este caso sólo uno de 739Kb, app.js, que se genera a partir de toda esa lista de ficheros que vemos. Hay muchos pero entre ellos podemos distinguir dos nuestros: index.jsApp.js. El resto son de React.

Si además abrimos el fichero generado, app.js, veremos un JavaScript bastante difícil de entender. Webpack introduce un poco de JS que utiliza para cargar módulos internamente. Tampoco nos importa, lo único que tenemos que comprobar es si el plugin funciona. Para ello vamos a cargar la URL

wp-admin/admin.php?page=react-plugin

Y debería aparecer lo siguiente:

Que es lo que nos aparecía en la entrada anterior. Si tienes dudas puedes acudir al repositorio de esta serie y echarle un vistazo al branch step-3.

En resumen: Webpack ha transpilado el código React y ES6 gracias a Babel, luego genera un gráfico de dependencias y seguidamente genera el bundle final en build/app.js.

Facilitando las cosas con npm

Para que no tengamos que ejecutar el archivo binario de Webpack cada vez que queremos generar el bundle, vamos a añadir un par de scripts a package.json:

...
"scripts": {
  "watch": "webpack --watch",
  "build": "webpack -p"
},
...

Es bastante obvio lo que hacemos con esto:

npm run watch

ejecutará Webpack por primera vez y luego quedará esperando a cualquier cambio que hagamos en los ficheros del bundle.

npm run build

Preparará en bundle para producción. Esto es, dist/app.js quedará comprimido y con el menor tamaño posible. Si lo ejecutamos, veremos que el tamaño de la aplicación se reduce muchísimo (de 739 a 144 KB en mi caso).

Source Maps

Cuando estamos haciendo  debug y nuestros scripts están comprimidos, encontrar un error o seguir la traza de ejcución es muy complicado. Los Source Maps acuden al rescate en estos casos. Ayudan a mapear el fichero comprimido de forma que en el navegador podamos ver el código original (sin comprimir) y podamos crear puntos de break en la pestaña Sources del navegador.

Webpack incluye varias formas de generarlos, cada una tiene sus pros y contras. Desgraciadamente, la documentación de Webpack no explica del todo bien las diferencias y tienes que saber mucho de Source Maps (todo una ciencia) para entender cuál es el mejor para nuestro modo de trabajar. Yo voy a usar dos: Uno para desarrollo y otro para producción. El de desarrollo es más rápido de generar ya que necesitamos rapidez a la hora de hacer un rebuild mientras el de producción nos importa menos.

Source Map en desarrollo

El Source Map más rápido es eval pero tiene un problema: El navegador no ayudará a debugar sobre el código original sino sobre el código transpilado a ES5. Es decir, que no veremos React por ninguna parte sino el código de React una vez transformado a JavaScript real. Quizás nos puede venir bien para otro tipo de proyecto que no use React pero con el que tenemos entre manos no nos vale.

El siguiente que nos interesa es eval-source-map, parecido al anterior y más lento al generar pero rápido a la hora de regenerar cuando hacemos watch. Además nos permite pasear por el código original en lugar del código transpilado desde el navegador.

devtool:'eval-source-map'

Nuestro webpack.config.js quedaría así:

Ahora ejecutamos watch de nuevo y veremos que nuestro app.js ha incrementado su tamaño considerablemente. Cuando abrimos el inspector de Chrome podemos ver la siguiente estructura:

Bajo webpack:// encontramos todos los ficheros .js originales. Ahora podemos poner puntos de break en los ficheros originales, ¿O no? Pues parece que a día de hoy (28 de Abril de 2017), Chrome no los respeta así que hay que buscar otra fórmula. Entering…

debugger;

Este comando JavaScript hará que el navegador pare en el punto que queramos. Chrome detendrá la ejecución y desde ahí podemos ejecutar JavaScript línea por línea. Puedes probar donde quieras y verás los resultados: La ejecución parará en el fichero original en lugar del app.js.

Source Maps en producción

Para la versión de producción vamos a usar source-map, que es el más apropiado para estos entornos. Generará aparte un fichero .map  que le servirá al navegador para mapear todo el código original. Además, Chrome respetará los puntos de break. Es lento de generar pero no nos importa ya que el bundle para producción lo generaremos muy de vez en cuando.

¿Cómo diferenciamos si estamos preparando dicho bundle para desarrollo o producción? Con Webpack podemos pasar variables desde los scripts de package.json para indicar a Webpack si estamos trabajando con la versión de desarrollo o producción. Para ello vamos a cambiar la sección de scripts  en package.json:

"scripts": {
  "watch": "webpack --watch",
  "build": "webpack -p --env=prod"
},

El único cambio es –env=prod, con esto mandamos una variable llamada env al fichero webpack.config.js. Cuando hacemos watch, el valor de dicha variable es undefined. Para poder utilizarla sin embargo hay que cambiar un par de cosas. Primero vamos a crear un objeto común de configuración sin la propiedad devtool. Luego, dependiendo de la configuración (desarrollo/producción), añadiremos dicha propiedad. Además, module.exports dejará de ser un simple objeto para pasar a ser una función que evaluará en qué entorno nos encontramos:

Ahora tenemos dos configuraciones distintas:

  • npm run watch

    El bundle generado es mayor, los Source Maps se incluyen en el mismo fichero pero la regeneración del fichero es rápida.

  • npm run build

    El bundle ahorra mucho espacio y los Source Maps se generan en un fichero .map aparte que sólo cargará el navegador cuando abrimos el inspector.

 

Corolario

Webpack ofrece un universo ya conocido de posibilidades pero con mucha más elasticidad y eficiencia. Además la configuración es relativamente sencilla y los conceptos, aunque algo abstractos al principio, encajan liuego fácilmente una vez los comprendemos.

Podemos ir mucho más allá. Webpack ofrece plugins y loaders para:

  • CSS modular
  • Un servidor (webpack-dev-server) de desarrollo que recargará la página cuando haya un cambio en nuestro código. Hay que decir que para nuestro caso no es muy útil porque es WordPress el que maneja toda la parte backend y configurar el servidor para que recargue PHP es bastante complicado.
  • Loader de imágenes
  • Multitud de plugins para hacernos la vida más sencilla a la hora de desarrollar.

Como última recomendación, Jetpack está hecho en React y utiliza Webpack para el desarrollo (ojo, a día de hoy utiliza Webpack 1 y no el 2 por lo que hay cosas que varían en la configuración).

¡A explorar!

React, Browserify y Babel en Plugins de WordPress (I)

Al grano. Vamos crear un plugin muy sencillo que utilice React en un menú del wp-admin. El plugin generará una página de administración y cargaremos React ahí mismo. Obviamente lo podríamos usar para crear una interfaz en el front y no sólo en el admin.

Para los más impacientes hay un repositorio donde voy poniendo el código: https://github.com/igmoweb/react-plugin/. Para ver este post en concreto hay que elegir la rama step-1.

Qué necesitamos

  • ReactEs una famosísima librería para generar interfaces de usuario. No es un todo en uno como Angular, React sólo sirve para crear la interfaz, no contiene nada para modelar datos y demás. No profundizaré mucho en React porque no es el objetivo de este post.
  • BrowserifyNos ayudará a generar un único archivo JavaScript que contiene todos los módulos necesarios. A este archivo final se le llama en inglés bundle. Browserify nos permite importar paquetes o módulos desde distintos archivos (véase React entre otros) para luego juntarlos todos en uno sólo, que es el archivo que usaremos en nuestro menú wp-admin.
  • BabelEs un transcompilador de ES6 (ECMAScript 6). Convierte código JavaScript ES6 en código ES5, que es el que entienden todos los navegadores hoy en día. Trabajar en ES6 es más fácil que en ES5, y más divertido.

¿Por qué no Webpack? Te estarás preguntando, amigo informático “listillo”. Pues porque Webpack es complicado, un monstruo, una bestia que hace DE TODO, un Browserify supervitaminado. Antes de meternos en Webpack podemos usar herramientas más sencillas como Browserify porque luego Webpack se entiende mejor.

Aquí un dibujo de cómo funciona más o menos todo a la vez:

Diagrama de React, Browserify y Babel

Requisitos previos

Tenemos que instalar Node.js en nuestro ordenador. Node incluye un gestor de paquetes llamado npm que nos servirá para gestionar las dependencias de nuestro proyecto. Como nuestro plugin utiliza React y Browserify, éstos se convierten en dependencias del plugin.

El plugin

El plugin que vamos a crear es muy sencillo: Crea un menú de administración en WordPress desde donde cargará el archivo JS que sacará por pantalla el HTML necesario gracias a React. Para esto creamos una nueva carpeta en wp-content/plugins, por ejemplo react-plugin. Ahora creamos el archivo principal del plugin (no va a haber más): react-plugin.php, y empezamos a picar código:

Es un plugin bastante tonto: Genera un menú (cuyo ID generado lo guarda como atributo de la clase React_Plugin, ahora veremos porqué) y nada más. El contenido del menú (React Plugin), es un simple div#app. Dentro de este div, React generará el código HTML.

Estado de las carpetas:

-wp-content/plugins/react-plugin
  * react-plugin.php

Instalación de dependencias

Gracias a npm, vamos a empezar a instalar todas las dependencias para nuestro proyecto. En el terminal, nos situamos en la carpeta de nuestro plugin y escribimos:

npm init -y

Esto nos genera un archivo package.json, un documento que npm necesita para saber qué instalar y qué características tiene el proyecto. Lo vamos a dejar tal cual porque no quiero meterme mucho en él, es todo un mundo.

Instalamos las dependencias:

npm install --save react react-dom

npm install --save-dev browserify babelify babel-preset-react babel-preset-es2015

Con esto, veremos que se ha creado una carpeta nueva, node_modules, donde se habrán descargado las siguientes dependencias:

  • React y ReactDOM. Éste último sirve para renderizar los componentes de React.
  • Browserify
  • Babelify: Un plugin de Browserify para transcompilar el código ES6 a ES5. Al final es Babel adaptado a Browserify, nada más
  • Babel Preset React y Babel Preset ES2015: Son paquetes de Babel que contienen la configuración necesaria para transcompilar de ES6 (la versión de Junio de 2015) a ES5. Podríamos usar otro paquete como ES2016 o ES2017 si quisiéramos una versión de ES6 más avanzada. El ES2015 es más que suficiente. El Preset de React ayuda a transcompilar el código de React a ES5 también.

Una vez todo instalado, el fichero package.json debería quedar más o menos así:

{
  "name": "react-plugin",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "react": "^15.3.2",
    "react-dom": "^15.3.2"
  },
  "devDependencies": {
    "babel-preset-es2015": "^6.16.0",
    "babel-preset-react": "^6.16.0",
    "babelify": "^7.3.0",
    "browserify": "^13.1.0"
  }
}

Estado de las carpetas:

-wp-content/plugins/react-plugin
  - node_modules
  * package.json
  * react-plugin.php

Estructura de carpetas

Dentro de la carpeta del plugin vamos a crear dos carpetas adicionales:

  • src: Aquí vamos a meter todos los archivos JS escritos en ES6 así como los componentes de React.
  • build: Con ayuda de Browserify, a partir de todo lo que hay en src + React (que está en node_modules/react), se generará un archivo app.js dentro de este directorio. Dicho archivo será el que cargaremos en nuestro menú de WordPress.

Estado de las carpetas:

-wp-content/plugins/react-plugin
  - build
  - node_modules
  - src
  * package.json
  * react-plugin.php

Componentes de React

Vamos a crear el componente principal de nuestro proyecto. Va a ser un simple Hello WordPress

src/App.js

src/index.js

Es algo bastante simple: App.js es el elemento principal de la interfaz. Es un simple div con un Hello WordPress dentro. Para crear un componente en React, extendemos simepre de la clase React.Component (en ES5 sería más feo y engorroso). export default significa que cuando queramos importar (con import from) dicho archivo desde otro fichero JS, es la clase App la que se exporta.

index.js  es el archivo principal de la aplicación. Lo que hace es importar el componente App (ya sabemos, con import from) y renderizarlo en el elemento HTML div#app. También importa React y ReactDOM para pintar en la página. Recordad que este elemento ya lo tenemos en nuestro menú de administración.

-wp-content/plugins/react-plugin
  - build
  - node_modules
  - src
     * App.js
     * index.js
  * package.json
  * react-plugin.php

Configuración de Babel

Si pusiéramos a trabajar ahora mismo a Browserify, podríamos generar el archivo build/app.js pero todo saldría en ES6, cosa que los navegadores todavía no entienden bien. Tenemos que decirle a Browserify que utilice Babel y que éste use los presets que nosotros le digamos. Para ello creamos un nuevo fichero:

.babelrc

{
  "presets": ["es2015", "react"]
}

Con esto le estamos diciendo a Babel que use los presets (que ya tenemos instalados en node_modules) ES2015 y React.

-wp-content/plugins/react-plugin
  - build
  - node_modules
  - src
    * App.js
    * index.js
  * .babelrc
  * package.json
  * react-plugin.php

Ejecución de Browserify

Emoción, vamos a generar por primera vez nuestro fichero build/app.js con su React, sus componentes y todo traducido a ES5 por un módico precio de cero maravedíes. A su vez, nuestros componentes estarán separados en sus respectivas carpetas para una mejor organización del código. Es un win-win en toda regla.

Para ello ejecutamos la siguiente sentencia en nuestro terminal:

./node_modules/.bin/browserify -t [ babelify ] src/index.js -o build/app.js

Con esto le decimos a Browserify: “Usando el plugin babelify, transforma el archivo src/index.js y todo lo que importa éste, en build/app.js. Si lo ejecutáis deberíais ver un archivo súper tocho en build/app.js.

Para no tener que ejecutar un comando tan largo, package.json tiene un apartado para crear scripts propios. Añadimos lo siguiente:

{
  "name": "react-plugin",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "browserify src/index.js -t [ babelify ] -o build/app.js"
  },
...

Ahora simplemente ejecutamos

npm run build

Y Browserify se ejecutará tal y como hemos hecho antes.

Estado de las carpetas:

-wp-content/plugins/react-plugin
  - build
    * app.js
  - node_modules
  - src
    * App.js
    * index.js
  * .babelrc
  * package.json
  * react-plugin.php

De vuelta al plugin

Vale, ya tenemos generado nuestro build/app.js pero no lo estamos usando en el plugin de ninguna manera. Hay que hacer uso de hooks y funciones de WordPress para ello. Aquí está la versión definitiva del mini plugin que hemos hecho:

Si nos fijamos, utilizando el hook admin_enqueue_scripts, añadimos nuestro build/app.js sólo en la página de nuestro plugin gracias a la comparación $this->page_id === $hook. De esta forma no agregamos nuestro JS en todas las páginas. Sólo una nota más: El script debe ir en el footer, de otra forma no funcionará, ya que en el header todavía no existe nuestro div#app.

Próximamente…

En el siguiente post vamos a adentrarnos en cómo automatizar todo esto. Es un poco fastidioso ejecutar npm build cada vez que queremos cambiar algo. Para echo, usaremos un watcher que detecte los cambios en los ficheros JS y que rehaga el build en build/app.js.