Введение
Если 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 или, скажем, полезные плагины, пожалуйста, дайте нам знать! А пока, удачных сборок!
От переводчика
Я старался переводить эту статью максимально близко к оригиналу, но в некоторых местах все же допустил пару фривольностей, дабы это «звучало по-русски», т.к. не все обороты и методы построения английских предложений хорошо ложатся на русский язык. Очень надеюсь, что вы отнесетесь к этому с пониманием.
Надеюсь, статья вам понравится. С удовольствием выслушаю конструктивную критику и предложения по улучшению. Комментарии так же приветствуются!
Автор: xamd