[Из песочницы] Продвинутый Gulp и Browserify: интересные трюки
Пару недель назад я начал цикл о том, как делал некоммерческий музыкальный проект (первый пост есть в «я пиарюсь», не буду ставить ссылок), но, к сожалению, в первой же статье увлекся, и вместо того, чтобы рассказывать о том, как делал конкретно его, начал вспоминать эффективные трюки из других проектов. Видимо, именно это вкупе с прописанным акцентом на сам проект привело к тому, что за мной и постом прилетело НЛО.Однако все то, что было в статье, было по крайней мере малоизвестно, а половина ее была вообще уникальна, как я уверен, и каждый из этих советов может ощутимо облегчить работу с gulp, поэтому мне действительно было бы жаль, если этот материал безвозвратно пропал бы.
Поэтому я постарался убрать все упоминания проекта и повторно публикую (с доработками и правками) статью, которую по сути никто еще не видел. Если вы фанат grunt — почитайте хотя бы вторую часть: то, что вы не любите gulp, не значит, что вы не любите browserify.
Краткое содержание:
Простой способ обработки ошибок Универсальная структура для хранения исходных файлов Объединение нескольких потоков (например, скомпилированный coffee и js) в один Создание потока из текста создание собственных плагинов для Browserify создание плагинов из плагинов 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-таск сможет повторно запустить вашу сборку стилей, когда вы их обновите.
Выглядит это так:
Папки и файлы Если у вас все аккуратно разложено по папочкам типа: 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-ов и алгоритмы, которые умеют выдавать точные рекомендации по музыкальным предпочтениям пользователя на основании крайне малого количества данных.