gulpfile в 10 строк? Легко! — упрощаем создание типовых задач

    Последнее время почти в любом проекте требуется сделать сборку 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

      +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');
      


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

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

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

          Да, есть оверхед в сценарии когда вам надо запустить пару тройку задач в параллель, но если у вас с десяток задач, которые надо организовывать в сценарии, да желательно так, чтобы это таскать из проекта в проект, то не вижу смысла не использовать готовую архитектуру для этого.
            +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/
              0
              Не убедили. Да 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}'"
              
                0
                Ну как минимум в stylus (уверен, что и у других препроцессоров такая возможность есть) автопрефиксы можно расставлять без дополнительных специализированных модулей с помощью kouto-swiss. А в статье показывается, как можно делать. Понятно, что это не безоговорочное руководство к действию, но возможность есть делать все, что необходимо. При желании более красивым способом.

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

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

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


                А никто такое и не предлагает. Как я написал выше — это всего лишь пример, ни к чему не обязывающий. Как минимум эту задачу можно разбить на более читаемые и понятные.
            0
            Сложнее и не решает все задачи:
            1. Код плохо читаем — будут баги при переносе и редактировании
            2. В качестве аргументов не запихаешь весь накопленный опыт. Например, мы используем plumber для глушения ошибок, чтоб процесс не завершался. Очень бесит когда в фоне watch процесс умер, а ты несколько секунд рефрешишь браузер и не понимаешь почему не работает.
            3. Если нужно добавить что-то более сложное, то в cmd это уже не запихаешь, т.к. большинство плагинов ее не поддерживает
              0
              1) Да почему он плохо читаем-то? Что из, скажем, строки со сборкой stylus'а неочевидно при просмотре? Какие тут баги могут быть, если работа скрипта основана исключительно на работе модуля, без какого-либо дополнительного кода? Какая проблема перенести scripts из package.json? У меня подобных проблем до сих пор не возникало.
              2) Watch процессы не умирают, а просто выводят ошибку, возникшую при сборке, в консоль. По крайней мере у меня так с вышеуказанными скриптами.
              3) Что же есть настолько сложное (или несовместимое) в сборке фронтенда, что это не съест cmd? Вышеописанный (и немного более сложный, этот я немного упростил) набор скриптов вполне себе нормально отрабатывает в и виндовом cmd, и в bash
                +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'
                });
                

                Вам серьезно нравится такое читать? Ведь всегда можно заглянуть в документацию и узнать что же значат эти магические аргументы!
                  0
                  Строка 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. И что же тут «плохо читается»?
                    0
                    Ну вот я и говорю, посмотрите аналогичный код на 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.
                      0
                      Во-первых, какой такой КАЖДЫЙ раз? Таски пишутся один раз и потом, возможно, немного изменяются и дополняются, и то со временем все реже и реже. Во-вторых, я и пытаюсь донести мысль, что гадать не нужно, так как все это легко запоминается (по крайней мере не сложнее, чем названия всех необходимых модулей для сборки и код для их запуска). В-третьих, npm scripts — это и есть специальный инструмент. Да, он проще, чем модули, но тем не менее. В-четвертых, взгляните на мой первый комментарий, там ясно видно, что сервер скрипты тоже умеют запускать. Ну а сборка из памяти или уже реализована на уровне модулей, которые запускаются у меня из командной строки, или работают слишком быстро, чтобы я это заметил — точно сам не знаю. Хотя, возможно, на очень большом количестве файлов выигрыш будет заметен. Но как минимум для сборки js есть watchify, который вне зависимости от количества и сложности сборки на лету собирает только изменившийся файл, что в итоге занимает до 20 мс.
                0
                Пользуетесь plumber чтобы watch не падал? А вы в курсе, что у вас теперь процесс всегда завершается с кодом 0, то есть успешно? Это означает, что команда, к примеру, npm test && npm run deploy задеплоит даже код с ошибками.

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

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

                Одно радует, раз у вас есть общий модуль с рецептами для сборки, можно поправить один раз, и станет лучше во всех зависимых проектах
                  0
                  Спасибо за ссылку на пост. Я глушу ошибки именно в develop режиме, потому что часто бывает такой сценарий: начал писать код (название функции), переключился на браузер чтобы посмотреть документацию и IDE (в моем случае PHPStorm) сохраняет файл при потере фокуса окна, gulp watch начинает выполняться, падает. Все это происходит в фоне и потому не заметно.
                  А в продакшен билде да, вывод ошибок обязателен… Пойду проверю падает ли он в продакшене при ошибках… и с каким кодом.
                    0
                    Мое решение не глушит ошибки, а обрабатывает правильно. То есть одна сборка у вас упадет, но процесс c watch останется. Поправите ошибки, сохранитесь – watch запустит сборку, и она пройдет успешно.
              +3
              Автоматическая автоматизация автоматизации для автоматизации…
                0
                О, я тоже писал подобного монстра, только смысл в том, что он умеет компилить вообще всё подряд вперемешку — например кофе вместе с бабелом, потом js, потом опять кофе, потом всё объединяет в одмн, минифицирует, создаёт gzip и прочее. В общем выложил в gist с примером. Может кому понадобится: https://gist.github.com/SerafimArts/2101d66020c4791295aa
                  +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.
                    0
                    Да, я предполагал что мое решение выбивается из идеологии gulp (потому что подобных модулей не нашел), но оно было необходимо.
                    +3
                    У меня просто есть скелет проекта, клонирую его, а там уже все что надо есть.
                    В gulpfile всего две строки:
                    var requireDir = require('require-dir');
                    requireDir('./gulp/tasks', { recurse: true });
                    

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

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

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

                    P.S. А парни пошли дальше. Они назвали директорию gulpfile.js ну а там естественно index.js и вообще красота получается =) Надо будет в своем скелете так же сделать =)
                      0
                      Спасибо за наводку! Действительно неплохое решение, но применимое скорее для больших проектов.
                      У меня была задача сделать что-то для небольших (обычно уже существующих со своей структурой) проектов + этим должны пользоваться джуниоры/мидлы и ничего не сломать :) А то за последнее время народилось кучи разносортных gulp/grunt файлов, в каждом проекте свой формат и свои костыли %)
                        0
                        Использую практически такой же подход, только я для своих нужд написал loader (gulp-task-loader-recursive) для gulp тасков, который, к тому же, загружает их рекурсивно и проставляет каждому файлу с таском красивое имя в зависимости от имени файла и папок, где он находится. В результате gulpfile.js состоит всего из 2 строк (можно и в 1 вместить), а сами задачи лежат в отдельных файликах. Кроме того, очень легко подключается Babel, который потом можно использовать в этих task файлах. Такие отдельные файлики уже гораздо проще копировать между проектами.

                        В целом, почему мне Gulp нравится больше, чем запуск задач через npm тем, что Gulp позволяет автоматизировать достаточно сложные вещи в достаточно читаемом и понятном виде.
                        +2
                        это глоток воздуха после grunt'a <…> нужно подключать с десяток зависимостей и писать почти 50 строчек

                        Спасибо, наглотался:)
                          0
                          grunt тоже требовал подключения множества модулей и еще большим количеством кода. Про глоток воздуха — это про структуру тасков
                            0
                            Ну для сборки commonjs достаточно одного модуля.
                          0
                          Посмотрел код, вроде es6, а вроде ужас какой-то. Нафига вы столько написали, когда как раз для описанных задач есть webpack. Где в 1 строку вписывается правило для сборки less или чего вы там еще хотите.
                            0
                            es6 раньше не писал, в чем ужас?
                            webpack — альтернатива browserify, но он не умеет упаковывать less, копировать файлы и еще что-нить, что может понадобитсья. Gulp умеет много и позволяет делать что угодно.
                              –1
                              Webpack умеет собирать less тоже. Посмотрите на less-loader
                            +1
                            Хочу еще упомянуть про TARS. Уже писал на хабре про него. + у меня есть CLI-утилита для него.
                              0
                              Было бы здорово, если бы после less сразу применялся autoprefixer.
                              Например в аналогичном инструменте так и есть: github.com/ng-tools/factory-angular-channels/blob/master/lib/channels/styles/base.js#L13
                                0
                                У Laravel есть подобный подпроект, называется Elixir.
                                При небольшом желании, можно использовать отдельно.

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

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

                                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                Самое читаемое