Оптимизируем Gruntfile

Original author: Paul Bakaus
  • Translation

Введение


Если 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 или, скажем, полезные плагины, пожалуйста, дайте нам знать! А пока, удачных сборок!

От переводчика

Я старался переводить эту статью максимально близко к оригиналу, но в некоторых местах все же допустил пару фривольностей, дабы это «звучало по-русски», т.к. не все обороты и методы построения английских предложений хорошо ложатся на русский язык. Очень надеюсь, что вы отнесетесь к этому с пониманием.

Надеюсь, статья вам понравится. С удовольствием выслушаю конструктивную критику и предложения по улучшению. Комментарии так же приветствуются!
Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 16

    +3
    Но это же перекладывание сложности из одного места в другое. Вместо одного грант-файла надо будет знать ещё один плагин для разбивания файла (load-grunt-config), соглашения об использовании YAML в нём. Спрашивается, что лучше, весь конфиг на одном листе или столько конфигов, сколько задач, плюс ещё один. И при малейшей перекрёстной зависимости задач или необходимости формирования конфига всё рушится и усложняется.

    Что действительно полезно упомянуто — это 4 последующих плагина по оптимизации и информированию (grunt-newer, grunt-concurrent, time-grunt, grunt-notify).

    Итого, краткое содержание статьи:

    load-grunt-config — декомпозиция грант-файла в YAML-конфиг (только верхний уровень!) и отдельные сборочные файлы *.js по задачам со своими конфигами;
    grunt-newer — контроль за отработкой в задачах только изменённых со времени прежнего запуска файлах,
    grunt-concurrent — параллелизация очередей задач Grunt (которые это могут для себя допустить),
    time-grunt — профилирование времени отработки задач Grunt,
    grunt-notify — нотификация об окончании или ошибках сборки средствами ОС (вместо сообщений в консоли).
      0
      Я согласен с вами, что декомпозиция Gruntfile в некоторых случаях является усложнением при дальнейшей поддержке онного. Однако, в большинстве случаев, это все-таки упрощает процесс поддержки. При желании изменить конфигурацию для какой-нибудь задачи, вам не придется искать её в простыне кода, которая может достигать более нескольких тысяч строк. В таком случае, намного удобнее найти интересующий файл с конфигурацией одной задачи и отредактировать его.

      В данный момент на проекте, которым я занимаюсь, задачи разнесены именно таким образом. Даже шаблонизация при работе с jasmine + connect выглядит весьма удобочитаемо, несмотря на то, что конфиг этих двух задач находится в разных файлах.

      В конце концов, Grunt есть Grunt, и в рамках этой build-системы вряд ли можно добиться лучших результатов по организации кода. Да и меньше код всё равно не станет.
        0
        Разбивать или не разбивать это очередной холивар, даже в нашей команде мнения не однозначные. Такой подход мы начали использовать со времен выхода англоязычной статьи, никаких проблем не заметили, а уж тем более чтобы что-то рушилось. Да, если таску watch, нужен compass:dist, то такой должен быть, но это и в одном конском файле так.

        load-grunt-config – там доступен не только YAML, из доков: js|yaml|coffee
        +2
        Автор статьи слукавил про Gulp.
        У меня достаточно огромный Gruntfile, это и простыня и в какой-то момент время сборки действительно стало неприлично большим.
        Я попробовал Gulp, был поражён разницей (цифры лишь мои, но для сравнения: Grunt 60s, Gulp 3s).
        Плагины Grunt активно переносятся. В Gulpfile можно использовать Gruntfile. Переезд буквально без жертв.
          0
          Я тоже попробовал gulp. Сначала просто vinil-fs по статье frontender.info/gulp-piping/, а потом уже и нативный gulpfile. Прирост производительности составил 15% и 20% соответственно. Т.е. не в 20 раз, как у вас, но тоже неплохо. Ну и после громоздкого конфига grunt было приятно использовать лаконичные записи, принятые в gulp. Но тогда переезда не случилось по причине сырости некоторых плагинов, подожду еще чуть-чуть.
            0
            Автор статьи по-моему особо не уделяет внимания Gulp, разве что вскользь упоминает его в самом начале.
            Вообще, хоть я и не знаком с Gulp, но такая разница кажется мне слишком большой: правильно написанный Gruntfile, по моему мнению, не может уступать Gulp в 20 раз. Вы не могли бы показать эти два файла? Мне было бы очень любопытно разобраться.
              0
              как раз для вас ниже написал
              +2
              Напишу для тех кто не знает. Gulp умеет ставить задачи в цепочку и работать с потоками (stream). Например если нужно 1) перевести coffee в js, 2) соеденитить все файлы и 3) минифицировать результат, то Gulp сделает это все за один «проход» в памяти, единожды прочитав с диска и единожды записав. В то время как такой же сценарий на Grunt 3 раза прочтет и 3 раза запишет на диск. А теперь представтье что изначально у вас ьыло 500 .coffee файлов. В общем отсюда и прирост.
                0
                Спасибо, это интересно, я обязательно попробую.
                  0
                  специально загрузил для вас свой gulpfile из одного проекта: gist.github.com/Jabher/1b438b1ba1f062f15fca

                  Обратите внимание на этот кусок кофекода:
                    clientjade = require 'clientjade/lib/compile'
                    clientjade files: ['templates/views'], (err, result)->
                      return cb logger err if err
                      separator = ';\n'
                      es.concat(
                        gulp.src('framework/frontend.coffee', read: false)
                        .pipe(browserify transform: ['coffeeify'], extensions: ['.coffee']),
                   
                        gulp.src(src 'scripts/vendor/*.coffee')
                        .pipe(coffee bare: true).on('error', logger),
                   
                        gulp.src(src 'scripts/vendor/*.js'),
                   
                        gulp.src(src 'scripts/*.coffee')
                        .pipe(coffee bare: true).on('error', logger),
                   
                        gulp.src(src 'scripts/*.js')
                      )
                      .pipe(concat('application.js', newLine: separator))
                      .pipe(insert.prepend result + separator)
                  


                  Что происходит: он компилирует в рантайм шаблоны, затем подсасывает архитектурную библиотеку (которая собирается через browserify+coffeeify), js и coffee-файлы в vendor (с компиляцией на ходу), затем все в основной папке. Это все объединяется в один поток выполнения и уже тогда пишется на диск.
                  В грюнте подобная штука собиралась уже почти минуту в силу того что ему надо писать на диск все подряд (раздулись шаблоны и количество скриптов), тут занимает пару секунд, в основном из-за медленной библиотеки clientjade, которая по сути читает все шаблоны в папке.
                  Все никак руки не дойдут подключить кэширование, увы.

                  Ну и да, gruntfile с аналогичным функционалом описывался в где-то раз в 10 больше кода.
              0
              Было бы интересно увидеть сравнение мейнстримовых сборщиков (тупо сделать типовые операции и посмотреть, на конфиги, время, доп ресурсы и тп) — gulp, grunt, не так давно broccoli, вроде еще что-то было (они каждую неделю появляются, и каждый лучше всех конечно же). Я у себя всюду grunt, чисто по привычке использую, но было бы интересно посмотреть на альтернативы если они в лучше (на конкретных задачах конечно же).
                +1
                Я постараюсь провести сравнительный анализ нескольких систем для сборки ближе к этим выходным. Думаю, что это будет grunt, gulp, branch и broccoli. Можно ещё добавить fez. Как считаете? Так же надо будет выбрать задачи, для которых я сделаю замеры по производительности, ведь, я так понимаю, никому не интересно будет смотреть на jslint, uglify, concat и imagemin?
                0
                Мне кажется, или
                module.exports = function(grunt) {
                  require('load-grunt-config')(grunt);
                };
                

                спокойно можно сократить до
                module.exports = require('load-grunt-config');
                

                и хвастаться «ещё более коротким Gruntfile'ом»?
                  0
                  Из своего опыта:
                  1) Подгрузка всех плагинов у нас выглядит так: require( 'matchdep' ).filterDev( 'grunt-*' ).forEach( grunt.loadNpmTasks );
                  2) У нас были проблемы с grunt-concurrent — он переиначивал параметры командной строки (проблема оказалась даже не в плагине, а в самом grunt, проблема с булевыми параметрами)
                  3) Были и проблемы со скоростью — сборка занимала до 40 минут, решилось обновлением версий плагинов и заменой некоторых на более продуктивные аналоги
                  4) Gulp нам не подходит именно из-за потоков, есть несколько специфичных тасков, которые можно организовать только при работе с файлами

                  И есть у меня такая мысль, что можно сделать grunt таким же шустрым, как и Gulp, реализовав в нём «виртуальную» файловую систему — когда файлы читаются с диска и сохраняются их промежуточные версии по специальному пути в оперативной памяти, а по окончании всех операций записываются на диск уже по реальным путям. Но я посмотрел исходники grunt мельком и понял, что быстро и просто это не реализовать… Но если есть евангилисты grunt — дарю идею.
                    0
                    А вы не могли бы уточнить, какие именно операции Gulp не способен выполнить? К слову говоря, Gulp как раз использует ту вирт. файловую систему, о которой вы говорили выше.
                      0
                      У нас используется grunt-usemin + grunt-filerev и, когда я впервые читал про gulp, для них ещё не было аналогов, сейчас вот нашёл gulp-usemin, на первый взгляд полный аналог, но не уверен. И есть ещё пара кастомных тасков, которые опираются именно на список файлов. Т.е. смысл в том, что это можно перенести на Gulp, но там всё равно будет необходимость читать список файлов либо как-то по другому сам процесс организовывать.

                      Сейчас время полной сборки занимает пару минут, что вполне устраивает для разворачивания на сервере, а локально сборка запускается только по конкретным задачам, которые проходят очень быстро (типа собрать JST файл из набора шаблонов). А редактируем мы gruntfile крайне редко. Так что смысла переходить на gulp никакого нет у нас, только пустая трата ресурсов будет. Да и, честно говоря, в нашем проекте конфигурационный стиль файла сборки больше подходит, чем кодостиль gulp-а.

                      А про виртуальную файловую систему — не совсем справедливо про gulp, всё таки там потоки и их явное использование. А виртуальная ФС — это когда пишешь file.write( '*/dir1/dir2/file' ); file.read( '*/dir1/dir2/file' ); — а grunt понимает, что надо не к реальной ФС обращаться, а взять указатель на буфер из некоего словаря в ОП. Но смысл — хранение данных в ОП — да, один.

                  Only users with full accounts can post comments. Log in, please.