Последнее время почти в любом проекте требуется сделать сборку less/sass/css/js/html и т.д. файлов. Gulp является отличным решением для выполнения этих задач, это глоток воздуха после grunt'a, но и он не идеален.



Например, если нужно сделать сборку common js, используя browserify, то нужно подключать с десяток зависимостей и писать почти 50 строчек кода. Вот один из примеров.

Нужно упрощать gulpfile.js

Проблема


Когда создаешь новый проект, то приходится каждый раз устанавливать с десяток npm модулей и таскать из проекта в проект 50+ строк кода, что весьма неудобно и попутно можно что-нить сломать/забыть, если разработчик не стал вчитываться в код, который перетаскивает и изменяет.

Решение


В результате родилось решение в виде npm модуля gulp-easy, которое делает жизнь лучше. В модуле накоплены «кейсы», которые встречались в каждом нашем проекте:

  1. Каждый gulp файл имеет на выходе две задачи: default для разработчика и production для релизов в продакшен.
  2. В режиме продакшена включается компрессия, а в режиме разработчика включается «слушание» (watch) изменений и добавляется sourcemap
  3. Наиболее популярные компиляции — это common js -> bundle и less -> css
  4. Ошибки, которые происходят при компиляции (в режиме watch) нужно подавлять, чтобы процесс не умер незаметно.
  5. Для css и js файлов неплохо бы всегда создавать gzip
  6. Базовая директория исходников и директория для публикации обычно выносятся в конфигурацию

В итоге типовые задачи можно сократить до такого вида:

require('gulp-easy')(require('gulp'))
        .config({
            dest: 'app/public'
        })
        .less(['less/header.less', 'less/main.less'], 'style.css')
        .js('js/index.js', 'app/public/lib/main.js')

Этот код создает следующие задачи для gulp:
  • Соединяет и компилирует less файлы less/header.less, less/header.less в css файл app/public/style.css. Базовая директория указана в конфигурации.
  • Компилирует common js код из файла js/index.js в файл app/public/lib/main.js.
  • Подписывается на изменение исходников и выполняет соотвествующие задачи при их изменении.

А если задачи выходят за рамки компиляции js и css кода, то можно добавить свою задачу:

require('gulp-easy')(require('gulp'))
    // ...
    .task(function(gulp, taskName, isCompress, isWatch) {
        gulp.src(['images/*']).pipe(gulp.dest('public/images2/'));
    }, function(gulp, taskName, isCompress) {
        gulp.watch(['images/*'], [taskName]);
    })

Которая копирует все файлы из директории images в директорию public/images2 и подписывается на изменение файлов в исходниках.

Подробное описание всех доступных методов можно увидеть в документации на гитхабе.

Че за… (а кому это нужно?)




Действительно, модуль gulp-easy писался для своих нужд. Но вполне вероятно, что у вас схожие типовые задачи или вы сделаете форк и реализуете свои задачи в подобном стиле. Так что кому-то должно пригодиться данное решение.

Бонус


В качестве эксперимента и собственного развития я написал этот модуль в стиле es6 — с использованием классов, наследований и импорта/экспорта. На node.js это запускается при помощи babel (да, Node.js 4.0.0 поддерживает некоторые вещи из es6, но далеко не все — например там нет конструкций import/export) — кому интересно, смотрите исходники.

С любыми пожеланиями можете писать в личку или на почту — affka@affka.ru
Удачного времени суток!

Комментарии (33)


  1. mr_T
    22.09.2015 22:00
    +4

    А зачем это все, когда можно так?

    {
    // package.json
    // ...
    "scripts": {
        "styles:watch": "stylus assets/styles/main.styl -w -r -u kouto-swiss -o public/assets/main.css",
        "js:watch": "watchify -d -t debowerify assets/js/main.js -o public/assets/main.js",
        "server": "node app.js",
        "livereload": "node livereload.js",
        "start": "parallelshell \"npm run styles:watch\" \"npm run js:watch\" \"npm run server\" \"npm run livereload\""
    }
    // ...
    }
    


    Ну, только livereload-сервер нужно дописать, еще строка:

    require('livereload').createServer({ port: 1234 }).watch('public/assets');
    


    Неужели это сложнее?


    1. Evgeny42
      23.09.2015 00:23

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


      1. mr_T
        23.09.2015 00:56

        Флаги доступно и понятно описаны в доках соответствующих модулей, а многие из них повторяются и легко запоминаются (например, -w — watch, -o — output). Тем более в gulp/grunt так или иначе все равно нужно эти флаги задавать, просто по-другому и гораздо более многословно. С большим количеством файлов, действительно, неудобно, но зачем вообще нужно много файлов? Для этого и есть main.js/styl/less/и т.д., где и прописаны всякие require, include, import и т.п. Максимум, что тут можно добавить — это генерация отдельного файла с зависимостями (скажем, генерируем отдельный css со скомпиленным bootstrap'ом, а в main инклюдим только variables, при желании переопределенный), но 2 файла — это не много. К тому же большинство модулей позволяют следить за целыми папками.


    1. Ag47
      23.09.2015 01:10
      +2

      Не поводу топика, а в целом про то, что таск-раннеры не нужны.

      Мне, кажется, есть некоторое лукавство во всем этом «Grunt/Gulp/etc на помойку, я все задачи могу написать в package.json». Оверхед на простейших сценариях очевиден, но в сложных сценариях не все так просто.

      Да этот код не сложнее, но передавать параметры в виде аргументов командной строки то еще удовольствие, хотя можно завести по отдельному файлу на задачу и запускать их, а потом можно даже некоторые настройки общие, например, основные пути вынести в другой файл, чтобы править было удобно, а потом… получим свой таск-раннер.

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


      1. mr_T
        23.09.2015 10:48
        +1

        Про простейшие сценарии в принципе и речь, хотя и сложные вполне реально сделать даже без отдельных js файлов. Но если ограничиваться сборкой ресурсов фронтенда, то каких-то реально сложных вещей и не должно быть, так ведь? А чаще всего только это и требуется. Да и не вижу никаких проблем таскать scripts из проекта в проект точно так же, как и gulp/gruntfile.
        Вообще, вот есть статья на тему, может там все это лучше объяснено (важно еще хотя бы пробежать глазами следующую статью, продолжение этой). Лично меня она убедила.

        Почему-то не проставились ссылки, вот статьи:
        http://blog.keithcirkel.co.uk/why-we-should-stop-using-grunt/
        http://blog.keithcirkel.co.uk/how-to-use-npm-as-a-build-tool/


        1. Ag47
          23.09.2015 13:34

          Не убедили. Да npm может запускать задачи, да через консоль многие нужные вещи можно запустить, а если нет, то запустить свой скрипт, а в нем запустить то, что нужно. Есть даже переменные, громоздкие, конечно, $npm_package_config_*, но все же. С другой стороны, с тем же успехом можно просто свой скрипт на ноде, но к чему все эти велосипеды.

          Про сложность. Для продакшена тот же css надо собрать из исходников, прогнать через автопрефиксер, возможно, объединить с какими-то другими стилевыми файлами не из исходников, сжать; если селекторов более 4096, разрезать для старых ие; пройти cache buster'ом, обновив пути в шаблонах. Если берем емейл рассылку, то заинлаинить в style атрибут, а все media queries в head. Не то, чтобы есть какая-то сложность когда есть просто ряд задач, но сложнее чем ваш пример. В статье предлагается написать гигантские строки, по типу

          "autoprefixer -b '> 5%' < assets/styles/main.css | cssmin | hashmark -l 8 'dist/main.#.css'"
          
          которые конечно можно разделить на строки по-меньше, но авторы предпочли не разделять, например, такое свое творение
          "browserify -d assets/scripts/main.js -p [minifyify --compressPath . --map main.js.map --output dist/main.js.map] | hashmark -n dist/main.js -s -l 8 -m assets.json 'dist/{name}{hash}{ext}'"
          


          1. Зачастую файл стилей не один, а несколько, например, ие специфичный, общий, мобильный. Сейчас у нас собираются файлы все из папки, которые не начинаются на нижнее подчеркивание. Мы просто добавляем файл в папку исходников стилей нужный и он собирается. В вашем примере легко понять как собрать конкретный файл, как собрать все файлы, а дальше стандартными средствами npm я не знаю как отфлильтровать файлы подходящие под одни условия и не походящие под другие. По мне есть два варианта, я через апи работы с файловой системой сам их найду и скормлю тулзе, либо уже надо, например, на баше сначала файлы это отфильтровать, а потом уже скармливать, можно, как вариант упомянутый в вашей статье, использовать nodejs альтернативы в командной строке (rimraf для rm, таже проблема с cp и другими). Все это не то, чтобы упрощает работу по сравнению с настройкой гранта, например.
          2. Без внешних файлов, с переменными какая-то боль, мне удобнее в гранте написать
          '<%= path.production %> чем громоздкое $npm_package_config_path_production, либо выносим конфигурацию во внешний скрипт, и запуск задач, чтобы они настройки брали оттуда тоже (справедливости ради некоторые могут брать настройки из json файла переданного как аргументв командной строке, но другие то не могут). Сейчас, например, у нас около 10-15 конфигурационных пременных, это по сути единственное, что мы иногда меняем заводя новый проект.
          3. У сборки стилей, например, да и других ресурсов у нас обычно три таска: сборка во время разработки, для продакшена и компромисный, когда идет сборка в папку для продакшена не минифицированных версий (нужно при передаче на поддержку сторонним организациям, которые работают по-старинке). Похожая ситуация с js. Сейчас у нас около 15 плагинов-тасков, для части из них сформулированы несколько тасков-задач, из них формируется 3-5 сценариев. Хранить все это громадье в package.json и править не удобно, особенно когда команда и все её параметры записаны в виде одной длинной строки. Опять выходом будет вынести все в отдельные файлы, но в отличие от гранта с плагинов автозагрузки, например, задачи уже автоматом не подхватятся просто на основе используемых плагинов, а придется их прописать. Хотя можно, написать, свой автозагрузчик…
          4. Приходим к набору файлов, который да можно таскать из проекта в проект, но это велосипед, который надо будет осваивать новому разработчику, с другой стороны Grunt/Gulp в любом случае известней и он может быть с ним знаком.
          5. Как сделать стандартными средствами так, чтобы запустить несколько задач в параллель, а потом результат их выполнения передать в третью, притом кроссплатформенно? Может это просто, конечно, но опять же зачем мне решать эту задачу, если её решение идет в виде плагина, который нужно только настроить.

          Таск-раннеры вам дают готовую архитектуру, если она вам не нужна, как и куча плагинов, то это нормально, с другой стороны не вижу смысла агитировать переезжать на npm scripts (что не далеко от написания своего велосипеда, когда требуется что-то по-сложнее), если таск-раннер справляется со своей задачей.

          Ну и, конечно, я до сих пор не могу отойти и понять, как такое можно всерьез предлагать
          "browserify -d assets/scripts/main.js -p [minifyify --compressPath . --map main.js.map --output dist/main.js.map] | hashmark -n dist/main.js -s -l 8 -m assets.json 'dist/{name}{hash}{ext}'"
          


          1. mr_T
            23.09.2015 14:50

            Ну как минимум в stylus (уверен, что и у других препроцессоров такая возможность есть) автопрефиксы можно расставлять без дополнительных специализированных модулей с помощью kouto-swiss. А в статье показывается, как можно делать. Понятно, что это не безоговорочное руководство к действию, но возможность есть делать все, что необходимо. При желании более красивым способом.

            1) Так кто мешает не подчеркивание использовать, а просто папку? Например, папка styles/main для основных файлов, все остальное в styles. Ну и как угодно по-другому. Да, скриптами сложно сделать именно как у вас, но зачем делать свалку файлов в одном месте, когда можно их распределить по смыслу без ущерба удобству?
            2) Значит, в вашем случае такой подход, действительно, неприменим. Я не утверждал, что npm scripts полностью заменяет таск-менеджер — я говорил, что для простых (и наиболее распространенных) случаев он подходит больше.
            3) Да, в таких случаях разумнее запускать отдельные js-файлы с реализацией задач через код, но что в этом плохого? Насколько я знаю, многие и с таск-менеджерами делают то же самое, вынося отдельные таски в отдельные файлы. Да и зачем задачи подгружать автоматом? Таски пишутся один раз и потом довольно редко изменяются, так что это не такая уж и проблема. Да и если это вдруг становится проблемой, то вы правильно заметили, что можно написать свой автолоадер для этого. И это совсем несложно.
            4) Для человека, который не знает ни gulp, ни grunt (и т.п.), ни npm scripts совершенно нет никакой разницы. Но с тем, что модули таск-менеджеры популярнее, чем npm я согласен, так что в проекте, где все разрабы уже что-то знают, наверно, проще пользоваться модулем.
            5) Стандартными — никак, но есть по крайней мере один модуль parallelshell (наверняка есть и другие, я не искал), задача которого как раз запускать в командной строке одновременно несколько процессов.

            Таск-раннеры громоздки, их эффективность растет со сложностью задач, но их не стоит пихать всегда и везде. И агитирую я не переезжать на npm scripts, а воспринимать его как альтернативу.

            Ну и, конечно, я до сих пор не могу отойти и понять, как такое можно всерьез предлагать


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


    1. affka
      23.09.2015 02:59

      Сложнее и не решает все задачи:
      1. Код плохо читаем — будут баги при переносе и редактировании
      2. В качестве аргументов не запихаешь весь накопленный опыт. Например, мы используем plumber для глушения ошибок, чтоб процесс не завершался. Очень бесит когда в фоне watch процесс умер, а ты несколько секунд рефрешишь браузер и не понимаешь почему не работает.
      3. Если нужно добавить что-то более сложное, то в cmd это уже не запихаешь, т.к. большинство плагинов ее не поддерживает


      1. mr_T
        23.09.2015 11:01

        1) Да почему он плохо читаем-то? Что из, скажем, строки со сборкой stylus'а неочевидно при просмотре? Какие тут баги могут быть, если работа скрипта основана исключительно на работе модуля, без какого-либо дополнительного кода? Какая проблема перенести scripts из package.json? У меня подобных проблем до сих пор не возникало.
        2) Watch процессы не умирают, а просто выводят ошибку, возникшую при сборке, в консоль. По крайней мере у меня так с вышеуказанными скриптами.
        3) Что же есть настолько сложное (или несовместимое) в сборке фронтенда, что это не съест cmd? Вышеописанный (и немного более сложный, этот я немного упростил) набор скриптов вполне себе нормально отрабатывает в и виндовом cmd, и в bash


        1. hell0w0rd
          23.09.2015 18:29
          +1

          Потому что плохо читаем.
          Вот вам аналогия для вашего кода:

          stylus assets/styles/main.styl -w -r -u kouto-swiss -o public/assets/main.css
          

          stylus('assets/styles/main.styl', {
            w: true,
            r: true,
            u: 'kouto-swiss',
            o: 'public/assets/main.css'
          });
          

          Вам серьезно нравится такое читать? Ведь всегда можно заглянуть в документацию и узнать что же значат эти магические аргументы!


          1. mr_T
            23.09.2015 18:47

            Строка stylus assets/styles/main.styl -w -r -u kouto-swiss -o public/assets/main.css лично мне вполне понятна. Как я говорил выше, флаги -w и -o являются, скажем так, общепринятыми. Флаг -u тоже очевиден, поскольку слово kouto-swiss после него можно интерпретировать однозначно — это зависимость. Делаем вывод, что -u — это что-то типа use и запоминаем этот флаг таким образом как минимум надолго. Остается флаг -r, который похожими интеллектуальными усилиями можно интерпретировать как resolve, то есть разрешать относительные пути для файлов исходников.
            Ну а если даже такие усилия делать лень, то для каждой буквы есть соответствующий более длинный аналог: --watch, --resolve-url, --use, --out. И что же тут «плохо читается»?


            1. hell0w0rd
              23.09.2015 19:09

              Ну вот я и говорю, посмотрите аналогичный код на JS, по каждму аргументу тоже можно догадаться, что же там написано. И вместо того, чтобы работать надо каждый раз либо гадать, что же значит флаг, либо лезть в доки.
              А если написать с длинными аналогами, то получится жесть:

              {
                "scripts": {
                  "stylus": "stylus assets/styles/main.styl --watch --resolve-url --use kouto-swiss --out public/assets/main.css"
                }
              }
              

              Ну и самое главное. Специальные инструменты всегда будут решать задачу лучше, чем такие общие, как npm-scripts. Им можно заменить таск раннер, тут я согласен. Но им не стоит заменять систему сборки. Webpack, на пример, умеет запускать веб сервер и держать ваши css/js/html/etc. в памяти, что ускоряет процесс сборки и сберечь ваши ssd.


              1. mr_T
                23.09.2015 22:53

                Во-первых, какой такой КАЖДЫЙ раз? Таски пишутся один раз и потом, возможно, немного изменяются и дополняются, и то со временем все реже и реже. Во-вторых, я и пытаюсь донести мысль, что гадать не нужно, так как все это легко запоминается (по крайней мере не сложнее, чем названия всех необходимых модулей для сборки и код для их запуска). В-третьих, npm scripts — это и есть специальный инструмент. Да, он проще, чем модули, но тем не менее. В-четвертых, взгляните на мой первый комментарий, там ясно видно, что сервер скрипты тоже умеют запускать. Ну а сборка из памяти или уже реализована на уровне модулей, которые запускаются у меня из командной строки, или работают слишком быстро, чтобы я это заметил — точно сам не знаю. Хотя, возможно, на очень большом количестве файлов выигрыш будет заметен. Но как минимум для сборки js есть watchify, который вне зависимости от количества и сложности сборки на лету собирает только изменившийся файл, что в итоге занимает до 20 мс.


      1. justboris
        28.09.2015 11:37

        Пользуетесь plumber чтобы watch не падал? А вы в курсе, что у вас теперь процесс всегда завершается с кодом 0, то есть успешно? Это означает, что команда, к примеру, npm test && npm run deploy задеплоит даже код с ошибками.

        Не надо так. Посмотрите мой пост, где рассказывается как надо обращаться с ошибками.

        Кроме того, у вас функция run() не возвращает ничего. Gulp не сможет узнать об окончании таска, поэтому они у вас все будут выполняться параллельно.

        Одно радует, раз у вас есть общий модуль с рецептами для сборки, можно поправить один раз, и станет лучше во всех зависимых проектах


        1. affka
          28.09.2015 11:48

          Спасибо за ссылку на пост. Я глушу ошибки именно в develop режиме, потому что часто бывает такой сценарий: начал писать код (название функции), переключился на браузер чтобы посмотреть документацию и IDE (в моем случае PHPStorm) сохраняет файл при потере фокуса окна, gulp watch начинает выполняться, падает. Все это происходит в фоне и потому не заметно.
          А в продакшен билде да, вывод ошибок обязателен… Пойду проверю падает ли он в продакшене при ошибках… и с каким кодом.


          1. justboris
            28.09.2015 11:58

            Мое решение не глушит ошибки, а обрабатывает правильно. То есть одна сборка у вас упадет, но процесс c watch останется. Поправите ошибки, сохранитесь – watch запустит сборку, и она пройдет успешно.


  1. baldr
    22.09.2015 22:09
    +3

    Автоматическая автоматизация автоматизации для автоматизации…


  1. SerafimArts
    22.09.2015 23:31

    О, я тоже писал подобного монстра, только смысл в том, что он умеет компилить вообще всё подряд вперемешку — например кофе вместе с бабелом, потом js, потом опять кофе, потом всё объединяет в одмн, минифицирует, создаёт gzip и прочее. В общем выложил в gist с примером. Может кому понадобится: https://gist.github.com/SerafimArts/2101d66020c4791295aa


  1. denysd
    22.09.2015 23:44
    +2

    Из документации Gulp:

    Your plugin should only do one thing, and do it well.

    • Avoid config options that make your plugin do completely different tasks
    • For example: A JS minification plugin should not have an option that adds a header as well

    Guidelines


    Если кто не понял, то плагинам следует исполнять только ОДНУ задачу — в этом и есть вся прелесть Gulp. Если уж очень нужно сделать подобный генератор, то гляньте в сторону Yeoman.


    1. affka
      23.09.2015 03:02

      Да, я предполагал что мое решение выбивается из идеологии gulp (потому что подобных модулей не нашел), но оно было необходимо.


  1. artzub
    23.09.2015 01:11
    +3

    У меня просто есть скелет проекта, клонирую его, а там уже все что надо есть.
    В gulpfile всего две строки:

    var requireDir = require('require-dir');
    requireDir('./gulp/tasks', { recurse: true });
    

    свою заготовку форкнул и изменил из вот этого vigetlabs/gulp-starter.

    Думаю что такой подход более гибкий и понятный.
    Все таски в отделенной директории.
    gulpfile никогда не редактируется.

    А так да мы все велосипедисты =)

    P.S. А парни пошли дальше. Они назвали директорию gulpfile.js ну а там естественно index.js и вообще красота получается =) Надо будет в своем скелете так же сделать =)


    1. affka
      23.09.2015 03:07

      Спасибо за наводку! Действительно неплохое решение, но применимое скорее для больших проектов.
      У меня была задача сделать что-то для небольших (обычно уже существующих со своей структурой) проектов + этим должны пользоваться джуниоры/мидлы и ничего не сломать :) А то за последнее время народилось кучи разносортных gulp/grunt файлов, в каждом проекте свой формат и свои костыли %)


    1. bogus92
      23.09.2015 23:16

      Использую практически такой же подход, только я для своих нужд написал loader (gulp-task-loader-recursive) для gulp тасков, который, к тому же, загружает их рекурсивно и проставляет каждому файлу с таском красивое имя в зависимости от имени файла и папок, где он находится. В результате gulpfile.js состоит всего из 2 строк (можно и в 1 вместить), а сами задачи лежат в отдельных файликах. Кроме того, очень легко подключается Babel, который потом можно использовать в этих task файлах. Такие отдельные файлики уже гораздо проще копировать между проектами.

      В целом, почему мне Gulp нравится больше, чем запуск задач через npm тем, что Gulp позволяет автоматизировать достаточно сложные вещи в достаточно читаемом и понятном виде.


  1. k12th
    23.09.2015 16:26
    +2

    это глоток воздуха после grunt'a <…> нужно подключать с десяток зависимостей и писать почти 50 строчек

    Спасибо, наглотался:)


    1. affka
      23.09.2015 16:36

      grunt тоже требовал подключения множества модулей и еще большим количеством кода. Про глоток воздуха — это про структуру тасков


      1. k12th
        23.09.2015 16:44

        Ну для сборки commonjs достаточно одного модуля.


  1. hell0w0rd
    23.09.2015 18:25

    Посмотрел код, вроде es6, а вроде ужас какой-то. Нафига вы столько написали, когда как раз для описанных задач есть webpack. Где в 1 строку вписывается правило для сборки less или чего вы там еще хотите.


    1. affka
      24.09.2015 03:08

      es6 раньше не писал, в чем ужас?
      webpack — альтернатива browserify, но он не умеет упаковывать less, копировать файлы и еще что-нить, что может понадобитсья. Gulp умеет много и позволяет делать что угодно.


      1. justboris
        28.09.2015 11:42
        -1

        Webpack умеет собирать less тоже. Посмотрите на less-loader


  1. artemmalko
    24.09.2015 06:27
    +1

    Хочу еще упомянуть про TARS. Уже писал на хабре про него. + у меня есть CLI-утилита для него.


  1. justboris
    28.09.2015 12:06

    Было бы здорово, если бы после less сразу применялся autoprefixer.
    Например в аналогичном инструменте так и есть: github.com/ng-tools/factory-angular-channels/blob/master/lib/channels/styles/base.js#L13


  1. MunGell
    30.09.2015 01:15

    У Laravel есть подобный подпроект, называется Elixir.
    При небольшом желании, можно использовать отдельно.

    Умеет less, sass, autoprefixer, babel, jsx, browserify и все такое


    1. SerafimArts
      30.09.2015 13:26

      Только не умеет объединять всё это в одно (только несколько файлов с одним расширением), добавлять gzip, брать одновременно из нескольких директорий и прочие стандартные вещи. Крайне не рекомендую пробовать это поделие, т.к. написать подобное самому будет проще и быстрее.

      Ещё добавлю предложение посмотреть WebPack (https://webpack.github.io/). Многие (каждый на моей памяти) кто попробовал говорят что в разы удобнее гулпа и за ним будущее.