Novembro de 2016

Volume 31 - Número 11

O programador: Como ser MEAN: Uso do Gulp

De Ted Neward | Novembro de 2016

Ted NewardBem-vindos de volta, MEANers.

Se você tiver lido a minha coluna de outubro, que falava sobre o “reinício” da base de código (usando Yeoman para adicionar scaffold do básico e colar código), talvez tenha notado que uma nova ferramenta foi adicionada à mistura sem muita explicação. Estou me referindo, é claro, à ferramenta “Gulp”, usada para iniciar o servidor e abrir o navegador cliente para o aplicativo cliente com scaffold (msdn.com/magazine/mt742874).

Mesmo que você não tenha lido minha última coluna, será muito fácil entender. Primeiro, verifique se os geradores do Yeoman e da “pilha completa angular” estão instalados em seu ambiente de desenvolvimento do Node.js (junto com uma cópia local em execução do MongoDB):

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

Em seguida, depois de responder perguntas sobre quais ferramentas o Yeoman deve aplicar scaffold no local (para esta coluna, as opções feitas são, em sua maioria, irrelevantes) e depois que o Yeoman tiver iniciado prestativamente uma “instalação npm” para suspender todo o tempo de execução e as dependências de desenvolvimento, o scaffolder relatará que o aplicativo está pronto para o teste por meio da execução de “gulp test” ou de “gulp start:server”.

Claramente, seja o que for o Gulp, é algum tipo de ferramenta de compilação, parecida com o Make, o MSBuild ou o Ant. No entanto, seu funcionamento é ligeiramente diferente de qualquer uma dessas ferramentas e, por essa razão, merece uma discussão.

Um Pouco de Gulp para Começar

Embora não seja exatamente justo chamar o Gulp de “ferramenta de compilação” para uma linguagem que nunca é compilada (lembre-se, o ECMAScript geralmente é criado para ser uma linguagem interpretada), essa é a melhor terminologia que você tem para uma ferramenta cujo uso pretendido é ser executada depois (ou durante) o desenvolvimento para garantir que tudo fique alinhado e pronto. Talvez um nome melhor para todas elas seria “ferramenta de automação de desenvolvimento”, já que isso é mais preciso e incluiria o ato de compilar código e de montá-lo em um artefato implantável. No entanto, como há muitas e como “ferramenta de compilação” está na ponta da língua, por ora vamos ficar com a ideia de que o Gulp é uma ferramenta de compilação.

Para começar a usar o Gulp, vamos dividir o código com scaffold anterior e começar do zero para nos concentrarmos naquilo que (e como) o Gulp faz o que faz. Instale as ferramentas de linha de comando do Gulp (“npm install --g gulp-cli”) primeiro. Em seguida, em um novo diretório, crie um projeto vazio do Node.js ao executar o seguinte:

npm init
npm install --save-dev gulp

Isso garantirá que o Gulp seja referenciado no arquivo package.json como uma dependência do desenvolvedor e, portanto, se você suspender o projeto, não precisará se lembrar de instalar o Gulp. Ele simplesmente aparecerá no próximo “npm install” em um ambiente novo.

Agora, em um editor de texto à sua escolha, crie um novo arquivo, chamado gulpfile.js:

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

Em seguida, no mesmo diretório, emita o comando de estoque do Gulp, que (talvez surpreendentemente) seja apenas “gulp”. O Gulp pensa sobre isso por um segundo e diz:

[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

Nada mal! Parece um pouco excessivo no momento, mas nada mal!

Algumas Tarefas do Gulp

O Gulp, como a maioria das ferramentas de compilação, pensa em termos de “tarefas” e, mais importante, como controlar as dependências entre essas tarefas. Dessa forma, no Gulpfile simples mostrado aqui, ele vê que há uma tarefa, chamada “default” (que é a convenção bem entendida para significar a tarefa que deverá ser executada caso nenhuma seja especificada na linha de comando), e executa o corpo do literal da função associada quando a execução da tarefa é solicitada. É muito fácil nomear a tarefa de forma diferente:

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

Deve ficar aparente que o Gulp é apenas código e, portanto, tudo que puder ser feito em código também poderá ser feito no corpo de uma tarefa do Gulp. Isso oferece muitas opções incríveis, como ler a partir de um banco de dados para encontrar elementos que precisam ser gerados por código, conversar com outros serviços online sobre configuração ou, até mesmo, simplesmente mostrar a data e a hora atuais:

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

Isso produz o seguinte:

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

As tarefas dependentes são simplesmente listadas como uma matriz de cadeia de caracteres entre o nome da tarefa e o corpo literal da função e, portanto, fazer o “echo” de uma tarefa depender de outra tarefa é tão simples quanto isso:

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

Aqui, a tarefa “gen-date” usa um pacote padrão do Node.js, “child_process”, para iniciar uma ferramenta externa para gravar a data em um arquivo, só para provarmos que você pode fazer isso. O que, para sermos honestos, é muito bom, mas geralmente espera-se que as ferramentas de compilação façam algo de maior importância do que simplesmente gravar coisas no console e descobrir a data e a hora atuais.

Um Pouco Mais do Gulp

Vamos colocar um pouco mais de recheio nisso. Crie um arquivo index.js com o seguinte código ECMAScript nele, incluindo as partes de código não muito boas:

// 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()

Sim, não faz muito sentido e sim, ele claramente tem alguns problemas, mas esse é aí que queremos chegar. Não seria legal se houvesse uma ferramenta que pudesse apontar alguns desses erros e relatá-los? (Ouso dizer, fazer uma “verificação em tempo de compilação”?) Felizmente, já existe uma ferramenta assim no JSHint (jshint.com), mas é instalada por padrão como uma ferramenta de linha de comando e seria muito chato ter sempre que lembrar de executá-la.

Felizmente, uma ferramenta de compilação serve para isso. Voltando ao Gulpfile, vamos fazer o Gulp executar o JSHint em cada um dos arquivos de origem (nós só temos um no momento) mostrando para ele os arquivos de origem em questão e, em seguida, solicitando que ele execute o JSHint em cada um:

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

Ao executar isso, a ferramenta mostrará que há algumas alterações sugeridas para todos os arquivos “js” no diretório atual, incluindo o próprio Gulpfile. Bobagem. Você não tem a intenção de verificar o Gulpfile e, portanto, vamos retirá-lo da coleção de arquivos a serem executados:

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

Isso removerá o “gulpfile.js” da lista atual de arquivos antes de passá-lo para o próximo estágio no pipeline. Na verdade, provavelmente você vai querer que a maior parte do código resida em um diretório “src” (ou em diretórios “servidor” e “cliente”, um para cada lado) e, portanto, adiciona esses e todos os subdiretórios à lista de arquivos a serem processados:

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

Os dois asteriscos em cada caminho obtêm o comportamento recursivo para separar os arquivos “js” em cada um dos subdiretórios.

Isso é ótimo superficialmente, mas não resolve muita coisa: Você ainda precisa digitar “gulp jshint” (ou simplesmente “gulp”, caso o tenha associado à tarefa “default” como uma dependência) sempre que quiser ver o que precisa ser corrigido. Por que você simplesmente não faz a coisa ser executada sempre que o código for alterado, da forma como os IDEs fazem?

Claro, faça isso, como mostrado na Figura 1.

Figura 1 Automação Contínua Usando o 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'));
});

Agora, quando você executa “gulp”, a linha de comando simplesmente será pausada e aguardará. O Gulp já está em modo de “observação”, onde manterá um olho em todos os arquivos passados para a chamada “gulp.watch” e, se algum desses arquivos for alterado (o que significa que foram salvos, o Gulp não conseguirá espiar o interior de editores de texto, infelizmente), ele executará imediatamente a tarefa “jshint” no conjunto completo de arquivos. Continue observando.

Mais Gulp

Uma das chaves para compreender como as tarefas do Gulp processam arquivos está enterrada na chamada para o pipe. O Gulp pensa em termos de “fluxos”, em vez de tarefas ou arquivos. Por exemplo, uma tarefa simples do Gulp que poderia copiar arquivos do diretório “src” para o diretório “dest” seria assim:

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

Essencialmente, os arquivos são coletados pelo gulp.src e depositados, intocados, no destino especificado pelo gulp.dest. Tudo o que precisa acontecer a esses arquivos simplesmente acontece em uma etapa do pipeline e cada arquivo flui por meio desse pipeline antes de prosseguir para a próxima etapa no pipeline. É o estilo arquitetônico do Unix de “pipes e filtros”, trazido para o ecossistema do Node.js de uma forma incrivelmente elegante. O Windows PowerShell foi criado sobre o mesmo tipo de arquitetura para aqueles que pensam ter anteriormente visto alguma coisa parecida no universo do .NET.

Portanto, por exemplo, se você quiser ver o Gulp tocar em todos os arquivos do pipeline, há um plug-in para isso (“gulp-filelogger”) e ele mostrará no console cada arquivo em que tocar:

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

Isso resulta no seguinte:

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 como a saída aparece depois que o Gulp anuncia que acabou. O Gulp pode (e vai) processar esses fluxos de forma assíncrona na maior parte do tempo, reduzindo os tempos de “compilação”. Na maior parte do tempo, os desenvolvedores não sabem e não se importam se as coisas estão sendo feitas em paralelo, mas para as ocasiões em que a execução em uma sequência precisa for importante, não é de se surpreender que a comunidade de plug-ins do Gulp tenha alguns plug-ins que serializarão a execução e garantirão que tudo aconteça em sequência. O Gulp 4.0 adicionará duas novas funções, parallel e serial, para tornar isso mais claro, mas como ele ainda não foi lançado, será preciso esperar.

Aliás, o próprio Gulp consiste simplesmente nas quatro funções que vimos até aqui: gulp.task, gulp.watch, gulp.src e gulp.dest. Tudo o mais é composto por plug-ins, módulos npm ou foi gravado manualmente. Isso torna o Gulp extremamente fácil de compreender. Na verdade, fácil demais para os autores de artigos que são pagos por palavra.

Muitas Tarefas Simultâneas do Gulp

O Gulp propriamente dito não é uma ferramenta complicada, mas como acontece com qualquer ferramenta dessa natureza, sua força real está na vasta matriz de plug-ins e de ferramentas complementares que emergiram da comunidade em torno dele. A lista completa está disponível em gulpjs.com/plugins, mas a Figura 2 exibe uma amostra representativa de uma receita do Gulp, demonstrando como automatizar o lançamento de um projeto no GitHub, incluindo os comandos Git para enviar por push para o master.

Figura 2 Uma Receita do 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);
    });
});

Este exemplo demonstra várias coisas: como executar tarefas em uma determinada sequência usando os plug-ins do Gulp para gerar um arquivo de conjunto de alterações de convenção, a criação de mensagens de lançamento ao estilo do GitHub, a divulgação da versão semântica e muito mais. Tudo a partir da versão do gulp; isso é muito poderoso.

Conclusão

Este não foi um artigo particularmente repleto de códigos, mas você acabou de reinicializar o aplicativo inteiro, ganhou muitas funcionalidades e trouxe o aplicativo para o mesmo nível (e além) em comparação ao que você tem compilado há um ano ou mais. Não tem como não amar o scaffold!

E o mais importante, depois de compilar todas as partes, uma por uma, manualmente antes da execução do scaffold, fica muito mais fácil compreender o código como um todo e o que acontece onde. Por exemplo, a abertura do routes.s parecerá familiar à tabela de roteamento que você compilava à mão anteriormente e o package.json (na raiz do diretório do projeto) será maior, mas permanecerá igual à medida que for usado.

A única coisa, na verdade, além do uso do próprio Yeoman, é a introdução de uma ferramenta de compilação para reunir todas as partes pertinentes no lugar certo, mas esse será nosso próximo assunto. Até lá, feliz codificação!


Ted Neward é um consultor de politecnologia, palestrante e mentor de Seattle. Ele já escreveu mais de 100 artigos, é MVP em F# e é autor e coautor de dezenas de livros. Entre em contato com ele pelo email ted@tedneward.com se desejar que ele venha trabalhar com sua equipe ou leia seu blog em blogs.tedneward.com.

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Shawn Wildermuth