miércoles, 27 de enero de 2016

Tutorial sobre Gulp.js III: Actualización Automática de Cambios

https://github.com/gulpjs/artwork/blob/master/gulp-2x.png

Introducción

En posts anteriores se ha venido hablando sobre Gulp y como ayuda a la automatización de procesos como la minificación de archivos CSS y JavaScript y el análisis de código mediante herramientas adicionales como JSHint y JSCS.

En esta ocasión, para finalizar esta pequeña serie de introducción a Gulp, se mostrará cómo también se puede automatizar la ejecución de las tareas de Gulp, así como también una pequeña utilidad para ver más rápido los cambios aplicados en un proyecto web.

Para mantener el ejemplo sencillo se usará como base el proyecto web desarrollado en el post anterior sobre el análisis de código JavaScript: https://github.com/guillermo-varela/gulp-demo-code-analysis

Nota: en caso de clonar el repositorio desde GitHub, se deben instalar primero las dependencias que ya tiene el proyecto ejecutando:

npm install

Ejecución Automática de Tareas

Hasta ahora las tareas que se han definido en Gulp requieren que se ejecuten manualmente mediante comandos como por ejemplo "gulp jshint".

El método "watch" de Gulp permite ejecutar funciones u otras tareas de Gulp cuando se modifica algún archivo que coincida con una expresión regular (globs). Para mostrar cómo se puede usar se creará una nueva tarea en la cual se ejecutarán las tareas de inyección de archivos CSS y JavaScript (inject) y análisis de código (jshint, jscs).

gulpfile.js
'use strict';

var gulp        = require('gulp');
var inject      = require('gulp-inject');
var useref      = require('gulp-useref');
var gulpIf      = require('gulp-if');
var uglify      = require('gulp-uglify');
var gutil       = require('gulp-util');
var cssnano     = require('gulp-cssnano');
var jshint      = require('gulp-jshint');
var jscs        = require('gulp-jscs');
var del         = require('del');
var runSequence = require('run-sequence');

// Search for js and css files created for injection in index.html
gulp.task('inject', function () {
  return gulp.src('index.html', {cwd: './app'})
    .pipe(inject(
      gulp.src('**/*.js', {cwd: './app', read: false}), {
        relative: true
      }))
    .pipe(inject(
      gulp.src('**/*.css', {cwd: './app', read: false}), {
        relative: true
      }))
    .pipe(gulp.dest('./app'));
});

// Compress into a single file the ones in between of blocks "build:xx" in index.html
gulp.task('compress', ['inject'], function () {
  return gulp.src('index.html', {cwd: './app'})
    .pipe(useref())
    .pipe(gulpIf('**/*.js', uglify({
      mangle: true
    }).on('error', gutil.log)))
    .pipe(gulpIf('**/*.css', cssnano()))
    .pipe(gulp.dest('./dist'));
});

// Copies the assets into the dist folder
gulp.task('copy:assets', function () {
  return gulp.src('assets*/**', {cwd: './app'})
    .pipe(gulp.dest('./dist'));
});

// Looks for code correctness errors in JS and prints them
gulp.task('jshint', function() {
  return gulp.src('**/*.js', {cwd: './app'})
    .pipe(jshint())
    .pipe(jshint.reporter('jshint-stylish'))
    .pipe(jshint.reporter('fail'));
});

// Looks for code style errors in JS and prints them
gulp.task('jscs', function () {
  return gulp.src('**/*.js', {cwd: './app'})
    .pipe(jscs())
    .pipe(jscs.reporter())
    .pipe(jscs.reporter('fail'));
});

// Cleans the dist folder
gulp.task('clean:dist', function () {
  return del('dist/**/*');
});

// Watch changes on application files
gulp.task('watch', function() {
  gulp.watch('**/*.css', {cwd: './app'}, ['inject']);
  gulp.watch('**/*.js', {cwd: './app'}, ['jshint', 'jscs', 'inject']);
});

// Production build
gulp.task('build', function (done) {
  runSequence('jshint', 'jscs', 'clean:dist', 'compress', 'copy:assets', done);
});

Con respecto al archivo "gulpfile.js" original se realizaron los siguientes cambios:
  • Línea 68: Se crea una tarea nueva con nombre "watch".
  • Líneas 69: Se usa el método "watch" de Gulp para que al modificarse cualquier archivo CSS dentro de la carpeta con el código de la aplicación (app) se ejecute la tarea "inject" para agregar los nuevos archivos CSS creados en "index.html", así como también quitar los archivos CSS borrados en el HTML.
  • Línea 70: Similar a la línea anterior pero con los archivos JavaScript. Adicionalmente se ejecutan las tareas "jshint" y "jscs" para analizar los cambios en el código JavaScript.

De esta manera cuando se esté trabajando en este proyecto se puede ejecutar esta nueva tarea y de manera automática se agregan los nuevos archivos y se evalúa el código en cuanto se modifica para detectar errores de manera más inmediata y puntual, especialmente quienes usan múltiples monitores o un editor que muestre la consola/terminal junto con el código.

gulp watch

Using gulpfile /home/user/git/gulp-demo-watch-live/gulpfile.js
Starting 'watch'...
Finished 'watch' after 20 ms

Al introducir un error en alguno de los archivos JavaScript (por ejemplo borrar un ";") se puede ver que se ejecuta el análisis del código y se reporta el error:

gulp watch

Using gulpfile /home/user/git/gulp-demo-watch-live/gulpfile.js
Starting 'watch'...
Finished 'watch' after 20 ms
Starting 'jshint'...
Starting 'jscs'...
Starting 'inject'...
Missing semicolon after statement at /home/user/git/gulp-demo-watch-live/app/js/printer.js :
     2 |  return function(message) {
     3 |    return 'Gulp Demo says: ' + message;
     4 |  }
-----------^
     5 |})();
     6 |


1 code style error found.
Finished 'jscs' after 253 ms
gulp-inject 2 files into index.html.
gulp-inject 2 files into index.html.

app/js/printer.js
  line 4  col 4  Missing semicolon.

  ‼  1 warning

'jscs' errored after 374 ms
Error in plugin 'gulp-jscs'
Message:
    JSCS failed for: /home/user/git/gulp-demo-watch-live/app/js/printer.js
Finished 'inject' after 102 ms

En caso de tener pruebas unitarias también puede incluirse su ejecución para detectar en que punto se introducen errores en la lógica de la aplicación.

Servidor Local con Recarga Automática

Adicional a ejecutar automáticamente las tareas de Gulp, también es posible ver inmediatamente los cambios que se realizan en el navegador web, sin tener que reiniciar servidores ni recargar la página.

El plugin de Gulp "gulp-connect" permite iniciar un servidor web local con los archivos de la aplicación con la funcionalidad "LiveReload" la cual recarga automáticamente el contenido de la página en el navegador al detectar cambios en estos archivos, usando WebSockets, sin necesidad de tener que instalar extensiones adicionales en el navegador.

La idea es que este plugin recargue el contenido modificado en el navegador web, lo cual normalmente se haría con el método "watch" de Gulp, sin embargo debido a un problema (ya reportado) al usarlo de esta manera el plugin se ejecutaría dos veces. Es por ello que se usará adicionalmente "gulp-watch" para que la recarga del navegador se realice sólo una vez al detectar cambios:

npm install --save-dev gulp-connect gulp-watch

gulpfile.js
'use strict';

var gulp        = require('gulp');
var inject      = require('gulp-inject');
var useref      = require('gulp-useref');
var gulpIf      = require('gulp-if');
var uglify      = require('gulp-uglify');
var gutil       = require('gulp-util');
var cssnano     = require('gulp-cssnano');
var jshint      = require('gulp-jshint');
var jscs        = require('gulp-jscs');
var del         = require('del');
var watch       = require('gulp-watch');
var connect     = require('gulp-connect');
var runSequence = require('run-sequence');

// Search for js and css files created for injection in index.html
gulp.task('inject', function () {
  return gulp.src('index.html', {cwd: './app'})
    .pipe(inject(
      gulp.src('**/*.js', {cwd: './app', read: false}), {
        relative: true
      }))
    .pipe(inject(
      gulp.src('**/*.css', {cwd: './app', read: false}), {
        relative: true
      }))
    .pipe(gulp.dest('./app'));
});

// Compress into a single file the ones in between of blocks "build:xx" in index.html
gulp.task('compress', ['inject'], function () {
  return gulp.src('index.html', {cwd: './app'})
    .pipe(useref())
    .pipe(gulpIf('**/*.js', uglify({
      mangle: true
    }).on('error', gutil.log)))
    .pipe(gulpIf('**/*.css', cssnano()))
    .pipe(gulp.dest('./dist'));
});

// Copies the assets into the dist folder
gulp.task('copy:assets', function () {
  return gulp.src('assets*/**', {cwd: './app'})
    .pipe(gulp.dest('./dist'));
});

// Looks for code correctness errors in JS and prints them
gulp.task('jshint', function() {
  return gulp.src('**/*.js', {cwd: './app'})
    .pipe(jshint())
    .pipe(jshint.reporter('jshint-stylish'))
    .pipe(jshint.reporter('fail'));
});

// Looks for code style errors in JS and prints them
gulp.task('jscs', function () {
  return gulp.src('**/*.js', {cwd: './app'})
    .pipe(jscs())
    .pipe(jscs.reporter())
    .pipe(jscs.reporter('fail'));
});

// Cleans the dist folder
gulp.task('clean:dist', function () {
  return del('dist/**/*');
});

// Watch changes on application files
gulp.task('watch', function() {
  gulp.watch('**/*.css', {cwd: './app'}, ['inject']);
  gulp.watch('**/*.js', {cwd: './app'}, ['jshint', 'jscs', 'inject']);
  watch('**/*.html', {cwd: './app'})
    .pipe(connect.reload());
});

// Starts a development web server
gulp.task('server', function () {
  connect.server({
    root: './app',
    hostname: '0.0.0.0',
    port: 8080,
    livereload: true
  });
});

// Starts a server using the production build
gulp.task('server-dist', ['build'], function () {
  connect.server({
    root: './dist',
    hostname: '0.0.0.0',
    port: 8080
  });
});

// Production build
gulp.task('build', function (done) {
  runSequence('jshint', 'jscs', 'clean:dist', 'compress', 'copy:assets', done);
});

gulp.task('default', ['server', 'watch']);

Los cambios en "gulpfile.js" fueron:
  • Líneas 13-14: Se cargan los plugins "gulp-watch" y "gulp-connect".
  • Líneas 73-74: Mediante "gulp-watch" (variable "watch") se evalúan los cambios que se generen sobre cualquier archivo HTML dentro de la carpeta "app". Al producirse algún cambio se reiniciará el navegador. Sólo es necesario monitorear los cambios en HTML porque al modificar los archivos CSS y/o JavaScript se ejecuta la tarea "inject" la cual actualiza el archivo "index.html".
  • Líneas 78-85: Se crea una nueva tarea "server" con la cual se inicia un servidor web local usando los archivos dentro de la carpeta "app", usando como host "0.0.0.0" para que cualquier dispositivo en la misma red pueda accederlo, en el puerto "8080" y con la funcionalidad "LiveReload" habilitada para recargar el navegador automáticamente.
  • Líneas 88-94: Similar al punto anterior, se crea la tarea "server-dist", pero esta vez usando los archivos de la carpeta "dist" para probar los archivos generados al usar la tarea "build" antes de llevarlos a un entorno de producción.
  • Línea 101: Cuando se define una tarea en Gulp con el nombre "default" esta podrá ejecutarse simplemente con el comando "gulp", sin necesidad de indicar tareas o parámetros adicionales. Normalmente esta tarea es la que agrupa el conjunto de tareas que se quieren al momento de trabajar sobre el proyecto. En este caso cuando se esté trabajando en la aplicación se quiere iniciar el servidor local de pruebas (server) y que se empiecen a analizar los cambios que se hagan (watch).

gulp

Using gulpfile /home/user/git/gulp-demo-watch-live/gulpfile.js
Starting 'server'...
Finished 'server' after 42 ms
Starting 'watch'...
Finished 'watch' after 25 ms
Starting 'default'...
Finished 'default' after 5.13 µs
Server started http://localhost:8080
LiveReload started on port 35729

Como puede verse, al ejecutar simplemente "gulp" se lanzaron las tareas "server" y "watch" y al final se indica una URL para acceder a la aplicación.

Figura 1 - Aplicación en servidor web con LiveReload

Como puede verse en la figura 1, al abrir la aplicación en el navegador web no solamente se cargaron los archivos CSS, JavaScript y la imagen sino que también se tiene la conexión al WebSocket, en el puerto indicado por "gulp-connect" (35729).

La aplicación puede abrirse desde cualquier navegador que soporte WebSockets, inclusive desde dispositivos móviles en la misma red del servidor local, y al realizar cualquier cambio en CSS, JavaScript o en "index.html" el navegador actualizará el contenido para reflejar el cambio de manera casi instantánea.

Nota: El servidor que se inicia mediante "gulp-connect" sólo debe usarse para realizar en ambientes locales de desarrollo ya que no está diseñado para soportar la concurrencia, seguridad y otras características de un ambiente de producción.

Actualización 2016-05-08: El plugin "gulp-connect" ya fue corregido y usando la versión 4.0.0 se puede tener la siguiente tarea de Gulp "watch", sin necesidad de usar "gulp-watch":

...
// Watch changes on application files
gulp.task('watch', function() {
  gulp.watch('**/*.css', {cwd: './app'}, ['inject']);
  gulp.watch('**/*.js', {cwd: './app'}, ['jshint', 'jscs', 'inject']);
  gulp.watch('**/*.html', {cwd: './app'}, function (event) {
    gulp.src(event.path)
      .pipe(connect.reload());
  });
});
...

El proyecto completo se puede descargar desde: https://github.com/guillermo-varela/gulp-demo-watch-live

Conclusiones

Con lo hecho tanto en posts anteriores como en este se ha dado una introducción a Gulp mediante la demostración de algunas de las funcionalidades y plugins comunes en proyectos web (aunque puede ser usado también en otro tipo de proyectos).

Se ha mostrado también el uso que tienen los métodos principales del API de Gulp:

  • gulp.src
  • gulp.dest
  • gulp.task
  • gulp.watch

En la documentación oficial de Gulp puede encontrarse información más detallada acerca de cada método.

Existen muchos otros plugins y funcionalidades que se pueden hacer y automatizar mediante Gulp, todo depende de las necesidades específicas de cada proyecto, así que después de terminar el pequeño ejemplo desarrollado se espera que ya se pueda ver de una manera un poco más cómo Gulp puede ayudar en los proyectos web y JavaScript que se tengan.

No hay comentarios.:

Publicar un comentario