lunes, 22 de febrero de 2016

Tutorial sobre Gulp.js IV: Algunas Prácticas Recomendadas

Introducción

En posts anteriores se ha venido trabajando con Gulp.js, mostrando cómo esta herramienta puede ayudar en la automatización de tareas en los proyectos web, análisis de código JavaScript, actualización automática de cambios en el navegador web e inclusive su posibilidad de integrarse con otras herramientas como Bower.

En esta ocasión se mostrarán un par de prácticas recomendadas al usar Gulp.js que pueden ser de utilidad tanto en proyectos grandes como pequeños.

Para ahorrar algo de tiempo se tomará como base el proyecto desarrollado en el último post introductorio sobre Bower: https://github.com/guillermo-varela/gulp-bower-demo

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

npm install

Separar las Librerías Externas del Código del Proyecto

Actualmente el equipo de desarrollo de AngularJS está recomendando seguir la guía acerca de estilos y estándares propuestos por John Papahttps://github.com/johnpapa/angular-styleguide

Una de las recomendaciones allí aparecen es separar el código de las librerías externas del código desarrollado para el proyecto.

En el proyecto desarrollado previamente se tiene un archivo ".bowerrc" en el cual se indica que las librerías mediante Bower se descargarán en "app/lib" y es en "app/" donde se encuentra el código del proyecto.

Para seguir esta recomendación se borrará el archivo ".bowerrc", con lo cual al ejecutar "bower install" las librerías se descargarán en la raíz del proyecto en "bower_components/".

Figura 1 - Cambio en la Ubicación de las Librerías de Bower

En ".gitignore" se debe cambiar la entrada "app/lib/" por "bower_components/" para que las dependencias descargadas mediante Bower no se incluyan en el repositorio de código.

También deben hacerse algunos cambios en "gulpfile.js":
'use strict';

var gulp        = require('gulp');
var inject      = require('gulp-inject');
var wiredep     = require('wiredep').stream;
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 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'));
});

// Inject libraries via Bower in between of blocks "bower:xx" in index.html
gulp.task('wiredep', function () {
  return gulp.src('index.html', {cwd: './app'})
    .pipe(wiredep({
      'ignorePath': '..'
    }))
    .pipe(gulp.dest('./app'));
});

// Compress into a single file the ones in between of blocks "build:xx" in index.html
gulp.task('compress', ['inject', 'wiredep'], function () {
  return gulp.src('index.html', {cwd: './app'})
    .pipe(useref({ searchPath: ['./', './app'] }))
    .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']);
  gulp.watch(['./bower.json'], ['wiredep']);
  gulp.watch('**/*.html', {cwd: './app'}, function(event) {
    gulp.src(event.path)
      .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', ['inject', 'wiredep', 'server', 'watch']);

Los cambios son:

  • Líneas 21, 25, 59, 67, 80, 81: Dado que ahora las librerías descargadas mediante Bower están en una carpeta por fuera de "app/", ya no es necesario excluir el código de estas librerías de las tareas de Gulp que sólo deben usar el código de la aplicación, como por ejemplo "inject", "jshint", "jscs" y "watch". Esta es una gran ventaja que tiene el seguir esta recomendación ya que la configuración de las tareas de Gulp se hace más sencilla.
  • Línea 35: En la tarea "wiredep" se quita la propiedad "directory: './app/lib/'" ya que la ubicación de las librerías de Bower es la que se usa por defecto. En su lugar se incluye "'ignorePath': '..'" ya que de no hacerlo las referencias en "index.html", al ser URLs relativas tendrían la indicación de carpeta anterior "../", pero debido a que en las tareas "server" y "server-dist" exponen sólo el contenido de la carpeta "app/" (usando gulp-connect) ello generaría errores de recursos no encontrados (404). El resultado sería:
    • ../bower_components/jquery/dist/jquery.js => /bower_components/jquery/dist/jquery.js
    • ../bower_components/bootstrap/dist/js/bootstrap.js  => /bower_components/bootstrap/dist/js/bootstrap.js
  • Línea 43: En la tarea "compress", al usar el plugin "gulp-useref" para concatenar los recursos (JavaScript y CSS) referenciados en "index.html", se incluye la propiedad "searchPath" para indicar que los archivos se deben buscar tanto en la raíz "./" (para encontrar "bower_components") como en "./app".

Al iniciar el servidor de desarrollo, usando el comando "gulp", se podrá ver lo siguiente:
Figura 2 - Aplicación Web sin Dependencias Bower

Al revisar el archivo "index.html" se podrá ver que las dependencias Bower se están indicando con la raíz "/bower_components/" lo cual no existe desde la carpeta "./app/" que se está exponiendo en el servidor web. Para solucionar esto se requiere dejar disponible el contenido de "/bower_components/" como parte de la raíz del servidor web, lo cual se puede hacer usando el plugin "st", el cual expone contenido estático en un servidor web.

Para instalarlo se ejecuta el siguiente comando:

npm install --save-dev st

Para usarlo se debe incluir en "gulpfile.js":

...
var st          = require('st');
...

// Starts a development web server
gulp.task('server', function () {
  connect.server({
    root: './app',
    hostname: '0.0.0.0',
    port: 8080,
    livereload: true,
    middleware: function (connect, opt) {
      return [
        st({
          path: 'bower_components',
          url: '/bower_components'
        })
      ];
    }
  });
});

En la tarea de Gulp "server" se le agrega la propiedad "middleware" a "gulp-connect" para que usando "st" se exponga el contenido de "bower_components" como "/bower_components", justo como se tiene en "index.html". Al ejecutar de nuevo "gulp" se puede ver que la página ya carga con los estilos de Bootstrap.
Figura 3 - Aplicación Web con las Dependencias de Bower

Carga Automática de Plugins de Gulp

Hasta este momento al inicio del archivo "gulpfile.js" se tiene la declaración en variables de los plugins de npm que se usan actualmente para las tareas de Gulp del proyecto.

Para hacer que esta sección sea un poco más corta y que por ende el archivo "gulpfile.js" sea más fácil de entender se puede usar el plugin "gulp-load-plugins", el cual carga automáticamente todos los plugins de Gulp declarados en "package.json" y los deja disponible dentro de una variable que se defina. Por ejemplo si se usa la variable "plugins" entonces cada plugin se accede como un método de dicha variable quitando el prefijo "gulp-" y en Camel Case (para los plugins que lleven guión en su nombre), así para usar "gulp-inject" se tendría "plugins.inject".

De esta manera en el archivo "gulpfile.js" se tendría:
'use strict';

var gulp        = require('gulp');
var plugins     = require('gulp-load-plugins')();
var wiredep     = require('wiredep').stream;
var del         = require('del');
var st          = require('st');
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: paths.app})
    .pipe(plugins.inject(
      gulp.src(paths.js, {cwd: paths.app, read: false}), {
        relative: true
      }))
    .pipe(plugins.inject(
      gulp.src(paths.css, {cwd: paths.app, read: false}), {
        relative: true
      }))
    .pipe(gulp.dest(paths.app));
});

// Inject libraries via Bower in between of blocks "bower:xx" in index.html
gulp.task('wiredep', ['inject'], function () {
  return gulp.src('index.html', {cwd: './app'})
    .pipe(wiredep({
      'ignorePath': '..'
    }))
    .pipe(gulp.dest('./app'));
});

// Compress into a single file the ones in between of blocks "build:xx" in index.html
gulp.task('compress', ['wiredep'], function () {
  return gulp.src('index.html', {cwd: './app'})
    .pipe(plugins.useref({ searchPath: ['./', './app'] }))
    .pipe(plugins.if('**/*.js', plugins.uglify({
      mangle: true
    }).on('error', plugins.util.log)))
    .pipe(plugins.if('**/*.css', plugins.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(plugins.jshint())
    .pipe(plugins.jshint.reporter('jshint-stylish'))
    .pipe(plugins.jshint.reporter('fail'));
});

// Looks for code style errors in JS and prints them
gulp.task('jscs', function () {
  return gulp.src(['**/*.js'], {cwd: './app'})
    .pipe(plugins.jscs())
    .pipe(plugins.jscs.reporter())
    .pipe(plugins.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']);
  gulp.watch(['./bower.json'], ['wiredep']);
  pgulp.watch('**/*.html', {cwd: './app'}, function(event) {
    gulp.src(event.path)
      .pipe(plugins.connect.reload());
  });
});

// Starts a development web server
gulp.task('server', function () {
  plugins.connect.server({
    root: './app',
    hostname: '0.0.0.0',
    port: 8080,
    livereload: true,
    middleware: function (connect, opt) {
      return [
        st({
          path: 'bower_components',
          url: '/bower_components'
        })
      ];
    }
  });
});

// Starts a server using the production build
gulp.task('server-dist', ['build'], function () {
  plugins.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', ['inject', 'wiredep', 'server', 'watch']);

Nota: Los plugins que no son de Gulp (sin prefijo "gulp-") aún deben cargarse explícitamente.

Externalización de Rutas

Para evitar repetir constantemente el mismo conjunto de rutas (globs) y/o archivos, es posible tenerlos en variables aparte o inclusive en un archivo adicional, para que "gulpfile.js" no quede más largo y más fácil de leer y mantener, especialmente si el proyecto tiene una estructura de archivos más compleja (como por ejemplo https://github.com/johnpapa/ng-demos/blob/master/modular/gulp.config.json y https://github.com/johnpapa/ng-demos/blob/master/modular/gulpfile.js).

"gulp-paths.json"
{
  "app": "./app/",
  "js": ["**/*.js"],
  "css": ["**/*.css"],
  "dist": "./dist/"
}

Para los valores que correspondan a carpetas, se recomienda terminar en "/" para mantener consistencia en su uso.

"gulpfile.js"

'use strict';

var gulp        = require('gulp');
var plugins     = require('gulp-load-plugins')();
var paths       = require('./gulp-paths.json');
var wiredep     = require('wiredep').stream;
var del         = require('del');
var st          = require('st');
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: paths.app})
    .pipe(plugins.inject(
      gulp.src(paths.js, {cwd: paths.app, read: false}), {
        relative: true
      }))
    .pipe(plugins.inject(
      gulp.src(paths.css, {cwd: paths.app, read: false}), {
        relative: true
      }))
    .pipe(gulp.dest(paths.app));
});

// Inject libraries via Bower in between of blocks "bower:xx" in index.html
gulp.task('wiredep', ['inject'], function () {
  return gulp.src('index.html', {cwd: paths.app})
    .pipe(wiredep({
      'ignorePath': '..'
    }))
    .pipe(gulp.dest(paths.app));
});

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

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

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

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

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

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

// Starts a development web server
gulp.task('server', function () {
  plugins.connect.server({
    root: paths.app,
    hostname: '0.0.0.0',
    port: 8080,
    livereload: true,
    middleware: function (connect, opt) {
      return [
        st({
          path: 'bower_components',
          url: '/bower_components'
        })
      ];
    }
  });
});

// Starts a server using the production build
gulp.task('server-dist', ['build'], function () {
  plugins.connect.server({
    root: paths.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', ['inject', 'wiredep', 'server', 'watch']);

Los cambios fueron:

  • Línea 5: Se carga el archivo "gulp-paths.json" como un objeto JSON en la variable "paths".
  • Líneas 13, 15, 19, 22, 27, 31, 36, 37, 47, 53, 61, 74, 75, 77 y 86: Se reemplazó la cadena "./app" por "paths.app", para usar el valor indicado en "gulp-paths.json".
  • Líneas 15, 53, 61 y 75: Se reemplazó el arreglo "['**/*.js']" por "paths.js", para usar el valor indicado en "gulp-paths.json".
  • Líneas 19 y 74: Se reemplazó el arreglo "['**/*.css']" por "paths.css", para usar el valor indicado en "gulp-paths.json".
  • Líneas 42, 48, 69 y 104: Se reemplazó la cadena "./dist" por "paths.dist", para usar el valor indicado en "gulp-paths.json". Nótese que en la línea 69 se está concatenando "paths.dist" con "'**/*'" para indicar que se debe borrar el contenido y no la carpeta "dist" como tal y en este caso al tener como estándar el finalizar los valores de las carpetas con "/" se sabe que no es necesario incluir este caracter adicional.

Nota: En la tarea "compress" no se reemplazaron las cadenas "**/*.js" ni "**/*.css" ya que en estos casos no se están indicando rutas sino tipos de archivos.

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

Referencias

https://github.com/johnpapa/angular-styleguide
https://www.npmjs.com/package/st
https://www.npmjs.com/package/gulp-load-plugins

No hay comentarios.:

Publicar un comentario