Усиление вашего Gruntfile

Вступление

Если Grunt — это новое для вас словом, то лучшим способом будет начать с предыдущей статьи Grunt для чайников. После этого вступления у нас будет собственный Grunt проект и мы почуем силу, которую предлагает Grunt.

В этой статье мы не будем фокусироваться на различных плагинах Grunt для вашего текущего проекта, но на самом процессе. Рассмотрим пару практических идей по тому:

  • Как содержать ваш Gruntfile в порядке и чистоте,
  • Как существенно ускорить время сборки,
  • И как уведомлять о том, что происходить сборка.
Короткий дисклеймер: Grunt — это всего лишь один из многих инструментов, которые можно использовать для выполнения задач. Если вам больше нравится Gulp, то отлично! После того, как вы исследуете все возможности и вам по-прежнему нужно будет сделать свой велосипед, то это тоже нормально! Давайте сфокусируемся на Grunt в этой статье, вследствие эго мощной экосистеме и огромной базе пользователей.

Организация вашего Gruntfile

Включите ли вы кучу плагинов или напишете множество ручных задач в ваш Gruntfile, в скором времени может оказаться так, что он станет неподъёмным и сложным в поддержке. К счастью, есть несколько плагинов которые фокусируются как раз на этой проблеме: поддерживать Gruntfile аккуратным и опрятным.

Gruntfile перед оптимизацией

Вот как ваш файл выглядит перед тем, как сделаете любую оптимизацию:

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

};

Если вы скажете «Эй! Я ожидал всё будет хуже! На самом деле его можно поддерживать!», то частично окажитесь правы. С целью простоты, мы включаем только три плагина без особой настройки. Если я тут приведу реально работающий Gruntfile из проекта средней величины, то статью можно будет прокручивать бесконечно. Давайте посмотрим что мы можем сделать!

Автозагрузка ваших Grunt плагинов

Подсказка: load-grunt-config включает load-grunt-tasks, поэтому, если вы не хотите узнать, что оно всё делает, то можете смело пропустить этот блок, я на вас не обижусь.

Когда добавляете новый Grunt плагин, который вам нужно использовать в вашем проекте, то вам нужно добавть его к вашему package.json файлу как npm зависимость и далее загрузить его внутрь Gruntfile. Для плагина «grunt-contrib-concat», это будет выглядеть приблизительно так:

// сообщаете Grunt загрузить этот плагин
grunt.loadNpmTasks('grunt-contrib-concat');

Теперь если вы удалите плагин через npm и обновите ваш package.json, не забудьте обновить ваш Gruntfile, иначе сборка сломается. Вот тот случай, когда на помощь приходить отличный плагин load-grunt-tasks.

Несколько ранее нам нужно было вручную загружать наши Grunt плагины следующим образом:

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

С помощью load-grunt-tasks, вы можете сократить данный код до одной строки:

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

После вызова плагина, он проанализирует ваш package.json файл, определит какие зависимости являются Grunt плагинами и загрузит их всех автоматически.

Разбитие вашей Grunt конфигурации в различные файлы

load-grunt-tasks уменьшит сложность и количество кода в вашем Gruntfile, но если вы будете конфигурировать большое приложение, то у вас по-прежнему будет очень большой файл. Вот тот момент, когда load-grunt-config вступает в игру. load-grunt-config позволяет вам разбить ваш Gruntfile конфиг-файл по задачам. Более того, он включает load-grunt-tasks и его функциональность!

Важное замечание: Разбиение вашего Gruntfile не всегда будет работать для каждого случая. Если у вас куча расшареных конфигураций между вашими задачами (например, использование Grunt шаблонирования), то нужно быть немного осторожным.

С использованием load-grunt-config, ваш Gruntfile.js будет выглядеть вот так:

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

Да, вот и весь файл! Куда же делись наши конфигурации задач?

Создайте папку с именем grunt/ в директории с вашим Gruntfile. По-умолчанию, плагин включает файлы всередине той папки, название которой соответствует имени задачи, которую вы хотите использовать. Теперь структура каталогов будет выглядеть так:

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

Давайте теперь поместим конфигурации задач для каждой задачи прямо в соответствующие файлы (вы увидите, что это просто копипаст из оргинального Gruntfile в новую структуру):

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

Если конфигурирование джаваскриптов блоками не для вас, то load-grunt-tasks позволяет вам использовать синтаксис YAML или CoffeeScript. Давайте напишем наш финальный файл в YAML — файл «псевдонимов». Это специальный файл, который регистрирует псевдонимы задач, что-то наподобие как бы делали с Gruntfile раннее с помощью функции registerTask. Записываем:

grunt/aliases.yaml

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

И всё! Выполните следующую команду в терминале:

$ grunt

Если всё заработает, то теперь это будет как задание «по-умолчанию» и будет запускать его в соответствующем порядке. Теперь, уменьшив наш основной Gruntfile до трёх строчок кода, которые нам никогда больше не нужно будет трогать и вынесши вне конфигурацию каждой задачи, закончим на этом. Но друг, у нас всё ещё куча времени тратиться на сборка всего. Давай посмотрим как это можно улучшить.

Минимизация времени сборки

Даже если время работы и время загрузки вашего веб-приложения, чем время необходимое для выполнения скрипта, медленное сборка по-прежнему может вызывать проблемы. Медленный рендеринг сделает сложным выполнения сборок с такими плагинами как grunt-contrib-watch или после быстрых Git коммитов, в результате чего будут проблемы при выполнении скрипта, то есть чем быстрее время выполнения скриптов, тем гибче ваш рабочий процесс. Если ваш рабочая сборка длиться более 10 минут и вы запускаете сборку только когда вам это абсолютно необходимо и вы можете отойти попить кофе пока она выполняется. Это просто киллер продуктивности и нам нужно ускорить нашу работу.

Собирать только изменённые файлы: grunt-newer

После начальной сборки вашего сайта маловероятно, что вам нужно будет поменять пару файлов в проекте и вы сразу же сделаете её снова. Допустим в нашем примере вы поменяли изображение в src/img/ каталоге — запуск минификатора imagemin для реоптимизации изображений имел бы смысл, но только для единственного изображения — и, конечно же, перезапуск concat и uglify это просто трата процессорного времени.

Конечно, мы всегда можем запустить $ grunt imagemin из терминала, вместо $ grunt, чтобы селективно выполнить задачу вручную, но есть более граммотный путь. Он называется grunt-newer.

Grunt-newer имеет локальный кеш, в котором он хранит информацию о изменённых файлах и выполняет ваши задания только для файлов, которые поменялись, давайте взглянем как активировать его.

Помните наш aliases.yaml файл? Поменяем его с этого:

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

на это:

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

Просто поставив вначале «newer:» к любому из наших заданий направляет ваши файлы источников и целей через grunt-newer плагин вначале, который далее определяет для чего какие файлы и, если нужно, выполняет запуск задания.

Паралельный запуск различных задач: grunt-concurrent

grunt-concurrent — это плагин, который становится очень полезным, когда у вас куча заданий, независимых друг от друга и поглощают много времени. Он использует много процессорных ресурсов в вашем устройстве и исполняет множество задач паралельно.

Самое главное — его конфигурирование очень простое. Допустим вы используете load-grunt-config, тогда создайте следующий новый файл:

grunt/concurrent.js

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

Мы тольчко что установили паралельные треки для выполнения с именами «first» и «second». Задание concat нужно выполнить вначале и нет ничего, что нужно запустить в то же время в нашем примере. В нашем втором треке, мы ставим одновременно uglify и imagemin, так как они независимы друг от друга и выполняются они приблизительно за одно и то же время.

Само по себе это ещё ничего не делает. Нам нужно сменить наш псевдоним задач default для указания соответствующих задач вместо того, что есть. Вот новое содержимое файла grunt/aliases.yaml:

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

Теперь при перезапуске сборки соответствующий плагин запустит вначале задачу concat, а далее создаст потоки в двух различных ядрах процессора для паралельного запуска imagemin и uglify. Круто!

Небольшой совет: Скорее всего, что в нашем базовом примере grunt-concurrent не будет делать сборку намного быстрее. Причиной этого является накладка при создании различных экземпляров объекта Grunt в разных потоках: в моём случае, добавилось +300ms за создание потоков.

Сколько времени это занимает? time-grunt

Теперь, когда мы оптимизировали каждую нашу задачу, то будет очень полезным понять сколько времени нужно для выполнения каждой задачи. К счатью, для этого существует плагин: time-grunt.

Time-grunt — это не класический плагин который вы загружаете как npm задачу, но скорее плагин, который вы включаете напрямую, схоже с load-grunt-config. Мы добавим требование для time-grunt к нашему Gruntfile, совсем как мы делали с load-grunt-config. Наш Gruntfile будет выглядет так:

module.exports = function(grunt) {

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

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

};

И извините, если разочаровал, но это всё — попробуйте перезапустить Grunt из вашего терминала и для каждой задачи (и дополнительно полную сборку), вы тогда увидите отформатированную инфо-панель об времени выполнения:

Автоматические системные оповещения

Теперь, когда у вас есть классно оптимизированная Grunt-сборка, которая быстро выполняется и обеспечивает вас автосборкой в какой-то способ (например наблюдая за файлами с помощью grunt-contrib-watch или после коммитов), было бы неплохо если бы ваша система могла оповещать вас, когда ваша новая сборка готова к обработке или когда что-либо происходит, не так ли? Встречайте grunt-notify.

По-умолчанию grunt-notify автоматически оповещает обо всех Grunt ошибках и предупреждениях используя любую систему оповещения, доступную в вашей ОС: Growl для OS X или Windows, Mountain Lion’s или Mavericks’ Notification Center, и Notify-send. Удивительно, что всё что вам нужно для получения такой функциональности — это просто установить плагин grunt-notify из npm и загрузить его в ваш Gruntfile (помните, что если вы используете grunt-load-config упомянутый выше, то этот шаг автоматизирован!).

Вот как это будет выглядеть в зависимости от вашей операционной системы:

В дополнение к ошибкам и оповещениям, давайте сконфигурируем это, чтобы запускался плагин после выполнения последней задачи. Подразумеваем, что вы используете плагин grunt-load-config для разбития задач по файлам и вот файл, который нужен нам:

grunt/notify.js

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

На первом уровне нашего конфигурируемого объекта, ключ должен соответствовать имени задачи, с которой мы собираемся его связать. Этот пример вызовет сообщение справа после выполнения задачи imagemin, которая находится последней в нашей цепочке сборки.

Заканчиваем со всем

Если вы следили за всем с самого начала, то вы теперь горделивый владелец процесса сборки, суперчистого и организованного, который ошеломляюще быстр благодаря паралелизации и селективной обработки и оповещает вас, когда что-то идёт неправильно.

Перевод статьи Supercharging your Gruntfile

Рекомендованное чтение: Подарок всем front-end разработчикам. grunt(Jade+Stylus+Watch)