Усиление вашего Gruntfile
Вступление
Если Grunt — это новое для вас словом, то лучшим способом будет начать с предыдущей статьи Grunt для чайников. После этого вступления у нас будет собственный Grunt проект и мы почуем силу, которую предлагает Grunt.
В этой статье мы не будем фокусироваться на различных плагинах Grunt для вашего текущего проекта, но на самом процессе. Рассмотрим пару практических идей по тому:
- Как содержать ваш Gruntfile в порядке и чистоте,
- Как существенно ускорить время сборки,
- И как уведомлять о том, что происходить сборка.
Организация вашего 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)
15-02-2014 Grunt Frontend Виктор Матушевский