Broccoli является новой системой автоматической сборки. Её вполне можно сравнить с Rails asset pipeline, однако есть и некоторые различия: он запускается на Node.JS и не зависит от серверной части приложения.
После длинной вереницы 0.0.х альфа релизов, я только что выпустил первую бета версию, Broccoli 0.1.0.
Оглавление:
- Быстрый пример
- Мотивация / Особенности
- Архитектура
- За кулисами / Общий взгляд
- Сравнение с другими системами сборки
- Что дальше?
1. Быстрый пример
Ниже представлен пример конфигурационного файла для билда(Brocfile.js). Комментарии намеренно опущены, пример приведен только для того, чтобы проиллюстрировать синтаксис:
module.exports = function (broccoli) {
var filterCoffeeScript = require('broccoli-coffee');
var compileES6 = require('broccoli-es6-concatenator');
var sourceTree = broccoli.makeTree('lib');
sourceTree = filterCoffeeScript(sourceTree);
var appJs = compileES6(sourceTree, {
...
outputFile: '/assets/app.js'
});
var publicFiles = broccoli.makeTree('public');
return [appJs, publicFiles];
};
Запустите
broccoli serve
, чтобы начать отслеживать изменения исходных файлов. При каждом изменении любого файла из списка отслеживаемых, broccoli автоматически пересоберет его в целевой директории. Broccoli оптимизирован выполнять serve максимально быстро, поэтому вы не должны испытывать паузы между очередными сборками.
Запустите broccoli build dist
, чтобы выполнить единовременную сборку и положить результат в папку dist.
Для более подробного примера, взгляните на broccoli-sample-app
2. Мотивация / Особенности
2.1. Быстрый ребилд
Главной задачей при проектировании Broccoli, была реализация быстрых инкрементальных сборок. И вот почему:
Например, вы используете Grunt для сборки приложения, написанного на CoffeeScript, SASS, и еще нескольких препроцессорах. Когда вы что-то разрабатываете, вы хотите редактировать файлы и сразу же видеть результат в браузере, без постоянных запусков билд системы. Так вот, для этой цели вы используете grunt watch
, но по мере увеличения вашего приложения, сборка происходит все медленней и медленней. После нескольких месяцев работы над проектом, ваш цикл «отредактировал-обновил» превращается в «отредактировал-подождал-10-секунд-обновил».
Соответственно, чтобы ускорить вашу сборку, вы пытаетесь пересобирать только те файлы, которые были изменены. Это довольно тяжело, т.к. случается, что один файл на выходе зависит от нескольких файлов на входе. Вам приходится вручную настраивать правила, чтобы пересобирать правильные файлы, исходя из измененных и их зависимостей. Но Grunt не спроектирован таким образом, чтобы легко справляться с данной задачей, и поэтому, даже написав свои наборы правил, вы не сможете быть уверены, что пересобираться будут именно необходимые файлы. Иногда, он будет пересобирать файлы, когда в этом нет необходимости(и, тем самым, замедлять сборку), но что еще хуже, иногда он не будет пересобирать файлы, когда он должен это сделать(что делает вашу сборку ненадежной).
С помощью Broccoli, вы можете просто запустить broccoli serve
, и он сам поймет, какие файлы необходимо отслеживать, и будет пересобирать только те, которые в этом нуждаются.
В результате, это означает, что ребилд, как правило, должен иметь O(1) постоянное время выполнения, вне зависимости оттого, какое количество файлов используется в проекте, т.к. собирается всегда только один. Я стремлюсь к результату в 200ms на каждую сборку с типичным набором задач, а т.к. такое время задержки кажется почти мгновенным для человеческого мозга, то для меня приемлемы результаты до половины секунды.
2.2. Цепочки плагинов
Другая важная задача — возможность компановки вызова плагинов. Давате рассмотрим пример, чтобы показать как просто, используя Broccoli, вы можете компилировать CoffeeScript с последующей минифакцией:
var tree = broccoli.makeTree('lib')
tree = compileCoffeeScript(tree)
tree = uglifyJS(tree)
return tree
Используя Grunt, вам бы пришлось создавать временную директорию(помимо итоговой), чтобы сохранить туда вывод CoffeeScript. В результате всех этих манипуляций, Gruntfile обычно разрастается до довольно больших размеров. С помощью Broccoli, все подобные действия разруливаются автоматически.
3. Архитектура
Для особо любопытных, позвольте мне рассказать об архитектуре Broccoli.
3.1. Деревья, а не файлы
В качестве уровня абстракции для описания исходных и выходных данных используются не файлы, а деревья — директории с файлами и поддерикториями. Получается, что мы имеем не «файл-на-вход-файл-на-выход», а «дерево-на-вход-дерево-на-выход».
Если бы Broccoli работал с отдельными файлами, мы бы по-прежнему смогли компилировать CoffeeScript без проблем(т.к. файлы компилируются в соответствии 1 к 1), однако это вызвало бы проблемы при взаимодействии API с такими препроцессорами, как SASS(который позволяет использовать @import
, что позволяет компилировать n файлов в 1).
Однако Broccoli спроектирован таким образом, что решение задач для препроцессоров типа SASS(n:1) не вызывает проблем, а задачи для препроцессоров типа CoffeeScript(1:1) легко решаются как частный случай n:1. А вообще, для таких(1:1) преобразований, у нас имеется класс Filter
, который позволяет максимально просто использовать их в своих решениях.
3.2. Плагины просто возвращают новые деревья
Сперва, я спроектировал Broccoli с двумя примитивами:
tree
(далее «дерево»), которое представляют директории с файлами и
transform
(далее «преобразование»), которое берет на входе дерево и возвращает на выходе новое, скомпилированное дерево, после преобразований.
Это подразумевает, что мы преобразовываем деревья 1:1. Удивительно, но это не всегда является хорошей абстракцией. Например, в SASS есть «пути загрузки», которые используются для поиска файлов при использовании директивы @import
. По схожему принципу работают конкатенаторы вроде r.js: у него существует опция «paths», которая отвечает за поиск импортируемых модулей. Лучший способ представления таких путей — сет(структура данных), состоящий из деревьев.
Как вы можете заметить, в реальном мире многие компиляторы/препроцессоры собирают n «деревьев» в одно. Простейший способ придерживаться подобного подхода — позволять плагинам разбираться со своими входными «деревьями» самим, тем самым позволяя им принимать 0, 1 или n «деревьев» на вход.
Но теперь, когда мы решили, что плагины сами будут обрабатывать свои входные деревья, нам больше не нужно знать о компиляторах как об объектах первого порядка. Плагины просто экспортируют функцию, которая принимает от нуля до n исходных деревьев(и, возможно, какие-то настройки), и возвращает объект, представляющий новое дерево. Например:
broccoli.makeTree('lib') // => a tree
compileCoffeeScript(tree) // => a tree
compileSass(tree, {
loadPaths: [moreTrees, ...]
}) // => a tree
3.3. Файловая система и есть наше API
Помните, что из-за того, что Grunt не поддерживает использования «цепочек» плагинов, нам приходится возиться с временными директориями для промежуточных результатов сборок, что делает наши конфиги слишком большими и тяжелоподдерживаемыми.
Чтобы избежать этого, первое, что приходит на ум — вынести представление файловой системы в память, где наши деревья будут представлены в виде коллекции потоков. Gulp так и делает. Я тоже пытался реализовать такой подход в ранних версиях Broccoli, но это обернулось тем, что код стал достаточно сложным: с потоками, плагины должны были следить за очередями и взаимными блокировками. Также, к слову о потоках и путях: нам могут потребоваться атрибуты вроде времени последнего изменения или размера файла. Или, например, если нам будет необходима возможность считать файл заново или что-либо найти, отобразить файл в памяти, или, в конце концов, если нам потребуется передать входное дерево другому процессу через терминал — тут наше API для работы с потоками не сможет нам помочь, и нам придется сначала записать всё дерево в файловую систему. Слишком сложно!
Но подождите, если мы собираемся копировать каждую особенность файловой системы, а иногда и выдергивать из памяти наши деревья и перегонять их в физическое представление, после чего опять класть их в память, и опять… Почему бы просто не использовать файловую систему вместо потоков?
Модуль fs
Node.JS уже предоставляет необходимый нам API к файловой системе — все, чего мы можем только пожелать.
Единственное неудобство заключается в том, что нам придется работать с временными директориями «за сценой», а потом надо будет прибраться. Но, на самом деле, это не так сложно, как кажется.
Люди иногда беспокоятся, что запись на диск происходит медленнее, чем в память. Но даже если вы возьмете рельный жесткий диск, то пропускная способность современных SSD становится настолько высокой, что ее можно сравнить со скоростью работы CPU, а это означает, что мы получаем лишь незначительные накладные расходы.
3.4. Кеширование вместо частичного ребилда
Когда я пробовал решить проблему инкрементальных сборок, я пытался разработать способ проверить, какие файлы нужно пересобирать, чтобы позволить Broccoli вызывать это событие только для подмножества исходных файлов. При инкрементальной сборке нам необходимо знать, от каких исходных файлов зависит результат, ведь зачастую мы сталкиваемся с отношениями n:1. «Частичная сборка» — это классический подход Make, так же как и Rails asset pipeline, Rake::Pipeline или Brunch, но лично я для себя решил, что это ненужные трудности.
Подход Broccoli намного проще: мы просим все плагины кешировать выходные данные. Когда мы пересобираем проект полностью или когда мы перезапускаем отдельные плагины — большая часть информации будет браться из кеша самих плагинов, что будет занимать мизерное время.
Сначала Broccoli начал предоставлять некоторые кеширующие примитивы, но позже было решено исключить их из API ядра. Теперь мы просто ограничиваемся тем, что предоставляем архитектуру, которая не мешает реализации кеширования.
Для плагинов, которые мапятся 1:1, таких, как CoffeeScript, мы можем использовать общий кеширующий механизм(представленный в пакете broccoli-filter), оставляя код плагина очень простым. Плагины, которые собираются n:1, такие, как SASS, требуют более тщательной заботы о кешировании, поэтому для них требуется реализовывать особенную логику для работы с кешем. Я полагаю, что в будущем мы все же сможем выделить какую-то общую часть логики кеширования.
3.5. «Нет» параллельности
Если мы все страдаем от медленного выполнения сборок, может стоит попробовать выполнять задачи параллельно?
Мой ответ — «нет»: параллельный запуск задач делает возможным возникновение проблем с очередностью внутри плагинов, которые вы можете не заметить вплоть до деплоя. Это самые худшие из багов, поэтому уходя от параллельного запуска, мы ограждаем себя от целой категории багов.
С другой стороны, закон Амадаля останавливает нас от выигрыша большой производительности, при использовании параллельных запусков. Давайте приведем простой пример: наша сборка занмиает 16 секунд. Представим, что 50% мы можем запускать параллельно, а оставшаяся часть должна запускаться в порядке очереди(а-ля coffee->concate->uglify). Если мы запустим такую сборку на четырехядерной машине, то сборка будет занимать 10 секнуд: 8 секунд на синхронную часть, и 8 / 4 = 2 секунды на параллельную часть. В результате время сборки все равно целых 10 секнуд, а это всего лишь +40% производительности.
Для инкрементальных сборок, которые интересуют нас более всего, кеширование сводит к минимуму большинство преимуществ параллельного запуска задач, поэтому мы почти не проигрываем в производительности.
Именно поэтому я считаю, что реализация паралеллизации сборки не стоит своих преимуществ. В принципе, вам ничего не запрещает написать плагин для Broccoli, который бы предоставлял возможность парллелизировать некоторые задачи вашего процесса сборки. Однако, примитивы Broccoli также спроектированы таким образом, чтобы позволить максимально удобно проектировать плагины, которые будут запускаться в детерменированной последовательности.
4. За кулисами / Общий взгляд
В основе моего решения написать хорошую систему сборки лежат две причины.
Первая причина в улучшенной производительности, которая достигается инкрементальными сборками.
Вообще, я верю, что продуктивность разработчика определяется качеством библиотек и инструментов, которыми он пользуется. Цикл «отредактировал, перезагрузил страницу» мы повторяем изо дня в день по несколько сотен раз, и это, возможно, основной способ получения обратной связи. И если мы хотим улучшить производительность наших инструментов, надо сделать этот цикл настолько быстрым, насколько это возможно.
Вторая причина кроется в поддержке экосистемы front-end пакетов.
Я верю, что Bower и модульная система ES6 помогут нам построить прекрасную экосистему, но Bower сам по себе бесполезен, пока вы не надстроите над ним систему сборки. Вот почему Bower является абсолютно ни к чему не привязанным инструментом, позволяющим загружать все зависимости вашего проекта(рекурсивно, вместе с их зависимостями) в файловую систему — это все, что можно сделать с помощью него. Broccoli же нацелен стать именно тем недостающим звеном — надстройкой в виде системы сборки, под которой тот будет работать.
Кстати говоря, Broccoli сам по себе никак не связан с Bower или модулями ES6 — вы можете использовать его с тем, с чем захотите. (я знаю, что есть и другие связки, например npm + browserify, или npm + r.js.) Я затрону их в одном из следующих своих постов.
5. Сравнение с другими системами сборки
Если я вас почти убедил, но вам все еще интересно, как другие системы сборки ведут себя в сравнении с Broccoli, то позвольте мне объяснить, почему я написал Broccoli вместо того, чтобы использовать одну из нижеперечисленных:
Grunt — это инструмент для запуска задач, и он никогда не позиционировался, как система для сборки. Если вы попробуете использовать его как систему сборки, вы быстро разочаруетесь, т.к. он не предоставляет возможности использовать цепные вызовы(композицию), и вам быстро надоест разбираться со временными директориями, а тем временем файл конфигурации будет все разрастаться и разрастаться. Так же он не может обеспечить надежные инкриментальные сборки, поэтому ваши повторные сборки будут выполняться медленно и/или будут ненадежны, см. «Быстрый ребилд» выше.
Grunt создан как инструмент для запуска задач, который позволяет получить функциональность shell-скриптов на любой платформе, например скаффолдинг или деплой вашего приложения. В будущем, Broccoli будет доступен как плагин для Grunt, так что вы сможете вызывать его прямо из своего Gruntfile.
Gulp пытается решить проблему с последовательным вызовом плагинов, но, как по мне, он страдает определенными ошибками в архитектуре: несмотря на то, что вся его работа крутится вокруг «деревьев», они реализованы последовательностью файлов. Это отлично работает, когда один исходный файл преобразоввывается в один конечный файл. Но когда плагину требуется следовать import
директивам, а это требует обращение к файлам вне очереди, работа усложняется. Сейчас, плагины, которые используют import
директивы, приостанавливают выполнение сборки и считывают необходимые файлы прямо из файловой системы. В будущем, я слышал, будут использованы библиотеки, которые позволят запускать все потоки на виртуальной файловой системе и передавать их компилятору. Я считаю, что все эти усложнения — симптом полного несоответствия между системой сборки и компилятором. Вы можете еще раз прочесть раздел «Деревья, а не файлы», там я более подробно останавливался на этом вопросе. Я так же совсем не уверен, что абстрагируясь от файлов к потокам или буферу, мы получим более удобное API; см. «Файловая система и есть наше API».
Brunch, как и Gulp, использует API, основанное на файлах(а не на деревьях). Так же как и в Gulp, плагины, в конце концов, идут в обход системы сборки, когда им необходимо получить более одного файла. Brunch так же пытается выполнять частичный ребилд вместо кеширования, см. «Кеширование вместо частичного ребилда» выше.
Rake::Pipeline написан на Ruby, который менее вездесущ, чем Node в мире front-end разработки, и он так же пытается выполнять частичные сборки. Иегуда Кац сказал, что система не очень активно поддерживается, и он ставит на Broccoli.
Rails asset pipeline так же использует частичные сборки, и, более того, использует весьма различные подходы для разработки и продакшена, что может привести к весьма неожиданным ошибкам в момент деплоя. Но, что важнее, он требует ROR на бэкэнде.
6. Что дальше
Список плагинов все еще невелик. Но если этого достаточно для ваших задач, я бы настоятельно рекомендовал дать Broccoli шанс:
http://ift.tt/1cShDFTЯ бы хотел увидеть, как другие люди вовлекаются в разработку плагинов. «Заворачивать» компиляторы достаточно просто, но самое важно и тяжелое — это добиться правильного кеширования и минимальных потерь в скорости. Мы хотим выделить больше шаблонов кеширования, чтобы сократить повторяющийся код в плагинах.
В ближайшие пару недель в мои планы входит улучшение документации и вычищение кода ядра Broccoli и плагинов. Мы так же хотим добавить тесты для ядра Broccoli и предоставить элегантное решение интеграционных тестов для плагинов. Так же, в наших существующих плагинах отсутствует поддержка source map'ов. Это весьма накладно с точки зрения производительности, т.к. плагинам, при последовательном вызове, приходится брать Source Maps предыдущего плагина и правильно их интерполировать, но я пока еще не нашел времени заняться этим.
Скоро вы сможете увидеть активное использование Broccoli в экосистеме фреймворка Ember, который будет обеспечивать дефолтный стек ember-cli(скоро появится, по функциональности похожа на rails command line). Мы так же надеемся заменить Rake::Pipeline и Grunt при процессе сборки ядра Ember.
Я бы так же очень хотел увидеть Broccoli адаптированным под проекты вне Ember-сообщества. JS MVC приложения, написанные с помощью таких фреймворков, как Angular или Backbone, различные JS и CSS библиотеки, требующие сборок — главные кандидаты, чтобы быть собранными с помощью Broccoli. Используя Broccoli для реальных сценариев сборки, мы должны удостовериться в надежности его API, и я надеюсь, что в ближайшие несколько месяцев мы сможем выпустить первую стабильную(1.0.0) версию.
Этот пост является первым постом, детально рассматривающим архитектуру Broccoli, так что справочной информации/документации все еще мало. Я буду рад помочь вам начать работу, и исправить любые баги, с которыми вы столкнетесь. Вы можете найти меня на #broccolijs на Freenode, или написав мне на почту/позвонив в Google Talk: joliss42@gmail.com. Я так же буду рад ответить на любые интересующие вас вопросы в соответствующем разделе на Github.
Особые благодарности Jonas Nicklas, Josef Brandl, Paul Miller, Erik Bryn, Yehuda Katz, Jeff Felchner, Chris Willard, Joe Fiorini, Luke Melia, Andrew Davey, and Alex Matchneer за чтение и критику черновиков этой статьи.
От переводчика
Я бы хотел поблагодарить
z6Dabrata и Марию Гилёву за помощь в корректировании этого перевода.
This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.