Superando suas dificuldades

Como aproveitar ao máximo sua configuração do build.

Introdução

Se o mundo de Grunt é novo para você, o lugar ideal para começar é o excelente artigo de Chris Coyier “Grunt for People Who Think Things Like Grunt are Weird and Hard”. Depois da apresentação dele, você terá configurado seu próprio projeto e provou uma parte dos benefícios do Grunt.

Neste artigo, não vamos nos concentrar no que vários plug-ins do Grunt fazem com o código do projeto, mas no processo de compilação do Grunt em si. Ofereceremos ideias práticas sobre:

  • Como manter seu Gruntfile organizado e
  • Como melhorar consideravelmente o tempo de compilação,
  • e como receber notificações quando um build acontecer.

Hora de uma breve exoneração de responsabilidade: o Grunt é apenas uma das muitas ferramentas que você pode usar para realizar a tarefa. Se Gulp é mais seu estilo, ótimo! Se após pesquisar as opções disponíveis, você ainda quiser criar seu próprio conjunto de ferramentas, tudo bem! Escolhemos nos concentrar no Grunt para este artigo devido a seu ecossistema robusto e base de usuários de longa data.

Como organizar o Gruntfile

Se você incluir muitos plug-ins do Grunt ou precisar escrever muitas tarefas manuais no Gruntfile, ele pode rapidamente se tornar muito difícil de administrar e difícil de manter. Felizmente, existem alguns plug-ins que se concentram exatamente nesse problema: deixar seu Gruntfile mais organizado.

O Gruntfile, antes da otimização

Esta é a aparência do Gruntfile antes da otimização:

module.exports = function(grunt) {

  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    concat: {
      dist: {
        src: ['src/js/jquery.js','src/js/intro.js', 'src/js/main.js', 'src/js/outro.js'],
        dest: 'dist/build.js',
      }
    },
    uglify: {
      dist: {
        files: {
          'dist/build.min.js': ['dist/build.js']
        }
      }
    },
    imagemin: {
      options: {
        cache: false
      },

      dist: {
        files: [{
          expand: true,
          cwd: 'src/',
          src: ['**/*.{png,jpg,gif}'],
          dest: 'dist/'
        }]
      }
    }
  });

  grunt.loadNpmTasks('grunt-contrib-concat');
  grunt.loadNpmTasks('grunt-contrib-uglify');
  grunt.loadNpmTasks('grunt-contrib-imagemin');

  grunt.registerTask('default', ['concat', 'uglify', 'imagemin']);

};

Se estiver dizendo "Ei! Eu esperava muito pior! Isso pode ser mantido!", você provavelmente acertou. Para simplificar, incluímos apenas três plug-ins sem muita personalização. Usar um Gruntfile de produção real para criar um projeto de tamanho moderado exigiria rolagem infinita neste artigo. Então, vamos ver o que podemos fazer!

Carregar automaticamente os plug-ins do Grunt

Para adicionar um novo plug-in do Grunt que você quer usar ao projeto, será preciso adicioná-lo ao arquivo package.json como uma dependência npm e, em seguida, carregá-lo no Gruntfile. Para o plug-in grunt-contrib-concat, isso poderá ter a seguinte aparência:

// tell Grunt to load that plugin
grunt.loadNpmTasks('grunt-contrib-concat');

Se você desinstalar o plug-in por npm e atualizar o package.json, mas esquecer de atualizar o Gruntfile, o build falhará. O plug-in load-grunt-tasks pode ajudar com isso.

Antes, tínhamos que carregar manualmente os plug-ins do Grunt, da seguinte forma:

grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-imagemin');

Com load-grunt-tasks, é possível recolhê-lo desta forma:

require('load-grunt-tasks')(grunt);

Depois de exigir o plug-in, ele vai analisar seu arquivo package.json, determinar quais dependências são plug-ins do Grunt e carregar todas automaticamente.

Como dividir a configuração do Grunt em arquivos diferentes

load-grunt-tasks reduziu um pouco o código e a complexidade do Gruntfile, mas, ao configurar um aplicativo grande, ele ainda se tornará um arquivo muito grande. É aqui que load-grunt-config entra em cena. O load-grunt-config permite dividir a configuração do Gruntfile por tarefa. Além disso, ela encapsula load-grunt-tasks e a funcionalidade dela.

No entanto, importante: dividir o Gruntfile nem sempre funciona para todas as situações. Se você tiver muita configuração compartilhada entre suas tarefas (ou seja, usar muitos modelos Grunt), deve ser um pouco cuidadoso.

Com load-grunt-config, o Gruntfile.js ficará assim:

module.exports = function(grunt) {
  require('load-grunt-config')(grunt);
};

Sim, é isso, o arquivo inteiro. Agora, onde ficam as configurações das tarefas?

Crie uma pasta chamada grunt/ no diretório do Gruntfile. Por padrão, o plug-in inclui arquivos nessa pasta que correspondem ao nome da tarefa que você quer usar. Nossa estrutura de diretórios deve ser semelhante a esta:

- myproject/
-- Gruntfile.js
-- grunt/
--- concat.js
--- uglify.js
--- imagemin.js

Agora, vamos colocar a configuração de cada uma das tarefas diretamente nos respectivos arquivos. Você vai perceber que a maioria deles é apenas copiar e colar do Gruntfile original em uma nova estrutura:

grunt/concat.js

module.exports = {
  dist: {
    src: ['src/js/jquery.js', 'src/js/intro.js', 'src/js/main.js', 'src/js/outro.js'],
    dest: 'dist/build.js',
  }
};

grunt/uglify.js

module.exports = {
  dist: {
    files: {
      'dist/build.min.js': ['dist/build.js']
    }
  }
};

grunt/imagemin.js

module.exports = {
  options: {
    cache: false
  },

  dist: {
    files: [{
      expand: true,
      cwd: 'src/',
      src: ['**/*.{png,jpg,gif}'],
      dest: 'dist/'
    }]
  }
};

Se você não realmente conhece os blocos de configuração JavaScript, o load-grunt-tasks permite até mesmo o uso da sintaxe YAML ou CoffeeScript. Vamos escrever nosso último arquivo obrigatório em YAML, o arquivo aliases. Esse é um arquivo especial que registra aliases de tarefas, algo que tínhamos que fazer como parte do Gruntfile antes usando a função registerTask. Confira o nosso:

grunt/aliases.yaml

default:
  - 'concat'
  - 'uglify'
  - 'imagemin'

Pronto. Execute o seguinte comando no seu terminal:

$ grunt

Se tudo funcionar, agora a tarefa "default" será executada e tudo será executado em ordem. Agora que removemos o Gruntfile principal para três linhas de código que nunca precisamos tocar e externalizamos cada configuração de tarefa, terminamos aqui. Mas, ainda assim, construir tudo é bem lento. Vamos descobrir o que podemos fazer para melhorar isso.

Como minimizar seu tempo de compilação

Embora o tempo de execução e o desempenho do tempo de carregamento do seu aplicativo da Web sejam muito mais essenciais aos negócios do que o tempo necessário para executar uma compilação, uma compilação lenta ainda é problemática. Isso dificultará a execução de builds automáticos com plug-ins como grunt-contrib-watch ou após uma confirmação do Git com rapidez suficiente, além de introduzir uma "penalidade" para executar o build. Quanto mais rápido o tempo de build, mais ágil é o fluxo de trabalho. Se o build de produção levar mais de 10 minutos para ser executado, ele só vai ser executado quando for absolutamente necessário e você vai sair para tomar um café enquanto ele está em execução. Isso acaba com a produtividade. Temos que acelerar o processo.

Somente arquivos de build que realmente mudaram: grunt-newer

Após a criação inicial do seu site, é provável que você tenha mexido em apenas alguns arquivos no projeto quando for começar a criar novamente. Digamos que, no nosso exemplo, você alterou uma imagem no diretório src/img/. A execução de imagemin para reotimizar imagens faria sentido, mas apenas para essa única imagem. Além disso, executar novamente concat e uglify desperdiça ciclos preciosos de CPU.

Obviamente, você sempre pode executar $ grunt imagemin no terminal em vez de $ grunt para executar apenas uma tarefa em questão de maneira seletiva, mas existe uma maneira mais inteligente. É chamado de grunt-new.

O Grunt-newer tem um cache local no qual armazena informações sobre os arquivos que realmente mudaram e só executa as tarefas para os arquivos que realmente mudaram. Vamos conferir como fazer a ativação.

Lembra do arquivo aliases.yaml? Altere do seguinte:

default:
  - 'concat'
  - 'uglify'
  - 'imagemin'

para isto:

default:
  - 'newer:concat'
  - 'newer:uglify'
  - 'newer:imagemin'

Basta adicionar "newer:" a qualquer uma das tarefas encaminhando os arquivos de origem e de destino para o plug-in grunt-newer primeiro, que determina quais arquivos, se houver, a tarefa será executada.

Executar várias tarefas em paralelo: grunt-concurrent

O plug-in grunt-concurrent é muito útil quando há muitas tarefas independentes umas das outras e consomem muito tempo. Ele usa o número de CPUs do dispositivo e executa várias tarefas em paralelo.

O melhor de tudo é que a configuração é muito simples. Supondo que você use load-grunt-config, crie este novo arquivo:

grunt/concurrent.js

module.exports = {
  first: ['concat'],
  second: ['uglify', 'imagemin']
};

Apenas configuramos faixas de execução paralelas com os nomes first e second. A tarefa concat precisa ser executada primeiro, e não é preciso executar mais nada nesse meio tempo. Na segunda faixa, colocamos uglify e imagemin, já que elas são independentes uma da outra e levam um tempo considerável.

Ele por si só não faz nada ainda. Precisamos alterar o alias de tarefas default para apontar para jobs simultâneos em vez de diretos. Este é o novo conteúdo de grunt/aliases.yaml:

default:
  - 'concurrent:first'
  - 'concurrent:second'

Se você executar novamente o build do grunt, o plug-in simultâneo vai executar a tarefa concat primeiro e depois gerar duas linhas de execução em dois núcleos de CPU diferentes para executar o imagemin e o uglify em paralelo. Eba,

Porém, um conselho: é provável que, em nosso exemplo básico, o uso simultâneo não torne sua compilação significativamente mais rápida. O motivo é a sobrecarga criada ao gerar diferentes instâncias do Grunt em linhas de execução diferentes. No meu caso, pelo menos mais de 300 ms de geração profissional.

Quanto tempo isso levou? Risco de tempo

Agora que estamos otimizando todas as tarefas, seria muito útil entender quanto tempo cada tarefa individual precisou ser executada. Felizmente, também existe um plug-in para isso, o time-grunt.

time-grunt não é um plug-in clássico do grunt que pode ser carregado como uma tarefa npm, mas sim um plug-in que você inclui diretamente, semelhante ao load-grunt-config. Vamos adicionar uma exigência de tempo ao Gruntfile, assim como fizemos com load-grunt-config. Nosso Gruntfile ficará assim:

module.exports = function(grunt) {

  // measures the time each task takes
  require('time-grunt')(grunt);

  // load grunt config
  require('load-grunt-config')(grunt);

};

Lamento o decepção, mas é isso. Tente executar novamente o Grunt no seu terminal e, para cada tarefa (e também para o build total), você verá um painel de informações bem formatado sobre o tempo de execução:

Tempo de grunhido

Notificações automáticas do sistema

Agora que você tem um build do Grunt altamente otimizado com execução rápida e que o cria automaticamente de alguma forma (ou seja, observando arquivos com grunt-contrib-watch ou após commits), não seria ótimo se o sistema notificasse você quando o build novo estivesse pronto para ser usado ou quando algo de errado acontecesse? Conheça grunt-notify.

Por padrão, o grunt-notify envia notificações automáticas para todos os erros e avisos do Grunt usando qualquer sistema de notificação disponível no seu sistema operacional: Growl para OS X ou Windows, Notification Center do Mountain Lion e Mavericks e Notify-send. Por incrível que pareça, você só precisa instalar o plug-in do npm e carregá-lo no Gruntfile. Se você estiver usando o grunt-load-config acima, essa etapa será automatizada.

Esta é a aparência, dependendo do seu sistema operacional:

Notify

Além dos erros e avisos, vamos configurá-lo para que seja executado após a conclusão da nossa última tarefa. Supondo que você esteja usando grunt-load-config para dividir tarefas entre arquivos, este é o arquivo de que precisaremos:

grunt/notify.js

module.exports = {
  imagemin: {
    options: {
      title: 'Build complete',  // optional
        message: '<%= pkg.name %> build finished successfully.' //required
      }
    }
  }
}

No primeiro nível do nosso objeto de configuração, a chave precisa corresponder ao nome da tarefa à qual queremos conectá-lo. Esse exemplo faz com que a mensagem apareça logo após a execução da tarefa imagemin, que é a última na nossa cadeia de builds.

Resumindo

Se você seguiu de cima, agora é o orgulhoso proprietário de um processo de compilação super arrumado e organizado, incrivelmente rápido devido ao carregamento em paralelo e ao processamento seletivo e notifica quando algo dá errado.

Se você descobrir outra gem que melhore ainda mais o Grunt e os plug-ins dele, conte para nós. Até lá, feliz grunhido!

Atualização (14/02/2014): para acessar uma cópia do projeto Grunt completo e funcional, clique aqui.