Продвинутый Gulp и Browserify: интересные трюки

Пару недель назад я начал цикл о том, как делал некоммерческий музыкальный проект (первый пост есть в «я пиарюсь», не буду ставить ссылок), но, к сожалению, в первой же статье увлекся, и вместо того, чтобы рассказывать о том, как делал конкретно его, начал вспоминать эффективные трюки из других проектов. Видимо, именно это вкупе с прописанным акцентом на сам проект привело к тому, что за мной и постом прилетело НЛО.

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

Поэтому я постарался убрать все упоминания проекта и повторно публикую (с доработками и правками) статью, которую по сути никто еще не видел. Если вы фанат grunt — почитайте хотя бы вторую часть: то, что вы не любите gulp, не значит, что вы не любите browserify.

Краткое содержание:
  1. Простой способ обработки ошибок;
  2. Универсальная структура для хранения исходных файлов;
  3. Объединение нескольких потоков (например, скомпилированный coffee и js) в один;
  4. Создание потока из текста;
  5. создание собственных плагинов для Browserify;
  6. создание плагинов из плагинов Gulp для Browserify.




Я перешел на gulp во время работы над одним видеопорталом. Сейчас у них все хорошо, они развиваются, и, кажется, моя система сборки стоит у них до сих пор.
Тогда же я перешел на stylus.

Почему? Исключительно быстродействие.
Многие цифры я привожу по памяти, так что могу немного врать, но пропорции — одинаковые, их я точно помню.

Вводные:
  • grunt;
  • есть большое количество (73, если не путаю) файлов стилей, которые почти не связаны друг с другом, сборка — склейкой. В основном стили — это компоненты и постраничные хуки и лэйауты, так что они независимы друг от друга, в почти каждом подключается только набор переменных;
  • есть большое количество скриптов, которые склеиваются в один. CoffeeScript компилируется, обычный js не изменяется. Каждый файл оборачивается в замыкание;
  • есть файл приложения, который собирается из большого количества прекомпилированных шаблонов jade через какой-то специализированный плагин, который делал в глобальном пространстве объект со всеми шаблонами из папки, плюс склеивалось с основным движком приложения. По сути на выходе тоже js-файл;
  • время сборки каждого из пунктов — около 10 секунд на Macbook Air 2013, i5, суммарно — около 30-40 секунд. Напоминаю, что в MBA стоит SSD, котроый подключен через PCIe и выдает скорость работы с жестким диском, которая вроде как в принципе недостижима на обычном sata-подключении. На компьютерах с HDD время сборки может занимать больше минуты.


Все указывает на то, что идет затык в работе с жестким диском, причем в только одной задаче из трех все можно решить каким-то кэшем.

Изучение проблемы так же указывало на то, что стили компилируются дольше всех.

Решение проблемы было следующим:

  • заменить grunt на gulp. Решается проблема с компиляцией, а потом склейкой стилей и скриптов — убирается шаг записи на диск каждого отдельного файла;
  • заменить sass на stylus, перевести все на инклуды в глобальный файл. Компиляция стилей ускоряется до менее чем секунды. Видимо, передача каждого файла из ноды в руби съедала очень много ресурсов. Да и ruby-sass не очень быстрая все же штука. Перенос, кстати, произошел вообще без проблем — sass использовался базовый, без миксинов и функций, а с определенной точки зрения формат sass можно назвать подмножеством формата stylus;
  • Перевести весь coffeescript на JS для ускорения компиляции — благо, это были в основном старые виджеты;
  • Перевести сборку js для приложения на browserify для кэширования.


В итоге сборка всего проекта начала занимать 3-4 секунды, отдельных типов файлов — около секунды.
watchdog отрабатывал вообще моментально, понятное дело.

С тех пор я и пользую стек gulp+browserify.

Из плюсов браузерифая — оборачивание виджетов в замыкание, плюс по сути простейшая валидация кода — он не пропустит код, который не сможет распарсить.

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

Обработка ошибок


Сначала решением было что-то вроде:

gulp.task('build-html', function () {
    ...
        .pipe(plugins.jade()).on('error', console.log.bind(console))
    ...


Но, как оказалось, это не очень хорошо: стрим «подвисает» и повторно не запускается по обновлению.
Почему это происходит? Дело в том, что таск не завершается, а остается висеть в таком состоянии, а в gulp, похоже, повторный запуск невозможен без окончания предыдущего выполнения.

Я долго искал хорошее решение для этого вопроса, но со временем написал свою функцию для решения этой задачи:

function log(error) {
    console.log([
        '',
        "----------ERROR MESSAGE START----------".bold.red.underline,
        ("[" + error.name + " in " + error.plugin + "]").red.bold.inverse,
        error.message,
        "----------ERROR MESSAGE END----------".bold.red.underline,
        ''
    ].join('\n'));
    this.end();
}

gulp.task('build-html', function () {
    ...
        .pipe(plugins.jade()).on('error', log)
    ...


Она еще и закрывает стрим (this.end()), вызывая завершение таска.

По желанию сюда можно добавить, например, оповещения growl, но мне лично и так хватает.

Функция требует предустановленного npm-пакета color и дает весьма красивый вывод. Если не хочется ставить лишние пакеты — можно убрать методы у цветов.

Самое главное тут — в последней строке.

Когда мы выполняем this.end(), конкретный таск gulp завершает свою работу. Да, это немножко гадит в память, но зато watchdog-таск сможет повторно запустить вашу сборку стилей, когда вы их обновите.

Выглядит это так:

image

Папки и файлы


Если у вас все аккуратно разложено по папочкам типа:
  • assets
  • styles
  • scripts
  • templates


То я вас поздравляю.

Но у меня лично все валяется примерно так:



И это удобно, куда удобнее чем было раньше. Почему? Да потому что у меня появилась возможность как угодно логически структурировать инклудящиеся друг в друга файлы, не изменяя ничего в сборщике.
Пишешь @require в стилях, лэйауты и инклуды в шаблонах и browserify для скриптов, все просто работает.

В итоге собирается это все в index.html, app.js и style.css — та самая база для любого проекта.

Как я это получил?

Во всех проектах я стараюсь держаться подобной схемы:

gulp.task('build-js', function () {
    return gulp.src('src/**/[^_]*.js')
        ...


gulp.task('build-html', function () {
    return gulp.src('src/**/[^_]*.jade')
        ...

gulp.task('build-css', function () {
    return gulp.src('src/**/[^_]*.styl')
    	...


Что это за glob-путь такой?
Это выборка всех файлов, которые не начинаются с подчеркивания. На любой глубине. Соответственно, если вы называете файл src/lib/_some_lib.js, он не будет скомпилирован самостоятельно. А вот require его с удовольствием подцепит.

Склеивание результатов разных тасков


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

Но это очень интересно, и в свое время я не нашел этого нигде.

Когда мне потребовалось решить задачу типа «склеить все CoffeeScript файлы и js-файлы из папки vendor, а потом из основной», сначала я огорчился, потому что не знал что делать. Почему такая последовательность — думаю, понятно — вендорные скрипты надо загружать первыми, а если это сделать как-то еще, все перемешается.

Но я знал, что если что-то есть в памяти, то это можно использовать, и начал копать. Все же в gulp используются родные стримы nodejs, а значит, с этим можно что-то сделать.

Пришел к самодельному решению:

var es = require('event-stream');

gulp.task('build', function(){
    return es.concat(
        gulp.src('scripts/vendor/*.coffee').pipe(coffee()),
        gulp.src('scripts/vendor/*.js'),
        gulp.src('scripts/*.coffee').pipe(coffee()),
        gulp.src('scripts/*.js')
    )
    .pipe(concat())
    .pipe(dest(...));
})


Обратите внимание: судя по новой документации event-stream, метод concat переименовали в merge. Я делал это последний раз полгода назад, поэтому сейчас у метода могут быть новые тонкости использования — код взят из реального относительно старого проекта, работающего со уже старой версией EventStream.

Подключение плагинов


Когда у вас 10-20 плагинов, становится несколько утомительно прописывать их вручную.

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

var gulp = require('gulp'),
    plugins = {};
Object.keys(require('./package.json')['devDependencies'])
    .filter(function (pkg) { return pkg.indexOf('gulp-') === 0; })
    .forEach(function (pkg) {
        plugins[pkg.replace('gulp-', '').replace(/-/g, '_')] = require(pkg);
    });


Если кто-то не понял, что именно делает этот код — он открывает содержимое devDependencies в package.json и все элементы, которые начинаются в нем с gulp- — подключает как plugins[pluginName]. Если плагин называется как-то типа gulp-css-base64, он будет доступен по plugins.css_base64.

Как создать поток из текста


Иногда бывает нужно создать что-то в памяти и отправить в поток (да хоть той же склейкой). Опять же, для этого есть плагин, но зачем? Если можно все написать самому в три строки.

var gutil = require('gulp-util');

function string_from_src(filename, string) {
    var src = require('stream').Readable({objectMode: true});
    src._read = function () {
        this.push(new gutil.File({cwd: "", base: "", path: filename, contents: new Buffer(string)}));
        this.push(null);
    };
    return src;
}


Работает это все поверх Vynil FS из gulp-util, но какая нам разница?

Плагины для browserify


Почему browserify в посте про gulp? Да потому что его можно назвать мета-системой сборки, которая используется в других системах. Его возможности уже давным-давно вышли за пределы простой склейки js-модулей, а в следующей части поста все вообще сойдется воедино.

Если вы пользуетесь browserify и commonJS модулями — скажите честно, вам хотелось когда-нибудь писать вот так?

var vm = new Vue({
    template: require('./templates/_app.html.jade'),
...


Это реальный код из того самого проекта, за пост о котором за мной прилетело НЛО, кстати.

Как оказалось, клепать свои плагины для browserify — элементарно.

Реальный таск для сборки JS в итоге выглядит так:

gulp.task('build-js', function () {
    return gulp.src('src/**/[^_]*.js')
        .pipe(plugins.browserify(
            {
                transform: [require('./lib/html-jadeify'), 'es6ify'],
                debug    : true
            }
        )).on("error", log)
        .pipe(gulp.dest("build"));
});


Что это за… и как он работает? Да очень просто.

Простейшая обертка выглядит как-то так:

var through = require('through'),
    jade = require('jade');

function Jadify(file) {
    var data = '';
    if (/\.html\.jade$/.test(file) === false) 
        return through();
    else 
        return through(write, end);

    function write(buf) { data += buf; }

    function end() {
        compile(file, data, function (error, result) {
            if (error) stream.emit('error', error);
            else stream.queue(result);
            stream.queue(null);
        });
    }
}
function compile(file, data, callback) {
    callback(null,
        'module.exports = "' + jade.render(data, {filename: file})+ '"\n';
    );
}

Jadify.compile = compile;
Jadify.sourceMap = true; // use source maps by default

module.exports = Jadify;


В дальнейшем я буду цитировать только функцию compile — для экономии места.

Если тут есть browserify-ниндзя, которые уже знают все плагины наязусть, они спросят «ну и чо?».
Да ничо.
В таком виде плагины уже существуют.

Но фишка в том, что мы можем изменять синтаксис.
Например:

callback(null,
            'module.exports = "' + jade.render(data, {filename: file})
        .replace(/"/mg, '\\"')
        .replace(/\n/mg, '\\n')
        .replace(/@inject '([^']*)'/mg, '"+require("$1")+"')
        + '"\n'
    );


И теперь в jade-шаблоне мы можем написать

style @inject './_font_styles.styl'


В итоге мы можем инклудить шаблоны на jade в js, а стили в шаблоны на jade.

Мы можем подключать несколько сборщиков разом, например:

callback(null, 'module.exports = ' + dot.template(jade.render(data, {
        filename: file
    })) + '\n');


Это мы делаем JS-функцию шаблона на DoT (handlebars-подобный шаблонизатор поверх HTML), обернутый в Jade.

А можем даже…
… барабанная дробь…
… использовать плагины gulp для создания плагинов browserify, которые мы сможем подключить в качестве таска gulp


и, наконец, развязка всего поста. Мы можем превращать эту строку data в поток (о чем я как раз рассказывал в середине поста), который можно использовать с gulp. Мы берем функцию, которую я показывал выше, и получаем…

function string_src(filename, string) {
    var src = require('stream').Readable({ objectMode: true });
    src._read = function () {
        this.push(new gutil.File({ cwd: "", base: "", path: filename, contents: new Buffer(string) }));
        this.push(null)
    };
    return src;
}

function compile(path, data, cb) {
    string_src(path, data)
        .pipe(gulp_stylus())
        .pipe(gulp_css_base64({maxWeightResource: 32 * 1024}))
        .pipe(gulp_autoprefixer())
        .pipe(gulp_cssmin())
        .on('data',  function(file){
            cb(null, "module.exports = \""+
                file.contents.toString()
                .replace(/"/mg, '\\"')
                .replace(/\n/mg, '\\n')
                + '"');
        })
        .on('error', cb);
}


Еще раз, очень внимательно:

    string_src(path, data)
        .pipe(gulp_stylus())
        .pipe(gulp_css_base64({maxWeightResource: 32 * 1024}))
        .pipe(gulp_autoprefixer())
        .pipe(gulp_cssmin())
        .on('data',  function(file){
            cb(null, "module.exports = \""+
                file.contents.toString()
                .replace(/"/mg, '\\"')
                .replace(/\n/mg, '\\n')
                + '"');
        })


Мы только что пропустили через кучу плагинов gulp данные, которые ушли в browserify.

Да, выходит немного гемморойно. Но результат того стоит.

Зачем? Во славу сатане, конечно Потому что нельзя было просто взять и настроить в browserify сборку Stylus-стилей, которые бы еще и высасывали base64-картинки, и проходили бы через автопрефиксер и минификацию.

Заключение


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

Я надеюсь, вы узнали что-то новое. Точнее я уверен в этом, но хотелось красиво сказать.

А я надеюсь, что НЛО вернет меня на Хабр и даст рассказать про нейронные сети внутри Web Worker-ов и алгоритмы, которые умеют выдавать точные рекомендации по музыкальным предпочтениям пользователя на основании крайне малого количества данных.

Similar posts

Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 15

  • UFO just landed and posted this here
      0
      Да, постараюсь. Как только обновлю проект на новую версию с рекомендациями (оно пока убрано в дев-ветку по ряду причин), сразу же опубликую.

      Только это не nodejs приложение, все на клиентсайде :) В Web Workers. Пересчет сети на ~600 тренировочных данных и выдача около 100 значений занимает около 0.15 секунды, что вызывает просадку по перформансу. С воркерами — все нормально работает.
      На всякий случай: я использовал готовую библиотеку, но там были достаточно интересные ньюансы, связанные именно с обучением.

      Постараюсь в течении недели это все сделать.
      0
      дежавю
        0
        Я уже писал в шапке — прилетело НЛО, поэтому я почистил ссылки на проект и повторно опубликовал. Там было около 200 просмотров всего и НЛО прилетело через 15 минут после публикации, так что могли видеть, но вряд ли.
          0
          Я не просто видел, но и полностью прочел :)
        +2
        Обработку ошибок без подвисания стрима можно сделать через gulp-plumber. Его офигенный плюс, что он работает на уровне pipe, следовательно нет необходимости в каждом плагине делать .on
        gulp.src('*.js')
          .pipe(gulp_plumber())
          .pipe(something_what_can_throw_error()) 
          .pipe(gulp.dest('build'));
        

        Насчет замены sass на stylus ради ускорения компиляции — gulp-sass использует для компиляции node-реализацию процессора, libsass. Отрабатывает практически моментально даже на здоровенных файлах.
        К нему можно добавить compass с помощью bower-пакета compass-mixins, специально созданного для замены ruby-compass в libsass.
        Жаль, что не была затронута тема инкрементальных билдов.
          0
          Про plumber — спасибо, гляну, я в свое время видел его, но понял, что он просто решает какую-то специфическую проблему под windows, поэтому его все пихают «на всякий случай».

          libsass быстрый, но поддерживает, что пародоксально, не sass, но только scss-синтаксис (во всяком случае, так было полгода назад, насколько я помню). А у нас в той команде почему-то так завелось, что форматирование не отступами доставляло всегда кучу проблем при мердже — потому и был переход с хтмл на jade, та же самая ерунда была. Форматирование отступами позволило сливать ветки гита с куда меньшей кучей геммороя.

          Про инкрементальные билды — в моем случае все решалось установкой кэша для browserify в последнее время — у меня в том числе стили подключаются через JS по ряду причин.
            0
            Используете ли gulp-watch? С ним часто происходят ошибки при создании/удалении каталогов, и gulp-plumber не подавляет их. Нет ли решения такой проблемы?
            github.com/gulpjs/gulp/issues/660 github.com/floatdrop/gulp-watch/issues/83
              0
              Да, gulp-watch использую очень активно, но ни разу не встречался с вышеупомянутыми ошибками. Посмотрел issues — может быть это потому, что у меня OS X.
                0
                Уже решено: github.com/floatdrop/gulp-batch
              • UFO just landed and posted this here
                +1
                эм, жесть кое-где
                Перевести весь coffeescript на JS для ускорения компиляции — благо, это были в основном старые виджеты;

                я хоть и использую сейчас grunt, но почему, почему не использовать хотя бы gulp-changed?

                инкрементальный pipeline гораздо эффективнее. и не приходится отказываться от нормального Coffee или Type.
                  0
                  Там действительно был старый код, который уже не модифицировался — разные либы и так далее :) С инкрементальным билдом были определенные проблемы из-за особенностей сборки, там довольно были специфичные детали типа shared-кода и кода, генерируемого и хранящегося на сервере в памяти, сейчас я вижу это решаемым, тогда — это вызывало неллюзорные проблемы. И если правильно помню, один замороченный не очень популярный плагин тек, так что сборку приходилось перезапускать регулярно.
                  Плюс — как выше заметили уже, тогда в gulp-watch была проблема с удалением (или переименованием) новых папок, из-за этого оно стабильно вылетало. Компонетный подход с кучей папок был, которые периодически переименовывались.
                  В любом случае, собрать старые либы, которые уже не изменялись, в том числе для первоначальной «раскрутки» сборщика — лишним не было.
                  +1
                  Потому что нельзя было просто взять и настроить в browserify сборку Stylus-стилей, которые бы еще и высасывали base64-картинки, и проходили бы через автопрефиксер и минификацию.

                  Ах, вы ещё не слышали про webpack?
                    0
                    Хм. Посмотрел — да, действительно, есть какой-то аналог пайплайна.
                    Из явных минусов — отсутствие подсветки путей в IDE (я в webStorm обычно набираю имя файла, жамкаю cmd-enter и там есть пункт «создать файл по заданному пути и сразу открыть его»).
                    Очень вряд ли — возможность создавать свой синтаксис для инклуда одних типов файлов в другие. У меня лично последние два проекта — модули для хрома, там очень много компонентов, всунутых через shadow DOM, там нужно объявлять стили внутри шаблона, соответственно их нужно как-то подключать. Вариант, когда какой-то символ просто превращается в ..." + require($1) + "… — куда удобнее.
                    Сомневаюсь (просто не понял, честно говоря), как там подсасывать base64, так что 50/50%, может и есть оно.

                    В любом случае, это выбор каждого, но я пока что продолжу доверять gulp и browserify, которые мейнтейнятся двумя жирными такими сообществами, чем вебпаку, у которого автор явно пишет «чуваки, я это делаю в свободное время». Может, когда-нибудь потом, тем более что мои задачи вполне решаемы именно так.

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