React, Browserify y Babel en Plugins de WordPress (II): Ahora con Gulp
Este post es la continuación de React, Browserify y Babel en Plugins de WordPress (I). Si has llegado aquí, te recomiendo que leas la primera parte para conocer los conceptos básicos de Browserify.
Qué es Gulp
Gulp es una herramienta basada en nodeJS para automatizar labores repetitivas durante el desarrollo de software. Por ejemplo, podríamos escribir JavaScript modularizado en distintos archivos y luego Gulp los une y los comprime en uno sólo. También para procesar hojas de estilo en SASS. Es muy, muy útil, nos ahorra mucho tiempo, errores y dolores de cabeza.
¿Podríamos usar Grunt? Sí pero para lo que queremos hacer Grunt se antoja lento. Gulp hace uso del sistema de streams de Node dando lugar a una ejecución muy rápida a la hora de tratar con archivos de gran peso.
Para qué vamos a usar Gulp
- Para ejecutar Browserify directamente desde Gulp en lugar de ejecutar el script de npm que creamos en el post anterior. Esto tal cual no tiene ninguna ventaja, es lo que viene a continuación lo que nos beneficia.
- Comprimir JavaScript para que ocupe menos espacio en disco y sea más rápido al cargar.
- Crear un watcherpara que cuando modifiquemos algún archivo JS, Browserify se ejecute automáticamente sin tener nosotros que ejecutarlo de nuevo desde la consola.
Nuevos paquetes para npm
Necesitamos nuevas dependencias en nuestro plugin que descargaremos desde npm. Para ello podemos ejecutar:
npm install --save-dev gulp gulp-rename gulp-uglify vinyl-source-stream watchify vinyl-buffer
- gulp obviamente, sin él no podemos hacer nada
- gulp-rename nos servirá para renombrar archivos más adelante
- gulp-uglify es el compresor de JavaScript
- vinyl-source-stream: Vinyl no es más que la representación de un archivo en un objeto. Un archivo tiene dos propiedades importantes: La ruta y el contenido. Representar un archivo en Vinyl nos permitirá recibir de Browserify todo el JS procesado y tratarlo luego para renombrarlo, comprimirlo o lo que queramos. Es un paquete altamente utilizado en Gulp que por filosofía, éste último, se mantiene simple.
- watchify: Para crear nuestro watcher de archivos.
El archivo gulpfile.js
Bien, es hora de crear nuestro archivo, gulpfile.js donde indicaremos a Gulp cuáles son las tareas que queremos hacer. Para ello, creamos el archivo en la carpeta raíz de nuestro plugin y generamos la tarea por defecto o tarea default:
Dentro del archivo escribimos lo siguiente:
var gulp = require('gulp');
gulp.task( 'default', function() {
console.log("Hola Gulp");
});
Antes de ejecutar Gulp, vamos a crear un nuevo script dentro de package.json, sustituyendo el script build que generamos en el post anterior:
... "main": "index.js", "scripts": { "build": "gulp default" }, "keywords": [], ...
Ahora sólo tenemos que ejecutar
npm run build
y nos tendría que salir algo así:
Como vemos, a Gulp sólo hay que indicarle las tareas con un callback. Para ejecutar una tarea en concreto sólo hay que ejecutar gulp nombre-de-la-tarea.
Vamos a complicar un poco la cosa y vamos a hacer que Browserify se ejecute cada vez que ejecutamos npm build.
var gulp = require('gulp'),
browserify = require('browserify'),
babel = require('babelify'),
source = require('vinyl-source-stream'),
rename = require('gulp-rename');
function executeBundle(bundle) {
return bundle
.bundle()
.on("error", function (err) { console.log("Error : " + err.message); })
.pipe(source('index.js'))
.pipe(rename('app.js'))
.pipe(gulp.dest('./build'));
}
gulp.task('default', function () {
var options = {
entries: ['./src/index.js'],
debug: true
};
var bundle = browserify(options);
bundle.transform(babel.configure({presets: ["es2015", "react"]}));
return executeBundle( bundle );
});
Por pasos:
- Generamos un bundle a partir de Browserify (que ya instalamos en el post anterior y nos sirve también para Gulp, así como Babel). Le indicamos que el fichero de entrada se encuentra en src/index.js. La opción debug tendrá en cuenta los Source Maps.
- Le indicamos a Browserify que use Babel con los presets ES2015 y React. En el post anterior dicha configuración iba en el archivo .babelrc así que podemos borrar dicho archivo.
- Ejecutamos dicho Bundle y mediante el sistema de streaming vía funciones pipe (¿Os suena de UNIX?), lo recogemos con vynil-source-stream, le cambiamos el nombre a app.js y lo mandamos a la carpeta build.
Si volvemos a ejecutar npm build, veremos que al cabo de unos segundos se vuelve a generar el archivo build/app.js.
[17:37:45] Starting 'default'...[17:38:11] Finished 'default' after 26 s
Un momento, ¡¿26 segundos?! ¿No decíamos que Gulp era rápido? Y lo es, pero la primera ejecución es lenta. Cuando tengamos listo el watcher veremos que la tarea se ejecuta en muy pocos segundos.
Creación del watcher
Si no queremos estar ejecutando el script cada vez que hacemos un cambio lo más inteligente es crearnos un watcher. Para eso vamos a usar una nueva tarea llamada watch.
var gulp = require('gulp'),
browserify = require('browserify'),
babel = require('babelify'),
source = require('vinyl-source-stream'),
buffer = require('vinyl-buffer'),
rename = require('gulp-rename'),
watchify = require('watchify');
function executeBundle(bundle) {
return bundle
.bundle()
.on("error", function (err) { console.log("Error : " + err.message); })
.pipe(source('index.js'))
.pipe(rename('app.js'))
.pipe(gulp.dest('./build'));
}
gulp.task('default', function () {
var options = {
entries: ['./src/index.js'],
debug: true
};
var bundle = browserify(options);
bundle.transform(babel.configure({presets: ["es2015", "react"]}));
return executeBundle( bundle );
});
gulp.task('watch', function () {
var options = {
entries: ['./src/index.js'],
debug: true
};
var bundle = browserify(options);
bundle = watchify( bundle );
bundle.transform(babel.configure({presets: ["es2015", "react"]}));
bundle
.on('update', function( file ) {
console.log("Updated file. Bundling...");
console.log(file);
executeBundle( bundle );
})
.on('log', function( msg ) {
console.log( msg );
});
return executeBundle( bundle );
});
Es parecido a la anterior tarea sólo que ahora usamos watchifypara convertir el bundle en algo observable. Cada vez que se actualice un archivo, nos saldrá por pantalla el nombre del archivo y se volverá a ejecutar el bundle, esta vez en muy poco tiempo
Además agregamos un nuevo script en package.json:
... "main": "index.js", "scripts": { "build": "gulp default", "watch": "gulp watch" }, "keywords": [], ...
Ejecutamos npm run watch et voilà, Gulp ejecuta primero todo el bundle pero luego queda a la espera de cambios en cualquier archivo dentro de cualquier archivo que estemos importando desde src/index.js, recursivamente.
Compresión de JavaScript
Para realizar la compresión nos ayudamos del módulo uglify y sólo lo vamos a usar en la tarea defaultporque es posible que no queramos el código comprimido durante el desarrollo. También vamos refactorizar un poco agrupando código repetido. Aquí lo tenemos todo y comentado:
var gulp = require('gulp'),
browserify = require('browserify'),
babel = require('babelify'),
source = require('vinyl-source-stream'),
buffer = require('vinyl-buffer'),
rename = require('gulp-rename'),
watchify = require('watchify'),
uglify = require('gulp-uglify');
// Obtiene el bundle de Browserify
function getBundle() {
var options = {
entries: ['./src/index.js'], // Punto de entrada
debug: true // Source Maps
};
var bundle = browserify(options);
// configuración Babel + React
bundle.transform(babel.configure({presets: ["es2015", "react"]}));
return bundle;
}
function executeBundle(bundle) {
return bundle
.bundle() // Ejecuta Browserify
.on("error", function (err) {
console.log("Error : " + err.message);
}) // Muestra un mensaje en caso de error
.pipe(source('index.js')) // Recoge index.js y lo convierte a vinyl
.pipe(rename('app.js')) // Renombra el archivo a app.js
.pipe(gulp.dest('./build')); // Lo guardamos en build
}
gulp.task('bundle', function () {
// Sólo ejecuta el bundle
var bundle = getBundle();
return executeBundle(bundle);
});
gulp.task( 'default', ['bundle'], function() {
// Recogemos el fichero del bundle creado y lo comprimimos
return gulp.src(['./build/app.js'])
.pipe(uglify()) // Comprimimos
.pipe(gulp.dest('./build'));
});
gulp.task('watch', function () {
var bundle = getBundle();
// Convertimos el bundle en un watcher
bundle = watchify(bundle);
bundle
.on('update', function (file) {
// Algún fichero se ha actualizado
console.log("Updated file. Bundling...");
console.log(file);
executeBundle(bundle);
})
.on('log', function (msg) {
// Fin del proceso, mostrar un mensaje con el tiempo que se ha tardado
console.log(msg);
});
return executeBundle(bundle);
});
Hemos hecho algunas cosas aquí: Hemos creado una nueva tarea, bundle, que únicamente ejecuta el bundle y que no utilizaremos directamente. Luego hemos modificado la tarea default para que primero ejecute bundle y luego recoja el archivo resultante y lo comprima. Si no lo hiciéramos así puede que Gulp lanzara primero la ejecución del bundle y a la vez intentara comprimirlo, como el bundle tarda mucho más en componerse, se ejecutaría primero la compresión y luego se sobreescribiría con la ejecución del bundle. Esto es lo bueno y lo malo de los streams en Node, que son asíncronos.
Para demostrar ese problema podemos sustituir default por esto otro:
gulp.task( 'default', function() {
var bundle = getBundle();
executeBundle(bundle);
// Recogemos el fichero del bundle creado y lo comprimimos
return gulp.src(['./build/app.js'])
.pipe(uglify()) // Comprimimos
.pipe(gulp.dest('./build'));
});
Aparentemente la lógica es buena: Primero el bundle, luego la compresión. Pero como hemos dicho, Gulp no funciona así. En este caso lanzaría la ejecución del bundle y paralelamente lo comprimiría. El bundle acaba más tarde por lo que veremos en el archivo resultante un código sin comprimir.
Sólo falta ejecutar npm run watch para desarrollar y npm run build cuando queramos sacar el código a producción. Con la segunda ejecución vemos que el fichero build/app.js está comprimido
En el próximo episodio
Reescribiremos TODO para trabajar con Webpack. El repositorio del código visto en este post se encuentra en https://github.com/igmoweb/react-plugin