Compartir a través de



Noviembre de 2016

Volumen 31, número 11

El programador ocupado: cómo dominar MEAN: adoptar Gulp

Por Ted Neward | Noviembre de 2016

Ted NewardBienvenidos de nuevo, usuarios de MEAN.

Si lee mi última columna de octubre, en la que hablaba del "reinicio" del código base (usando Yeoman para quitar el scaffold de los datos básicos y el código de adherencia), observó probablemente la introducción de una nueva herramienta en la combinación sin muchas explicaciones. Me refiero, obviamente, a la herramienta "Gulp" usada para iniciar el servidor y abrir el explorador del cliente en la aplicación cliente con scaffold (msdn.com/magazine/mt742874).

Si se perdió mi última columna, puede encontrarla fácilmente. En primer lugar, asegúrese de que tanto Yeoman como los generadores de "angular-fullstack" estén instalados en su entorno de desarrollo de Node.js (junto con una copia en ejecución local de MongoDB):

npm install –g yeoman angular-fullstack-generator
yo angular-fullstack

A continuación, después de responder a las preguntas sobre las herramientas a las que Yeoman debe aplicar scaffolding en su lugar (para esta columna, las elecciones realizadas son mayoritariamente irrelevantes) y, después de que Yeoman haya iniciado amablemente una "instalación npm" para desplegar todas las dependencias de desarrollo y en tiempo de ejecución, la herramienta de scaffolding indicará que la aplicación está lista para probarse mediante la ejecución de "gulp test" o "gulp start:server".

Claramente, sea lo que sea Gulp, es algún tipo de herramienta de compilación, semejante en espíritu a Make, MSBuild o Ant. No obstante, su funcionamiento es un poco diferente al de estas otras tres herramientas, por lo que merece la pena dedicarle un tema.

Un poco de Gulp para comenzar

Aunque no es del todo acertado denominar Gulp a una "herramienta de compilación" para un lenguaje que ni tan solo se compila (recuerde que, por lo general, ECMAScript está destinado a ser un lenguaje interpretado), es en realidad el mejor término para una herramienta que se ejecuta tras (o durante) el desarrollo para garantizar que todo esté alineado y listo para avanzar. Quizás un nombre mejor para todas ellas sería "herramienta de automatización del desarrollo", porque es más preciso e incluiría el acto de compilar código y ensamblarlo en un artefacto implementable. No obstante, dado que es un trabalenguas y "herramienta de compilación" es fácil de decir, seguiremos con la idea de que Gulp es una herramienta de compilación por ahora.

Para empezar a usar Gulp, nos alejaremos del código con scaffold de antes y comenzaremos desde cero para centrarnos en qué hace Gulp y de qué manera lo hace. En primer lugar, instale las herramientas de línea de comandos globales de Gulp ("npm install --g gulp-cli"). A continuación, en un nuevo directorio, ejecute lo siguiente para crear un proyecto de Node.js vacío:

npm init
npm install --save-dev gulp

Garantizará que se haga referencia a Gulp en el archivo package.json como una dependencia de desarrollador, de modo que si despliega el proyecto, no tendrá que acordarse de instalar Gulp. Simplemente, se mostrará con la siguiente "instalación npm" en un entorno nuevo.

Ahora, en el editor de texto de su elección, cree un nuevo archivo denominado gulpfile.js:

const gulp = require('gulp');
gulp.task('default', function() {
  console.log("Gulp is running!");
});

A continuación, desde el mismo directorio, emita el comando de Gulp en existencias, que (quizás como era de esperar) sea simplemente "gulp". Gulp piensa sobre ello un segundo y luego vuelve con:

[18:09:38] Using gulpfile ~/Projects/code/gulpdemo/gulpfile.js
[18:09:38] Starting 'default'...
Gulp is running!
[18:09:38] Finished 'default' after 142 μs

No está mal. Parece algo exagerado en este momento, pero no está mal.

Engullir algunas tareas

Gulp, como la mayoría de las herramientas de compilación, piensa en términos de "tareas" y, más importante aún, en cómo seguir las dependencias entre dichas tareas. Por tanto, en el archivo Gulpfile simple, ve que existe una tarea, denominada "default" (que es la convención bien entendida para referirse a la tarea que debe ejecutarse si no se especifica ninguna en la línea de comandos), y ejecuta el cuerpo del literal de función asociado cuando se le pide que ejecute esa tarea. La denominación diferente de la tarea es trivial:

const gulp = require('gulp');
gulp.task('echo', function() {
  console.log("Gulp is running!");
});

Debería estar claro que Gulp es solo código, de modo que todo lo que se puede hacer en el código también se puede hacer en el cuerpo de una tarea de Gulp. Esto ofrece muchas opciones increíbles, como leer en una base de datos para encontrar elementos que deban generarse mediante código, hablar con otros servicios o configuración en línea, o incluso imprimir la fecha y hora actuales:

const gulp = require('gulp');
gulp.task('echo', function() {
  console.log("Gulp is running on " + (new Date()));
});

El resultado es el siguiente:

Teds-MacBook-Pro:gulpdemo tedneward$ gulp echo
[18:16:24] Using gulpfile ~/Projects/code/gulpdemo/gulpfile.js
[18:16:24] Starting 'echo'...
Gulp is running on Wed Sep 14 2016 18:16:24 GMT-0700 (PDT)
[18:16:24] Finished 'echo' after 227 μs

Las tareas dependientes se enumeran simplemente como una matriz de cadenas entre el nombre de la tarea y el cuerpo de su literal de función, de modo que la presencia de "eco" de una tarea que depende de otra tarea tiene el aspecto siguiente:

const gulp = require('gulp');
const child_process = require('child_process');
gulp.task('gen-date', function() {
  child_process.exec('sh date > curdate.txt');
});
gulp.task('echo', ['clean', 'gen-date'], function() {
  console.log("Gulp is running on " + (new Date()));
});

Aquí, la tarea "gen-date" usa un paquete de Node.js estándar, "child_process", para iniciar una herramienta externa con el fin de escribir la fecha en un archivo, simplemente para demostrar que puede hacerlo. Francamente, todo esto está bien, pero, en general, se espera que las herramientas de compilación hagan algo más importante que escribir cosas en la consola y descubrir la fecha y hora actuales.

Un poco más de Gulp

Vamos a profundizar un poco más. Cree un archivo index.js que contenga el siguiente código ECMAScript, incluidas las partes de código que no son del todo buenas:

// index.js
function main(args) {
  for (let arg in args) {
    if (arg == "hello")
      console.log("world!");
      console.log("from index.js!");
    }
}
console.log("Hello, from index.js!")
main()

Ciertamente no tiene mucho sentido y está claro que presenta algunos problemas, pero de eso se trata: estaría bien que hubiese una herramienta capaz de localizar y notificar algunos de estos problemas. (¿Debería decir "comprobación en tiempo de compilación"?) Afortunadamente, existe una herramienta como esta en JSHint (jshint.com), pero se instala de manera predeterminada como una herramienta de línea de comandos y sería difícil acordarse de ejecutarla constantemente.

Afortunadamente, este es el objetivo de una herramienta de compilación. Volviendo a Gulpfile, hagamos que Gulp ejecute JSHint en cada uno de los archivos de origen (solo existe uno en este momento). Para hacerlo, le indicaremos los archivos de origen en cuestión y, después, le pediremos que ejecute JSHint en cada uno de ellos:

// Don't need it in this file, but we need it installed
require('jshint');
require('jshint-stylish');
const gulp = require('gulp');
const jshint = require('gulp-jshint');
gulp.task('jshint', function() {
  return gulp.src('*.js')
    .pipe(jshint())
    .pipe(jshint.reporter('jshint-stylish'));
});

Cuando se ejecuta, la herramienta señala que existen algunos cambios sugeridos para todos los archivos "js" del directorio actual, incluido el propio Gulpfile. Bah. No es necesario que se preocupe por comprobar el archivo Gulpfile, así que lo vamos a descartar de la colección de archivos que se va a ejecutar:

gulp.task('jshint', function() {
  return gulp.src(['*.js', '!gulpfile.js'])
    .pipe(jshint())
    .pipe(jshint.reporter('jshint-stylish'));
});

Se quitará "gulpfile.js" de la lista actual de archivos antes de que se pase a la fase siguiente de la canalización. De hecho, probablemente querrá que la mayoría del código resida en un directorio "src" (o directorios "server" y "client", para cada lado), por lo que debe agregar estos directorios y sus subdirectorios a la lista de archivos que se van a procesar:

gulp.task('jshint', function() {
  return gulp.src(['*.js', '!gulpfile.js', 'server/**/*.js', 'client/**/*.js'])
    .pipe(jshint())
    .pipe(jshint.reporter('jshint-stylish'));
});

El "doble asterisco" de cada ruta de acceso obtiene el comportamiento recursivo para seleccionar los archivos "js" de cada uno de esos subdirectorios.

A primera vista, es genial, pero no parece que le vaya a resultar de gran ayuda: Aún tiene que escribir a mano "gulp jshint" (o solo "gulp" si lo enlazó a la tarea "default" como una dependencia) cada vez que quiera ver qué debe corregirse. ¿Por qué no puede hacer que se ejecute cada vez que el código cambie, como hacen los IDE?

Claro, haga eso mismo, tal como se muestra en la Figura 1.

Figura 1 Automatización continua con Gulp

// Don't need it in this file, but you need it installed
require('jshint');
const gulp = require('gulp');
const jshint = require('gulp-jshint');
gulp.task('default', ['watch']);
gulp.task('watch', function() {
  gulp.watch(['*.js', '!gulpfile.js', 'client/**/*.js', 'server/**/*.js'],
    ['jshint']);
});
gulp.task('jshint', function() {
  return gulp.src(['*.js', '!gulpfile.js', 'client/**/*.js', 'server/**/*.js'])
    .pipe(jshint())
    .pipe(jshint.reporter('default'));
});

Ahora, cuando ejecute "gulp", la línea de comandos se pondrá en pausa y esperará. Gulp está ahora en modo "watch", donde vigilará los archivos que se pasen a la llamada de "gulp.watch" y, si alguno de estos archivos cambia (es decir, se guarda, ya que Gulp, lamentablemente, no puede observar los editores de texto), ejecutará de inmediato la tarea "jshint" en el conjunto de archivos completo. Y siga observando.

Más Gulp

Una de las claves para comprender cómo las tareas de Gulp procesan archivos files se encuentra en la llamada a la canalización. Gulp piensa en términos de "secuencias", en lugar de en tareas o archivos. Por ejemplo, una tarea de Gulp simple que copiaría archivos del directorio "src" al directorio "dest" tendría el aspecto siguiente:

gulp.task('copy-files', function() {
  gulp.src('source/folder/**')
    .pipe( gulp.dest('dest/folder/**') );
});

En esencia, el archivo gulp.src extrae los archivos y los deposita, intactos, en el destino especificado por gulp.dest. Todo lo que deba suceder en estos archivos se realiza como un paso de la canalización y cada archivo fluye a través de esa canalización antes de continuar al siguiente paso de esta. Se trata del estilo de arquitectura de Unix de "canalizaciones y filtros", llevado al ecosistema de Node.js con una elegancia increíble. Windows PowerShell está integrado en el mismo tipo de arquitectura, para aquellos que crean haber visto algo parecido antes en el universo de .NET.

Por lo tanto, si solo quiere ver cómo Gulp toca cada archivo a través de la canalización, existe un complemento para ello ("gulp-filelogger"), que se imprimirá en la consola en cada archivo que toque:

gulp.task('copy-files', function () {
  gulp.src(srcFiles)
    .pipe(filelogger())
    .pipe(gulp.dest(destDir));
});

El resultado es el siguiente:

Teds-MacBook-Pro:gulpdemo tedneward$ gulp copy-files
[20:14:01] Using gulpfile ~/Projects/code/gulpdemo/gulpfile.js
[20:14:01] Starting 'copy-files'...
[20:14:01] Finished 'copy-files' after 14 ms
[20:14:01] [/Users/tedneward/Projects/code/gulpdemo/index.js]
Teds-MacBook-Pro:gulpdemo tedneward$

Observe cómo se muestra el resultado cuando Gulp notifica que ha terminado. Gulp puede (y lo hace) procesar estas secuencias de forma asincrónica la mayor parte del tiempo, lo que reduce los tiempos de "compilación". La mayor parte del tiempo, los desarrolladores no saben qué cosas se realizan en paralelo ni les preocupa, pero para esas veces en que hacer cosas en una secuencia precisa es importante, lo que es de esperar, la comunidad de complementos de Gulp tiene algunos complementos que serializarán la ejecución y garantizarán que todo se haga en secuencia. Gulp 4.0 agregará dos nuevas funciones, en paralelo y en serie, para que quede más claro, pero como aún no se ha enviado, deberá esperar para tenerlas.

Por cierto, Gulp en sí está formado simplemente de las cuatro funciones que ha podido ver hasta ahora: gulp.task, gulp.watch, gulp.src y gulp.dest. Todo lo demás son complementos, módulos npm o datos manuscritos. Esto hace que Gulp sea en sí mismo extremadamente fácil de comprender. Tristemente fácil, de hecho, para los autores de artículos que cobran por palabra.

Mucho más Gulp de una vez

Gulp en sí no es una herramienta tan complicada, pero como sucede con cualquier herramienta de este tipo, su verdadero punto fuerte es la amplia matriz de complementos y herramientas complementarias que han surgido de la comunidad que lo rodea. La lista completa está disponible en gulpjs.com/plugins, pero la Figura 2 ofrece una muestra representativa de una receta de Gulp, que demuestra cómo automatizar el lanzamiento de un proyecto en GitHub, incluidos los comandos GIT para insertar en el maestro.

Figura 2 Receta de Gulp

var gulp = require('gulp');
var runSequence = require('run-sequence');
var conventionalChangelog = require('gulp-conventional-changelog');
var conventionalGithubReleaser = require('conventional-github-releaser');
var bump = require('gulp-bump');
var gutil = require('gulp-util');
var git = require('gulp-git');
var fs = require('fs');
gulp.task('changelog', function () {
  return gulp.src('CHANGELOG.md', {
    buffer: false
  })
    .pipe(conventionalChangelog({
      preset: 'angular' // Or to any other commit message convention you use.
    }))
    .pipe(gulp.dest('./'));
});
gulp.task('github-release', function(done) {
  conventionalGithubReleaser({
    type: "oauth",
    token: '' // Change this to your own GitHub token.
  }, {
    preset: 'angular' // Or to any other commit message convention you use.
  }, done);
});
gulp.task('bump-version', function () {
// Hardcode the version change type to "patch," but it might be a good
// idea to use minimist (bit.ly/2cyPhfa) to determine with a command
// argument whether you're doing a "major," "minor" or a "patch" change.
  return gulp.src(['./bower.json', './package.json'])
    .pipe(bump({type: "patch"}).on('error', gutil.log))
    .pipe(gulp.dest('./'));
});
gulp.task('commit-changes', function () {
  return gulp.src('.')
    .pipe(git.add())
    .pipe(git.commit('[Prerelease] Bumped version number'));
});
gulp.task('push-changes', function (cb) {
  git.push('origin', 'master', cb);
});
gulp.task('create-new-tag', function (cb) {
  var version = getPackageJsonVersion();
  git.tag(version, 'Created Tag for version: ' + version, function (error) {
    if (error) {
      return cb(error);
    }
    git.push('origin', 'master', {args: '--tags'}, cb);
  });
  function getPackageJsonVersion () {
    // Parse the json file instead of using require because require caches
    // multiple calls so the version number won't be updated.
    return JSON.parse(fs.readFileSync('./package.json', 'utf8')).version;
  };
});
gulp.task('release', function (callback) {
  runSequence(
    'bump-version',
    'changelog',
    'commit-changes',
    'push-changes',
    'create-new-tag',
    'github-release',
    function (error) {
      if (error) {
        console.log(error.message);
      } else {
        console.log('RELEASE FINISHED SUCCESSFULLY');
      }
      callback(error);
    });
});

En este ejemplo, se demuestran varias cosas: cómo ejecutar tareas en una secuencia concreta usando los complementos de Gulp para generar un archivo de conjunto de cambios de convención, crear mensajes de lanzamiento de estilo de GitHub, lanzar la versión semántica y muchas cosas más. Todo ello desde la versión de Gulp, lo que es bastante eficaz.

Resumen

Este no ha sido un artículo especialmente cargado con codificación, aunque ha podido reiniciar toda la aplicación, obtener un lote de funciones completo y, esencialmente, llevar la aplicación al mismo nivel (y más allá) desde el que ha estado compilando durante el último año más o menos. Le encantará el scaffolding.

Y más importante, después de compilar todas las partes manualmente pieza por pieza y antes de ejecutar el scaffolding, es mucho más fácil comprender el código como un todo, así como qué pasa y dónde. Por ejemplo, la apertura de routes.js parecerá familiar para la tabla de enrutamiento que compiló a mano anteriormente, y el archivo package.json (en la raíz del directorio del proyecto) será más grande, aunque seguirá igual que el que estaba usando.

Lo único nuevo, de hecho, más allá del uso de Yeoman propiamente, es la introducción de una herramienta de compilación para reunir todas las piezas relevantes en el lugar adecuado, que es el tema que trataré la próxima vez. Hasta entonces… ¡feliz codificación!


Ted Neward es un consultor de politecnología, orador y mentor residente en Seattle. Ha escrito más de 100 artículos, es F #MVP, y es autor y coautor de una docena de libros. Puede comunicarse con él en ted@tedneward.com, si está interesado en recibir ayuda suya para su equipo, o puede leer su blog en blogs.tedneward.com.

Gracias al siguiente experto técnico por su ayuda en la revisión de este artículo: Shawn Wildermuth