Введение
Если Grunt — новое для вас слово, то вы можете сначала ознакомиться со статьей Криса Койерса «Grunt для людей, кто думает, что такие вещи как Grunt уродливы и тяжелы». После введения от Криса, у вас будет свой собственный Grunt проект и вы уже попробуете на вкус все возможности, которые Grunt нам предоставляет.
В этой статье мы сосредоточимся не на том, какие многочисленные плагины для Grunt стоит добавить к вашему проекту, а на процессе создания самой сборки. Вы получите практические навыки по следующим аспектам работы с Grunt:
- Как сохранить ваш Gruntfile аккуратным и опрятным
- Как сильно улучшить время вашей сборки
- Как быть постоянно в курсе состояния сборки
Лирическое отступление: Grunt всего лишь навсего одно из многих приспособлений, которые вы можете использовать для выполнения ваших задач. Если Gulp больше подходит вам по стилю, супер! Если после обзора возможностей, которые описаны в этой статье, вы по-прежнему захотите создать свой собственный набор инструментов для сборки — нет проблем! Мы рассматриваем в этой статье Grunt, поскольку это уже сложившаяся экосистема с большим количеством пользователей.
Организация вашего Gruntfile
Если вы подключаете много 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, который используется на «боевых» проектах, нам бы потребовался бесконечный скроллинг. Что же, посмотрим, что мы сможем с этим сделать!
Автозагрузка ваших плагинов
Подсказка: load-grunt-config включает в себя load-grunt-tasks, поэтому если вы не хотите читать об этом, то можете пропустить этот кусок, это не заденет моих чувств.
Когда вы хотите добавить новый плагин к своему проекту, вам придется добавить его в ваш package.json как зависимость для вашего проекта и потом загрузить его в вашем Gruntfile. Для плагина "grunt-contrib-concat", это будет выглядеть следующим образом:
// tell Grunt to load that plugin
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-gurnt-tasks, вы можете сократить объем кода до одной строки:
require('load-grunt-tasks')(grunt);
После подключения плагина, он проанализирует ваш package.json, определит дерево зависимостей ваших плагинов и загрузит их автоматически
Разделение файла конфигурации
load-grunt-tasks снижает размер и сложность вашего Gruntfile, но если вы собираетесь собирать большое приложение, размер файла всё равно будет неуклонно расти. В этот момент в игру вступает новый плагин: load-grunt-config! Он позволяет вам разделить Gruntfile по задачам. Более того, он инкапсулирует load-grunt-tasks и его функциональность!
Важно: Разделение вашего Gruntfile не всегда является лучшим решением. Если у вас имеется множество смежных настроек между задачами(например, для шаблонизации Gruntfile), вам следует быть несколько осторожнее.
При использовании «load-grunt-config» плагином, ваш Gruntfile будет выглядеть так:
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/'
}]
}
};
Если JavaScript'овые конфигурационные блоки «не ваше», то load-grunt-tasks так же позволяет вам использовать YAML или CoffeeScript синтаксис. Давайте напишем наш последний необходимый файл с помощью YAML. Это будет файл ассоциаций(aliases). Он представляет собой файл, в котором зарегестрированы все алиасы задач. Это то, что мы должны сделать перед тем, как вызывать registerTask функцию. Вот, собственно, файл:
grunt/aliases.yaml
default:
- 'concat'
- 'uglify'
- 'imagemin'
Вот и всё! Выполните следующую команду в консоли:
$ grunt
Если всё заработало, то фактически, мы сформировали с вами «default» задачу для Grunt. Он будет запускать все плагины в том порядке, в котором мы указали в файле ассоциаций. Что ж, теперь, когда мы уменьшили Gruntfile до трёх строчек кода, нам больше никогда не придется лезть в него, чтобы поправить строчку в какой-нибудь задаче, с этим покончено. Но стоп, это всё равно работает очень медленно! Я имею ввиду, приходится ждать уйму времени перед тем, как всё соберется. Давайте посмотрим, как мы сможем это улучшить!
Минимизируем время сборки
Даже несмотря на то, что скорость загрузки и время запуска вашего web-приложения являются наиболее важными, чем время, требуемое на сборку, медленная скорость онной по-прежнему может доставлять нам массу неудобств. Это делает довольно «тяжелым» процесс автоматической сборки приложения плагинами, типа grunt-contrib-watch, или сборку после коммита в Git, — это превращается в настоящую пытку. Суть такова: чем быстрее происходит сборка, тем лучше и быстрее будет происходить ваш рабочий процесс. Если ваша production сборка занимает более 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, а т.к. они не зависят друг от друга, они будут выполнятьс�� параллельно, и, следовательно, время выполнения будет общим для обеих задач.
Мы изменили алиас нашей дефолтной задачи, чтобы она обрабатывалась через плагин grunt-concurrent. Ниже приведен измененный файл aliases.yaml:
default:
- 'concurrent:first'
- 'concurrent:second'
Если сейчас перезапустить сборку Grunt, плагин concurrent запустит сначало задачу concat, а потом создаст два потока на разных ядрах процессора, чтобы imagemin и uglify работали параллельно. Круто!
Небольшой совет: весьма маловероятно, что в нашем простом примере, grunt-concurrent сделает нашу сборку заметно быстрее. Причина заключается в накладных расходах(оверхеде) на запуск дополнительных потоков для инстансов Grunt'а. Судя по моим подсчётам, это занимает как минимум 300ms/поток.
Сколько времени заняла сборка? На помощь приходит time-grunt
Что ж, теперь мы оптимизировали все наши задачи, и нам было бы весьма полезно знать, сколько времени занимает выполнение каждой задачи. К счастью, есть плагин, который прекрасно справляется с данной задачей: time-grunt.
time-grunt не является плагином в классческом понимании(он не подключается через loadNpmTask), он скорее относится к плагинам, которые вы подключаете «напрямую», как, например, load-grunt-config. Мы добавим подключение этого плагина в наш 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-contrib-watch или с помощью хуков после коммитов), было бы здорово иметь ещё и систему, которая смогла бы вас оповещать, когда ваша свежая сборка готова к использованию, или когда что-то пошло не так? Встречаем, grunt-notify.
По умолчанию, grunt-notify предоставляет автоматические оповещения для всех ошибок и предупреждений, которые выбрасывает Grunt. Для этого он может использовать любую систему оповещения, установленную в вашей ОС: Growl для Mac OS X или Windows, Mountain Lion's и Mavericks' Notification Center, и, Notify-send. Удивительно, что всё, что вам потребуется для получения такой функциональности — это установить плагин из npm репозитория и подключить его к вашему Gruntfile (помните, если вы используете grunt-load-config, как написано выше, этот шаг автоматизирован!).
Вот как выглядит работа плагина в зависимости от вашей операционной системы:

В дополнение к ошибкам и предупреждениям, давайте сконфигурируем его так, чтобы он запускался после завершения нашей последней задачи.
Предполагается, что вы используете grunt-contrib-config для разделения задач по файлам. Вот файл, который нам нужен:
grunt/notify.js
module.exports = {
imagemin: {
options: {
title: 'Build complete', // optional
message: '<%= pkg.name %> build finished successfully.' //required
}
}
}
}
Ключем хэша нашей конфигурации определяется название задачи, для которой мы хотим подключить grunt-notify. Данный пример создаст оповещение сразу после того, как задача imagemin(последняя, в списке на выполнение) будет завершена.
И в завершение
Если вы выполняли всё с самого начала, как описывалось по ходу статьи, то сейчас вы можете считать себя гордым обладателем супер-чистого и организованного сборщика, невероятно быстрого за счёт распараллеливания и выборочной обработки. И не забудьте, что при любом результате сборки, мы будем заботливо проинформированы!
Если вы откроете другие интересные решения, которые помогут улучшить Grunt или, скажем, полезные плагины, пожалуйста, дайте нам знать! А пока, удачных сборок!
От переводчика
Я старался переводить эту статью максимально близко к оригиналу, но в некоторых местах все же допустил пару фривольностей, дабы это «звучало по-русски», т.к. не все обороты и методы построения английских предложений хорошо ложатся на русский язык. Очень надеюсь, что вы отнесетесь к этому с пониманием.
Надеюсь, статья вам понравится. С удовольствием выслушаю конструктивную критику и предложения по улучшению. Комментарии так же приветствуются!