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.

domingo, 24 de enero de 2016

Tutorial sobre Gulp.js II: Análisis de Código JavaScript

Introducción

En un post anterior se dio una introducción a Gulp mediante un ejemplo que concatena y minifica de los archivos JavaScript y CSS de un proyecto web.

En esta ocasión se mostrará como Gulp también puede ayudar a automatizar el proceso de análisis estático del código JavaScript mediante JSHint y JSCS.

Debido a que JavaScript es un lenguaje interpretado, dinámico y no tipado tanto la detección de algunos errores que pueden considerarse comunes o el seguimiento de estándares de programación definidos pueden ser procesos complejos, permitiendo que algunas de estas fallas se encuentren cuando el sistema ya está en un ambiente de producción (de hecho también sucede en lenguajes compilados fuertemente tipados).

JSHint es una herramienta que revisa el código JavaScript buscando no sólo errores comunes sino también algunas prácticas configurables, como por ejemplo el uso de comparación estricta (eqeqeq) o limitar el número de bloques anidados (maxdepth), de manera similar a como lo hacen FindBugs o PMD con código Java.

Por otro lado JSCS es una herramienta que revisa el código JavaScript para asegurar que se están siguiendo las reglas de estilos configurados, por ejemplo mantener un número máximo de caracteres en cada línea (maximumLineLength) o el uso de CamelCase en las variables (requireCamelCaseOrUpperCaseIdentifiers). Puede compararse con la herramienta Checkstyle para Java.

Para mantener el ejemplo sencillo se usará como base el proyecto web desarrollado en aquel post anterior sobre Gulp: https://github.com/guillermo-varela/gulp-demo

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

npm install

JSHint

Para empezar a usarlo se necesita instalar 2 plugins de Gulp:
  • gulp-jshint: Permite usar JSHint desde tareas Gulp.
  • jshint-stylish: Presenta los errores encontrados por JSHint en un reporte detallado.
Ambos se pueden instalar (como dependencias de desarrollo) mediante el siguiente comando:

npm install --save-dev gulp-jshint jshint-stylish

JSHint tiene varias reglas para validar el código fuente, pero también es posible configurarlo bien sea creando un archivo JSON ".jshintrc", creando una propiedad "jshintConfig" en el archivo "package.json" o con comentarios en el propio código JavaScript (directivas) como por ejemplo "/* jshint strict: true */".

En este caso se tendrá un archivo ".jshintrc" en la raíz del proyecto con algunas reglas a manera de ejemplo. El conjunto completo de reglas (opciones) y sus definiciones se puede encontrar en: http://jshint.com/docs/options.

{
  "node": true,
  "browser": true,
  "bitwise": true,
  "curly": true,
  "eqeqeq": true,
  "forin": true,
  "freeze": true,
  "latedef": "nofunc",
  "noarg": true,
  "nonbsp": true,
  "nonew": true,
  "undef": true,
  "unused": true,
  "strict": true,
  "globals": {
    "angular": false,
    "google": false
  }
}

Ya teniendo JSHint instalado y configurado se procede entonces a crear la tarea en Gulp:

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');

// 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'));
});

Con respecto al archivo "gulpfile.js" original se realizaron los siguientes cambios:
  • Línea 10: Se carga el módulo "gulp-jshint".
  • Líneas 44-48: Se crea una tarea nueva con nombre "jshint" la cual ejecuta JSHint sobre todos los archivos ".js" de la carpeta con el código de la aplicación "app" y lleva los errores encontrados al módulo de reportes "jshint-stylish". Al final se usa ".pipe(jshint.reporter('fail'))" para indicar que en caso de encontrar errores en el código la tarea "jshint" debe terminar en error y no continuar con lo demás, lo cual es muy importante por ejemplo para los ambientes de integración continua (evitar despliegues si hay errores por ejemplo).

Ahora ya es posible ejecutar JSHint con la nueva tarea de Gulp mediante el siguiente comando:

gulp jshint

Using gulpfile /home/user/git/gulp-demo-code-analysis/gulpfile.js
Starting 'jshint'...

app\js\hello.js
  line 2  col 3  Missing "use strict" statement.
  line 4  col 4  Missing semicolon.
  line 1  col 5  'helloWorld' is defined but never used.

  ×  1 error
  ‼  2 warnings


app\js\printer.js
  line 2  col 3  Missing "use strict" statement.
  line 4  col 4  Missing semicolon.
  line 1  col 5  'printer' is defined but never used.

  ×  1 error
  ‼  2 warnings

'jshint' errored after 85 ms
Error in plugin 'gulp-jshint'
Message:
    JSHint failed for: app\js\hello.js, app\js\printer.js

Como puede verse el reporte que presenta "jshint-stylish" es muy detallado al indicar el error, y su ubicación exacta en cada archivo. En este caso advierte sobre la falta de la expresión "use strict", el signo ";" (punto y coma - semicolon) al finalizar y retornar la función creada y su asignación a una variable, que aunque se usa en "index.html" no se usa dentro de dichos archivos, lo cual se resuelve así:

app\js\hello.js
'use strict';

/*jshint unused:false*/
var helloWorld = (function () {
  return function () {
    return 'Hello World...';
  };
})();

app\js\printer.js
'use strict';

/*jshint unused:false*/
var printer = (function () {
  return function (message) {
    return 'Gulp Demo says: ' + message;
  };
})();


Al verificar nuevamente con JSHint se obtiene:
gulp jshint

Using gulpfile /home/user/git/gulp-demo-code-analysis/gulpfile.js
Starting 'jshint'...
Finished 'jshint' after 77 ms

Es de notar que en este caso se usó la directiva "jshint unused:false" para deshabilitar la validación de la regla "unused" desde su ubicación en los respectivos archivos.

JSCS

Esta herramienta también tiene un plugin de npm disponible: gulp-jscs

npm install --save-dev gulp-jscs

Su configuración puede realizarse mediante un archivo JSON llamado ".jscsrc" en la raíz del proyecto. Allí se pueden indicar las reglas que se requieran o inclusive usar una pre-configuración ya definida las cuales están basadas en reglas o recomendaciones que han dado algunas organizaciones como Google, JQuery o Wikimedia, tanto para sus desarrollos internos como para aquellas personas que contribuyen en sus proyectos open-source.

.jscsrc

{
  "preset": "google",
  "maximumLineLength": {
    "value": 160
  }
}

Allí se indica que se usarán las reglas definidas por los estándares de Google, pero se sobrescribe el límite en la longitud de las líneas (maximumLineLength). Inicialmente es 80 y se amplía a 160.

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');

// 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'));
});

Los cambios realizados son:
  • Línea 11: Se carga el módulo "gulp-jscs".
  • Líneas 53-58: Se crea una tarea nueva con nombre "jscs" la cual ejecuta JSCS sobre todos los archivos ".js" de la carpeta con el código de la aplicación "app" y finalmente lleva los errores encontrados al módulo de reportes que tiene por defecto.
Al ejecutar esta nueva tarea se obtiene:

gulp jscs

Using gulpfile /home/user/git/gulp-demo-code-analysis/gulpfile.js
Starting 'jscs'...
Illegal space before opening round brace at /home/user/git/gulp-demo-code-analysis/app/js/hello.js :
     1 |var helloWorld = (function () {
----------------------------------^
     2 |  return function () {
     3 |    return 'Hello World...';

Illegal space before opening round brace at /home/user/git/gulp-demo-code-analysis/app/js/hello.js :
     1 |var helloWorld = (function () {
----------------------------------^
     2 |  return function () {
     3 |    return 'Hello World...';

Illegal space before opening round brace at /home/user/git/gulp-demo-code-analysis/app/js/hello.js :
     1 |var helloWorld = (function () {
     2 |  return function () {
-------------------------^
     3 |    return 'Hello World...';
     4 |  };

Illegal space before opening round brace at /home/user/git/gulp-demo-code-analysis/app/js/hello.js :
     1 |var helloWorld = (function () {
     2 |  return function () {
-------------------------^
     3 |    return 'Hello World...';
     4 |  };


4 code style errors found.
Illegal space before opening round brace at /home/user/git/gulp-demo-code-analysis/app/js/printer.js :
     1 |var printer = (function () {
-------------------------------^
     2 |  return function (message) {
     3 |    return 'Gulp Demo says: ' + message;

Illegal space before opening round brace at /home/user/git/gulp-demo-code-analysis/app/js/printer.js :
     1 |var printer = (function () {
-------------------------------^
     2 |  return function (message) {
     3 |    return 'Gulp Demo says: ' + message;

Illegal space before opening round brace at /home/user/git/gulp-demo-code-analysis/app/js/printer.js :
     1 |var printer = (function () {
     2 |  return function (message) {
-------------------------^
     3 |    return 'Gulp Demo says: ' + message;
     4 |  };

Illegal space before opening round brace at /home/user/git/gulp-demo-code-analysis/app/js/printer.js :
     1 |var printer = (function () {
     2 |  return function (message) {
-------------------------^
     3 |    return 'Gulp Demo says: ' + message;
     4 |  };

4 code style errors found.
'jscs' errored after 288 ms
Error in plugin 'gulp-jscs'
Message:
    JSCS failed for: /home/user/git/gulp-demo-code-analysis/app/js/hello.js, /home/user/git/gulp-demo-code-analysis/app/js/printer.js

Básicamente está reportando que los paréntesis en los que se indican los parámetros de cada función no deben estar precedidos de espacios, lo cual se ajusta de la siguiente manera:

app\js\hello.js
'use strict';

/*jshint unused:false*/
var helloWorld = (function() {
  return function() {
    return 'Hello World...';
  };
})();

app\js\printer.js
'use strict';

/*jshint unused:false*/
var printer = (function() {
  return function(message) {
    return 'Gulp Demo says: ' + message;
  };
})();

Nota: JSCS tiene la opción "fix", la cual permite que automáticamente resuelva este tipo de errores, aunque personalmente prefiero resolver los errores manualmente para revisar caso por caso los reportes y tener mayor control del código desarrollado.


Tarea de Construcción

Ahora que se tienen varias tareas en la configuración de Gulp, se puede crear una sola que agrupe las tareas de análisis de código, concatenación y minificación.

gulp.task('build', ['jshint', 'jscs', 'compress', 'copy:assets']);

Al ejecutar esta tarea se tiene este resultado:
gulp build

Using gulpfile /home/user/git/gulp-demo-code-analysis/gulpfile.js
Starting 'jshint'...
Starting 'jscs'...
Starting 'inject'...
Starting 'copy:assets'...
Finished 'copy:assets' after 8.72 ms
Finished 'jscs' after 838 ms
gulp-inject 2 files into index.html.
gulp-inject 2 files into index.html.
Finished 'jshint' after 1.18 s
Finished 'inject' after 288 ms
Starting 'compress'...
Finished 'compress' after 2.9 s
Starting 'build'...
Finished 'build' after 19 µs

La tarea "inject" fue ejecutada a pesar de no estar incluida explicitamente en "build" debido a que es una dependencia de "compress". También puede apreciarse que las tareas "copy:assets", "jshint" y "jscs" fueron ejecutadas simultáneamente ya que no hay dependencias entre estas.

Cuando se empieza a tener muchas tareas de Gulp se vuelve dificil hacer seguimiento a las dependencias de cada tarea con la sintaxis actual:

gulp.task('taskName', ['dependency1', 'dependency2', ...], function () {
  return ...
});

Para Gulp 4.0 se planea tener una manera alternativa de declarar una lista de tareas que se deben ejecutar secuencialmente, en un orden determinado:

gulp.task('taskName', gulp.series('task1', 'task2', function(done) {
  done();
}));

Al momento de escribir este post la versión estable de Gulp es 3.9.0, así que mientras tanto se usará como alternativa "run-sequence".

npm install --save-dev run-sequence

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 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'));
});

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

Los cambios realizados son:
  • Línea 12: Se carga el módulo "run-sequence".
  • Líneas 61-63: Se crea una tarea nueva con nombre "build" la cual ejecuta el análisis del código JavaScript ("jshint", "jscs") y posteriormente la concatenación y minificación ("compress") de manera secuencial.

Al usar esta nueva tarea se tiene el siguiente resultado:
gulp build

Using gulpfile /home/user/git/gulp-demo-code-analysis/gulpfile.js
Starting 'build'...
Starting 'jshint'...
Finished 'jshint' after 89 ms
Starting 'jscs'...
Finished 'jscs' after 254 ms
Starting 'inject'...
gulp-inject 2 files into index.html.
gulp-inject 2 files into index.html.
Finished 'inject' after 22 ms
Starting 'compress'...
Finished 'compress' after 397 ms
Starting 'copy:assets'...
Finished 'copy:assets' after 1.16 ms
Finished 'build' after 773 ms

Como puede verse todas las tareas se ejecutaron dentro de "build" de manera secuencial y ordenada (una tarea sólo comenzó cuando la anterior terminó).


Limpieza de la Carpeta de Construcción

Aprovechando que ahora la construcción (build) se ejecuta siguiente un orden definido, puede incluirse una tarea adicional que borre el contenido de la carpeta "dist" antes de que se empiecen a copiar los archivos, con el fin de evitar que queden archivos que fueron borrados o re-nombrados en "app", sin correr el riesgo de borrar los archivos que se están copiando.

Para esto se usará el módulo "del". Aunque no es un plugin realmente desarrollado para Gulp, al ser un módulo/paquete disponible desde npm se puede usar también en tareas de Gulp.

npm install --save-dev del

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/**/*');
});

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

Los cambios realizados son:
  • Línea 12: Se carga el módulo "del".
  • Líneas 63-65: Se crea una tarea nueva con nombre "clean:dist" la cual borra el contenido dentro de "dist".
  • Línea 68: Adición de la nueva tarea en "build".

Ejecutando nuevamente la construcción se tiene:
gulp build

Using gulpfile /home/user/git/gulp-demo-code-analysis/gulpfile.js
Starting 'build'...
Starting 'jshint'...
Finished 'jshint' after 79 ms
Starting 'jscs'...
Finished 'jscs' after 268 ms
Starting 'clean:dist'...
Finished 'clean:dist' after 19 ms
Starting 'inject'...
gulp-inject 2 files into index.html.
gulp-inject 2 files into index.html.
Finished 'inject' after 31 ms
Starting 'compress'...
Finished 'compress' after 382 ms
Starting 'copy:assets'...
Finished 'copy:assets' after 4.91 ms
Finished 'build' after 795 ms

El proyecto completo puede descargarse desde: https://github.com/guillermo-varela/gulp-demo-code-analysis

Conclusiones

Con algunas cuantas tareas de Gulp adicionales se ha logrado tener un flujo de construcción para un proyecto web que, aunque en el presente ejemplo es bastante pequeño, puede ser aplicado en proyectos de mayor tamaño sin necesidad de cambio alguno.

En el próximo post se mostrará cómo se puede también automatizar la actualización de los cambios que se hacen durante el desarrollo.

Referencias

http://jshint.com/docs
http://jshint.com/docs/options
https://github.com/jshint/jshint
https://www.npmjs.com/package/gulp-jshint
http://jscs.info
https://google.github.io/styleguide/javascriptguide.xml
https://www.npmjs.com/package/gulp-jscs
https://scotch.io/tutorials/automate-your-tasks-easily-with-gulp-js
https://github.com/gulpjs/gulp/blob/master/docs/recipes/delete-files-folder.md

miércoles, 20 de enero de 2016

Tutorial sobre Gulp.js: Automatizando Tareas en Proyectos Web y JavaScript

Introducción

El desarrollo de aplicaciones web desde hace ya varios años se ha ido transformando de ser principalmente código del lado del servidor (con lenguajes como Java, PHP o Ruby) a tener cada vez más componentes del lado del cliente usando JavaScript, en su momento con librerías como JQuery y ahora con frameworks más completos como AngularJS o Ember.js.

Para proyectos grandes normalmente se generan muchos archivos para separar los scritps de JavaScript por módulos o secciones, lo cual puede convertirse en un problema al tratarse de un lenguaje tan dinámico y no tipado. Si a esto se le suma el que también se van a tener otro tipo de archivos como HTML, JavaScript y CSS, actividades como la optimización, minificación, validación de sintaxis y las pruebas tomarían demasiado tiempo.

Es aquí donde entra Gulp (o Gulp.js) como herramienta de automatización y construcción de proyectos JavaScript, la cual mediante una amplia variedad de plugins y el hecho de ser orientado más al código (code over configuration) permiten que cada proyecto tenga un proceso de construcción personalizado. En este aspecto podría decirse que es similar a Gradle por ejemplo.

Para ilustrar de una manera sencilla cómo se puede trabajar con Gulp en este post se desarrollará un proyecto pequeño con simplemente dos archivos JavaScript y CSS, lo cuales se concatenarán y minificarán en la construcción usando Gulp.

Otras tareas como la verificación de estándares y sintaxis (JSHint, JSLint) o la inclusión automática de dependencias de Bower o de AngularJS serán descritas en siguientes posts.

Cabe anotar que ahora que JavaScript ya no está limitado a la capa de presentación de las aplicaciones web, sino que gracias a herramientas como Node.js es posible tener código de servidor (backend) con dicho lenguaje, por lo cual Gulp también puede ser usado en dichas aplicaciones.

Instalación

Node.js

Gulp está desarrollado en Node.js y utiliza varias de sus módulos, como por ejemplo Stream para ejecutar y pasar la información de cada tarea en memoria (en lugar de archivos como lo hace Grunt).

Por esta razón el primer paso será instalar Node.js. Quienes usen Windows o Mac pueden usar el instalador disponible en la página oficial: https://nodejs.org

Para los usuario de Linux personalmente recomiendo usar Node Version Manager (nvm), ya que no sólo se instala más fácilmente sino que también permite tener múltiples versiones de Node.js y cambiar con un comando la versión actualmente usada. En Mac también puede usarse nvm, pero la instalación puede ser un poco más compleja.

Al finalizar la instalación se puede consultar la versión de Node.js ejecutando:

node -v
v0.12.7

Gulp

Una vez instalado Node.js se puede instalar Gulp mediante npm (el administrador de paquetes de Node,js, un poco similar a RubyGems para quienes han desarrollado con Ruby) simplemente ejecutando el siguiente comando:

npm install -g gulp

Nota: En Mac es necesario adicionar "sudo" al inicio del comando.

Este comando instalará la versión más reciente de Gulp disponible en el sitio oficial de npm y al adicionar el parámetro (flag) "-g" se indica se instale Gulp de manera global y así se pueda usar directamente desde una línea de comandos (terminal) independientemente del directorio actual.

Para verificar la instalación se puede ejecutar el siguiente comando:

gulp -v
CLI version 3.9.0

Desarrollo del Proyecto

Inicializando el Proyecto

Como primer paso se debe crear una carpeta para el proyecto y con una línea de comandos (terminal) ingresar hasta ella. En este caso la carpeta se llamará "gulp-demo".

Dado que ya se tiene Node.js (y por ende npm), puede usarse el comando "init" para crear un el proyecto como módulo de Node.js, respondiendo una serie de preguntas, aunque el sistema indicará en algunas ocasiones sugerencias que pueden aceptar simplemente presionando "Enter".

npm init

name: (gulp-demo)
version: (1.0.0)
description: Just a demo project for Gulp.
git repository:
keywords:
author:
license: (ISC) MIT
About to write to /home/user/git/gulp-demo/package.json:

{
  "name": "gulp-demo",
  "version": "1.0.0",
  "description": "Just a demo project for Gulp.",
  "author": "",
  "license": "MIT"
}


Is this ok? (yes) yes

Al finalizar se tendrá el archivo "package.json" con la configuración inicial del proyecto como módulo de Node.js. Un ejemplo más completo de lo que este archivo puede contener se puede encontrar en: http://browsenpm.org/package.json

Código del Proyecto

Para mantener simple el ejemplo, sólo se tendrán 6 archivos:
app
|   index.html
|
+---assets
|   \---img
|           favicon.ico
|
+---css
|       style1.css
|       style2.css
|
\---js
        hello.js
        printer.js

app/js/printer.js
var printer = (function () {
  return function (message) {
    return 'Gulp Demo says: ' + message;
  }
})();

app/js/hello.js
var helloWorld = (function () {
  return function () {
    return 'Hello World...';
  }
})();

Dado que estos archivos serán concatenados (combinados en uno solo) y minificados, es muy recomendable definirlos como Closures para evitar colisión de variables o funciones globales. Más información se puede encontrar en: http://www.w3schools.com/js/js_function_closures.asp


app/css/style1.css
.text1 {
    color: red;
}

app/css/style2.css
.text2 {
    color: blue;
}

app/assets/img/favicon.ico



app/index.html
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="description" content="Gulp Demo">
  <link rel="icon" href="assets/img/favicon.ico">

  <title>Gulp Demo</title>

  <!-- build:css css/styles.min.css -->
  <!-- inject:css -->
  <!-- endinject -->
  <!-- endbuild -->

  <!-- build:js js/scripts.min.js -->
  <!-- inject:js -->
  <!-- endinject -->
  <!-- endbuild -->
</head>

<body>
  <span id="first" class="text1"></span>
  <br/>
  <span id="second" class="text2"></span>
  <script>
    document.getElementById('first').innerHTML = helloWorld();
    document.getElementById('second').innerHTML = printer('Hello World');
  </script>
</body>

</html>

En este HTML se están usando las dos funciones JavaScript y clases CSS definidas anteriormente, pero no se están incluyendo los scripts ni los archivos CSS, sino que en su lugar se tienen comentarios "inject" y "build", los cuales serán procesados por Gulp.

Inyección de Archivos

Como se mencionaba inicialmente, un proyecto grande puede tener muchos archivos y tener que incluir cada uno manualmente, modificarlos o borrarlos de los lugares donde se estén usando puede ser muy tedioso. Es por eso que se usará el paquete de Gulp "gulp-inject".

Dicho paquete busca los archivos JavaScript o CSS que cumplan con una condición ubicada y los incluye dentro de comentarios "inject:js" y "endinject" o "inject:css" y "endinject" según corresponda.

Para empezar se debe instalar Gulp en el proyecto actual como dependencia de desarrollo (son aquellas que no son requeridas al momento de ejecutar la aplicación en ambientes de producción), mediante el siguiente comando:

npm install --save-dev gulp

gulp@3.9.0 node_modules\gulp
├── interpret@0.6.6
├── deprecated@0.0.1
├── pretty-hrtime@1.0.1
├── archy@1.0.0
├── minimist@1.2.0
├── semver@4.3.6
├── tildify@1.1.2 (os-homedir@1.0.1)
├── v8flags@2.0.11 (user-home@1.1.1)
├── chalk@1.1.1 (supports-color@2.0.0, escape-string-regexp@1.0.4, ansi-styles@2.1.0, has-ansi@2.0.0, strip-ansi@3.0.0)
├── orchestrator@0.3.7 (sequencify@0.0.7, stream-consume@0.1.0, end-of-stream@0.1.5)
├── liftoff@2.2.0 (rechoir@0.6.2, extend@2.0.1, flagged-respawn@0.3.1, resolve@1.1.6, findup-sync@0.3.0)
├── vinyl-fs@0.3.14 (graceful-fs@3.0.8, mkdirp@0.5.1, strip-bom@1.0.0, vinyl@0.4.6, defaults@1.0.3, through2@0.6.5, glob-stream@3.1.18, glob-watcher@0.0.6)
└── gulp-util@3.0.7 (array-differ@1.0.0, array-uniq@1.0.2, beeper@1.1.0, fancy-log@1.1.0, lodash._reescape@3.0.0, lodash._reevaluate@3.0.0, lodash._reinterpolate@3.0.0, object-assign@3.0.0, replace-ext@0.0.1, has-gulplog@0.1.0, gulplog@1.0.0, vinyl@0.5.3, lodash.template@3.6.2, through2@2.0.0, multipipe@0.1.2, dateformat@1.0.12)

Como puede verse el resultado es una estructura de árbol con las dependencias transitivas de Gulp, las cuales npm se encarga de descargar.

Cada módulo o paquete instalado mediante npm se almacenará automáticamente en la carpeta "node_modules" dentro del proyecto. Debido a que las dependencias de cada módulo npm se manejan de manera anidada, los módulos instalados en el proyecto tendrán a su vez una carpeta "node_modules".

gulp-demo
|
+---node_modules
|   \---gulp
|       \---bin
|       \---completion
|       \---lib
|       \---node_modules
|           \---archy
|           \---chalk
|           ...

Al comando "install" se le adicionó el flag "--save-dev" para que no solamente se descargue el paquete a instalar, sino que también adiciona la dependencia en el archivo "package.json":

{
  "name": "gulp-demo",
  "version": "1.0.0",
  "description": "Just a demo project for Gulp.",
  "main": "gulpfile.js",
  "author": "",
  "license": "MIT",
  "devDependencies": {
    "gulp": "^3.9.0"
  }
}

De esta manera no será necesario incluir el contenido de la carpeta "node_modules" en el repositorio de código sino que al descargar/clonar el proyecto las dependencias se descargarán simplemente ejecutando "npm install".

Ahora para instalar "gulp-inject" se ejecuta:

npm install --save-dev gulp-inject

Para empezar a usar Gulp se crea el archivo "gulpfile.js" en la raíz del proyecto:
'use strict';

var gulp   = require('gulp');
var inject = require('gulp-inject');

// 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'));
});

Líneas 3-4: Se declaran las variables que contienen los módulos "gulp" y "gulp-inject".

Línea 7: Creación de la tarea con nombre "inject" en Gulp. Al ejecutarla se llamará a la función definida.

Línea 8: El archivo "index.html" será en el que se busquen los comentarios "inject". El atributo "cwd" indica que al ejecutar esta tarea la carpeta de trabajo actual (Current Working Directory) sea "./app" que es donde se encuentra el código del proyecto.

Línea 9: Al Stream resultante de la línea anterior se le invoca la función "pipe" para que se pueda trabajar con dicho resultado sea procesado por la siguiente instrucción "inject", la cual incluirá una referencia a los archivos JavaScript en "index.html". En cierta medida es similar al Pipeline de Unix.

Línea 10: Se indica que se inyectarán todos los archivos con terminación ".js" que se encuentren dentro de "app/" y sus sub-carpetas. La expresión usada ("./app/**/*.js") es un Glob y permite usar comodines (wildcards como "*" o "!") para coincidir con uno o más archivos. Para ver las expresiones regulares que se pueden usar se puede consultar la documentación de Glob o de Minimatch, que es usada internamente por Glob.

Línea 11: Dado que en este punto no se necesita ver el contenido de los archivos sino solamente su ruta se indica que no se leerán.

Línea 12: Por defecto "inject" usará la ruta completa de cada archivo al incluirlo en "index.html" (por ejemplo "/app/js/hello.js"). Al usar "relative: true" la ruta de cada archivo será relativa con respecto a la carpeta de trabajo actual (por ejemplo "js/hello.js").

Líneas 14-18: Repite el mismo proceso anterior, pero para los archivos de estilos CSS.

Línea 19: Todas las operaciones hasta ahora se han realizado en memoria. Aquí se indica que el archivo "index.html" resultante se guarde en "/app", es decir se sobrescribe el archivo original.

Para ejecutar esta nueva tarea se usa el siguiente comando:

gulp inject

Using gulpfile /home/user/git/gulp-demo/gulpfile.js
Starting 'inject'...
gulp-inject 2 files into index.html.
gulp-inject 2 files into index.html.
Finished 'inject' after 36 ms

Al revisar el archivo "index.html" se ve que ya están incluidos las referencias a los archivos JavaScript y CSS.

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="description" content="Gulp Demo">
  <link rel="icon" href="assets/img/favicon.ico">

  <title>Gulp Demo</title>

  <!-- build:css css/styles.min.css -->
  <!-- inject:css -->
  <link rel="stylesheet" href="css/style1.css">
  <link rel="stylesheet" href="css/style2.css">
  <!-- endinject -->
  <!-- endbuild -->

  <!-- build:js js/scripts.min.js -->
  <!-- inject:js -->
  <script src="js/hello.js"></script>
  <script src="js/printer.js"></script>
  <!-- endinject -->
  <!-- endbuild -->
</head>

<body>
  <span id="first" class="text1"></span>
  <br/>
  <span id="second" class="text2"></span>
  <script>
    document.getElementById('first').innerHTML = helloWorld();
    document.getElementById('second').innerHTML = printer('Hello World');
  </script>
</body>

</html>

Para realizar una prueba puede abrirse el archivo "index.html" en un navegador web.
Figura 1 - Prueba de consola desde el navegador

Nota: En este caso no se tienen dependencias entre los scripts, pero dependiendo del framework JavaScript usado se tienen paquetes adicionales que pueden ordenar de manera adecuada y automática los archivos inyectados. En el caso de AngularJS por ejemplo se tiene "gulp-angular-filesort".

Concatenando y Minificando los Archivos

En ambientes locales de desarrollo tener muchos archivos de tamaño considerable es algo que poco afecta el tiempo de respuesta de las aplicaciones, pero cuando esta ya se encuentra en un ambiente de producción los tiempos de descarga de cada archivo pueden afectar la visualización de la página.

Figura 2 - Ejemplo de página mientras cargan sus scripts y estilos

Como se ve en la figura 2, mientras el navegador termina de descargar todos los recursos necesarios la página puede tener una presentación y funcionamiento no deseados y esto varía dependiendo de la conexión a Internet que tenga cada usuario.

Para mitigar un poco esta situación los archivos JavaScript y CSS pueden concatenarse (combinarse) y minificarse (eliminando espacios, saltos de línea y acortando nombres de variables) para que el navegador requiere de un menor número de recursos a descargar con el menor tamaño posible.

Sin embargo desarrollar aplicaciones usando directamente los archivos concatenados y minificados dificulta enormemente los procesos de depuración (debug), por lo cual dichos archivos serán generados en una carpeta adicional "dist". De esta manera mientras se desarrolla se usa el contenido de "app" y al finalizar se publica en producción el contenido generado en "dist".


Actualización 2016-05-08: A partir de la versión "4.0.0" de "gulp-inject" se aclara que la propiedad "read" debe indicarse en "gulp.src", por lo que la tarea "inject" debe quedar de la siguiente manera:

...
// 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'));
});
...

Concatenación

Primero, para concatenar los archivos JavaScript y CSS que se encuentren referenciados en "index.html" entre bloques de comentarios "build:js" o "build:css" y "endbuild" se usará el paquete "gulp-useref". El nombre que tendrá el archivo concatenado final será en que se indique frente a "build:xx", en este caso "js/scripts.min.js" y "css/styles.min.css".

npm install --save-dev gulp-useref

gulpfile.js
'use strict';

var gulp   = require('gulp');
var inject = require('gulp-inject');
var useref = require('gulp-useref');

// 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(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'));
});

Líneas 22-26: Se indica que la nueva tarea "compress" depende de la tarea "inject". Normalmente las tareas en Gulp se ejecutan de manera asíncrona, pero indicando dependencias entre tareas de esta manera se garantiza que al intentar ejecutar "compress" Gulp ejecutará y esperará a que termine primero "inject".

Líneas 29-32: Adicionalmente se crea la tarea "copy:assets" la cual se encargará de copiar la carpeta "assets" junto con todo su contenido a "dist". Para este ejemplo sólo se tiene una imagen en este directorio, pero aquí también pueden estar otros recursos como por ejemplo archivos de configuración, audios, videos, etc.


Nota: Para que la dependencia entre tareas pueda expresarse de esta manera, las tareas de las que se depende deben tener una instrucción "return".

Al ejecutar estas nuevas tareas se tiene:

gulp compress copy:assets

Using gulpfile /home/user/git/gulp-demo/gulpfile.js
Starting 'inject'...
Starting 'copy:assets'...
gulp-inject 2 files into index.html.
gulp-inject 2 files into index.html.
Finished 'inject' after 42 ms
Starting 'compress'...
Finished 'copy:assets' after 333 ms
Finished 'compress' after 341 ms

Como puede verse, aunque sólo se indicó a Gulp ejecutar "compress", debido a la dependencia indicada se ejecutó primero "inject", de esta manera se asegura que todos los archivos necesarios serán incluidos en la concatenación.

En cuanto a la tarea "copy:assets", al no tener ninguna dependencia fue iniciada de simultáneamente con "inject", lo cual para este caso no representa problemas ya que ambas trabajan con conjuntos de archivos diferentes.

Al finalizar la ejecución de "compress" se debe crear la carpeta "dist":
dist
|   index.html
|
+---assets
|   \---img
|           favicon.ico
|
+---css
|       styles.min.css
|
\---js
        scripts.min.js

dist/index.html
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="description" content="Gulp Demo">
  <link rel="icon" href="assets/img/favicon.ico">

  <title>Gulp Demo</title>

  <link rel="stylesheet" href="css/styles.min.css">

  <script src="js/scripts.min.js"></script>
</head>

<body>
  <span id="first" class="text1"></span>
  <br/>
  <span id="second" class="text2"></span>
  <script>
    document.getElementById('first').innerHTML = helloWorld();
    document.getElementById('second').innerHTML = printer('Hello World');
  </script>
</body>

</html>

En este archivo ya no aparecen las referencias a "js/hello.js", "js/printer.js", "css/style1.css" ni "css/style2.css", sino que "gulp-useref" las ha reemplazado por los nuevos archivos "js/scripts.min.js" y "js/scripts.min.js".


dist/css/styles.min.css
.text1 {
    color: red;
}
.text2 {
    color: blue;
}

dist/js/scripts.min.js
var helloWorld = (function () {
  return function () {
    return 'Hello World...';
  }
})();

var printer = (function () {
  return function (message) {
    return 'Gulp Demo says: ' + message;
  }
})();

Minificación

Para este paso se usará el paquete "gulp-uglify" para los archivos JavaScript, "gulp-cssnano" para los CSS, "gulp-if" para diferenciar que tipo de archivo se está procesando y "gulp-util" para que en caso de que un error de sintaxis en los archivos JavaScript impida su minificación se pueda ver un mensaje más claro del error.

npm install --save-dev gulp-uglify gulp-cssnano gulp-if gulp-util

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');

// 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'));
});

Línea 29: Sólo los archivos ".js" serán procesados por "gulp-uglify".

Línea 30: La opción "mangle" hace que "gulp-uglify" convierta los nombres de las variables locales a una o dos letras, reduciendo aún más el tamaño del archivo JavaScript final.

Línea 31: Los errores que se lleguen a generar durante la minificación de los archivos JavaScript serán procesados por "gulp-util" para que sean mostrados de una manera más comprensible.

Línea 32: Sólo los archivos ".css" serán procesados por "gulp-cssnano".


De esta manera al ejecutar de nuevo "gulp compress" se tendrá:

dist/css/styles.min.css
.text1{color:red}.text2{color:blue}

dist/js/scripts.min.js
var helloWorld=function(){return function(){return"Hello World..."}}(),printer=function(){return function(n){return"Gulp Demo says: "+n}}();

El archivo "dist/index.html" puede abrirse en un navegador web y podrá comprobarse que funciona como se espera, usando los nuevos archivos concatenados y minificados.
Figura 3 - Página usando archivos concatenados y minificados

Nota:  En caso de no usar "gulp-inject" para inyectar automáticamente los archivos (por que se usa otra herramienta o se incluyen manualmente) simplemente basta con quitar los bloques "inject:xx". La concatenación y minificación no dependen del uso de "gulp-inject".


Imágenes

Existen plugins de Gulp que aplican algunas técnicas de optimización para reducir su peso, como por ejemplo "gulp-image-optimization", "gulp-imagemin" o "gulp-image", sin embargo personalmente prefiero tener las imágenes ya optimizadas desde un principio en la carpeta "app", para lo cual uso herramientas online y gratuitas para procesar las imágenes como TinyPNGTinyJPG.


El proyecto completo se puede ver en: https://github.com/guillermo-varela/gulp-demo

Conclusiones

Aunque en principio la longitud del post pueda llevar a pensar que Gulp es una herramienta compleja, si realmente se analiza el código desarrollado es realmente poco, especialmente si se tiene en cuenta que el contenido del archivo "gulpfile.js" tendría pocas modificaciones si el proyecto tuviera más páginas, scripts o estilos CSS.

Lo más extenso del post son las explicaciones que se dan en cada paso, pero una vez se tiene una mejor comprensión de lo que es Gulp y cómo se puede usar, es sencillo de usar y allí se verá la ganancia de tiempo que se tiene al tener tareas como estas automatizadas.

En un próximo post se ampliará un poco más este proyecto para mostrar cómo se puede tener análisis del código JavaScript.

Referencias

https://github.com/gulpjs/gulp/blob/master/docs/README.md
https://markgoodyear.com/2014/01/getting-started-with-gulp
https://scotch.io/tutorials/automate-your-tasks-easily-with-gulp-js

viernes, 8 de enero de 2016

Tutorial Gradle

Tutoriales sobre Gradle

Gradle es una herramienta para manejar y automatizar los procesos de compilación, construcción e inclusive despliegue de proyectos de software.

Esta definición es algo amplia y por lo tanto tratar de demostrar que es Gradle y como usarlo en un solo tutorial es cuando menos complejo. Es por ello que se desarrolló una serie de posts o tutoriales que muestran una a una las caracteristicas de esta herramienta en ejemplos pequeños y sencillos.