ENB
Сборщик проектов. С помощью ENB можно собрать любой проект, который строится на модели node / target.
Возможности
- Сборка проекта и конкретных таргетов в разных режимах (с помощью ENV-переменной
YENV
). - Режим сервера как в виде самостоятельного express-сервера, так и в виде express-middleware.
Зачем нужен этот проект, если есть bem-tools?
ENB работает гораздо быстрее, чем bem-tools. Причем, как в режиме сборки, так и в режиме сервера.
Почему?
- Гибкая система кэширования.
- Обмен промежуточными данными в процессе сборки.
- Разбиение технологий на более мелкие (избавляемся от повторной работы).
- Используются более быстрые библиотеки (например, https://github.com/dfilatov/jspromise вместо Q).
- Нет порядка выполнения технологий (технологии зависят от таргетов, а не друг от друга), технологии зачастую выполняются параллельно.
- Тяжелые синхронные технологии выделяются в субпроцессы.
Некоторые отличия от bem-tools
- ENB (как платформа) свободна от идеологии BEM. Сбор префиксов не является частью платформы, а реализуется с помощью одной из технологий.
- Технологии в ENB не ограничены в том, каким образом они будут собирать те или иные таргеты.
- Все технологии настраиваемые (в большей или меньшей степени).
- ENB сложнее настроить для проекта. В нем нет готовых шаблонов make-файлов.
- В рамках ENB одна и та же технология может быть использована с разными опциями. Например, можно построить несколько разных
deps.js
в рамках одной ноды на основе различныхbemdecl.js
.
Как потестить?
Специально для этого я подготовил сборку project-stub на ENB: https://github.com/mdevils/project-stub
Пакеты для ENB
- Сборка BEMHTML: https://github.com/enb/enb-bemhtml
- Модульность для нового bem-core: https://github.com/enb/enb-modules
- Интеграция для grunt: https://github.com/megatolya/grunt-enb
Благодарности
- Дмитрию Филатову (@dfilatov). За
vow
,vow-fs
,inherit
, советы, поддержку и мотивацию. - Дмитрию Ковалю (@smith). За помощь в сборке тестов, production-режима и здоровый скептицизм.
- Александру Тармолову (@hevil). За помощь с
git
,modules
, поддержку и полезные ссылки. - Вениамину Клещенкову (@benjamin). За помощь в отладке и доработке ENB, поддержку, советы и ревью.
- Сергею Бережному (@veged). За
borschik
, советы и правильные вопросы. - Команде разработчиков bem-tools. За часть заимствованного кода.
- Егору Блинову (@escaton). За пулл-реквесты, идеи.
- Андрею Абрамову (@blond). За пулл-реквесты.
Использование из командной строки
Предполагается, что вы установили npm
-пакет enb
и находитесь в корне проекта.
Сборка всех страниц проекта:
./node_modules/.bin/enb make
Сборка всех страниц проекта со сбросом кэша:
./node_modules/.bin/enb make --no-cache
Сборка всех страниц проекта с построением графа сборки:
./node_modules/.bin/enb make --graph
Сборка всех страниц проекта с включённым профайлером (считает время работы таргетов и технологий):
./node_modules/.bin/enb make --profiler
Сборка всех страниц проекта и запись данных о сборке (результаты профайлера) в файл:
./node_modules/.bin/enb make --build-info-file="build-info.json"
Сборка конкретной страницы проекта:
./node_modules/.bin/enb make pages/index
Сборка конкретного файла:
./node_modules/.bin/enb make pages/index/index.html
Запуск в режиме сервера:
./node_modules/.bin/enb server
Отключение цветового форматирования при выводе прогресса в консоль:
NOCOLOR=1 ./node_modules/.bin/enb make
Установка лимита открытых файлов для асинхронных операций. Правильно выбранный лимит позволяет избежать EMFILE
-ошибок:
ENB_FILE_LIMIT=100 ./node_modules/.bin/enb make
Терминология
- Target (таргет) — это цель для сборки. Например, таргетом может быть
index.js
в рамках нодыpages/index
.. - Node (нода) — это папка, в которой находятся те или иные таргеты. Например,
pages/index
. - Suffix (суффикс) — это расширение исходного или конечного файла. Например,
js
. - Masked Target (замаскированный таргет) — это имя таргета, которое может содержать
?
. Знак?
заменяется на имя ноды в процессе настройки технологии, а с помощью подстроки{lang}
можно создать несколько копий технологии для каждого из языков, где{lang}
заменится на аббревиатуру языка в каждой из копий технологии. Например, таргет?.js
заменяется наsearch.js
, если нода —pages/search
. Такой подход удобен, когда мы настраиваем несколько нод черезnodeMask
. - Make-файл — файл, в котором конфигурируется ENB для сборки проекта. Находится в папке
<project_root>/.enb/make.js
. - Билдить — собирать, компилировать (используется в отношении таргетов).
Процесс сборки
- Какие таргеты необходимо билдить
ENB
узнаёт из командыenb make [target]
. Если вы запустилиenb make
без указания конкретного таргета,ENB
будет собирать все таргеты, определенные вmake.js
. ENB
инициализирует ноды, участвующие в сборке указанных таргетов. В это время каждая нода спрашивает у технологий (которые регистрировались внутри ноды) список таргетов.- Запускаются технологии, которые отвечают за те таргеты, которые необходимо билдить.
- В процессе выполнения технология может потребовать у ноды другие таргеты, необходимые для сборки (с помощью метода
requireSources
). В таком случае технология приостанавливается, нода запускает технологии, отвечающие за требуемые таргеты (если они не запущены) и после того, как технологии заканчивают сборку нужных таргетов, продолжает свою работу искомая технология. - После того, как технология выполнила свою работу по сборке таргета, она оповещает об этом ноду (с помощью метода
resolveTarget
). - Сборка завершается после того, как все необходимые таргеты собраны.
Как собрать проект - пошаговое руководство
- Прописать в
package.json
проекта зависимость от пакетаenb
(желательно в виде ">=последняя_версия"). - Выполнить
npm install
. - Проверить, что
ENB
установлен. Командаnode_modules/.bin/enb
должна выполниться без ошибок. - Создать make-файл
.bem/enb-make.js
вида:
module {};
- Проверить, что
ENB
работает. Командаnode_modules/.bin/enb make
должна выполниться без ошибок. - Теперь нужно настроить ноды. Для примера, я приведу вариант настройки ноды
pages/index
.
module { config;};
Так объявляется нода в рамках make-платформы. В данный момент она не настроена, а лишь объявлена. 7. Объявим таргеты, которые надо собрать для ноды:
module { config;};
Таргеты объявлены, но при попытке выполнить node_modules/.bin/enb make
будет ошибка, т.к. не зарегистрированы технологии, которые могут предоставить таргеты.
8. Зарегистрируем базовые технологии:
module { config;}; { return path: 'bem-bl/blocks-common' check: false path: 'bem-bl/blocks-desktop' check: false path: 'lego/blocks-common' check: false path: 'lego/blocks-desktop' check: false 'common.blocks' 'desktop.blocks' ;}
Чтобы не засорять конфиг ноды, функцию getLevels
мы выносим в нижнюю часть файла.
Рассмотрим каждую технологию:
enb/techs/levels — собирает информацию об уровнях переопределения проекта. Результат выполнения этой технологии необходим технологиям enb/techs/deps
, enb/techs/deps-old
и enb/techs/files
. Для каждой ноды по умолчанию добавляется уровень <путь_к_ноде>/blocks
. Например, для ноды pages/index
— pages/index/blocks
.
enb/techs/file-provider — сообщает make-платформе, что таргет (переданный в опции target
) уже готов. В нашем случае, исходным файлом для сборки является index.bemdecl.js
. Он лежит в репозитории и отдельная сборка для него не требуется.
enb/techs/deps-old — собирает ?.deps.js
(index.deps.js
) на основе index.bemdecl.js
и index.levels
. Почему deps-old
? В lego не хватает ряда зависимостей, поэтому ваш проект может не собраться с более быстрый технологией deps
без модификации lego. Технология deps-old
повторяет алгоритм сборки из bem-tools
и нехватка зависимостей становится незаметной, как раньше.
enb/techs/files — собирает полный список файлов со всех уровней переопределения в том порядке, в котором они идут в финальном index.deps.js
. Результат этой технологии может использоваться, например, в технологии enb/techs/js
.
- Регистрируем технологии, необходимые для сборки js и css.
module { config;}; { return path: 'bem-bl/blocks-common' check: false path: 'bem-bl/blocks-desktop' check: false path: 'lego/blocks-common' check: false path: 'lego/blocks-desktop' check: false 'common.blocks' 'desktop.blocks' ;}
Теперь файлы index.js
и index.css
могут собираться с помощью технологий enb/techs/js
и enb/techs/css
соответственно.
Но мы регистрировали иные таргеты: _?.js
(_index.js
) и _?.css
(_index.css
). Для их сборки воспользуемся технологией enb/techs/file-copy
.
module { config;}; { return path: 'bem-bl/blocks-common' check: false path: 'bem-bl/blocks-desktop' check: false path: 'lego/blocks-common' check: false path: 'lego/blocks-desktop' check: false 'common.blocks' 'desktop.blocks' ;}
Теперь можно выполнить команду node_modules/.bin/enb make
и в папке pages/index
будут столь нужные нам _index.js
и _index.css
.
Окей, мы получили результат, с которым можно работать. Но как же production-режим?
- Разделяем сборку финальных файлов для разных режимов.
module { config;}; { return path: 'bem-bl/blocks-common' check: false path: 'bem-bl/blocks-desktop' check: false path: 'lego/blocks-common' check: false path: 'lego/blocks-desktop' check: false 'common.blocks' 'desktop.blocks' ;}
Теперь для production-режима конечные файлы обрабатываются Борщиком. Production-режим запускается командой YENV=production node_modules/.bin/enb make
- Сборка
js
иcss
работает. Если в вашем проекте присутствуют другие цели или мультиязычность, то можно продолжить чтение данной документации в поисках информации о небходимых технологиях. - Собираем
node_modules/.bin/enb make
.
Настройка сборки
module { // Языки для проекта. config; // Добавление набора нод в сборку. config; // Добавление ноды в сборку + конфигурирование ноды. config; // Настройки для режима development. config; // Настройки для режима production. config; // Регистрация таска. config; // Установка переменных среды для shell-команд. config;};
Автоматизация с помощью express
При разработке nodejs
-приложений на базе express
можно сильно упростить использование enb
в development
-режиме.
Суть в том, что можно забыть о пересборке проекта, о других портах для статики и т.п. Можно просто отправлять в ENB
запросы на сборку тогда, когда это необходимо. То есть, когда вы открываете в браузере свой проект.
Для этого можно воспользоваться express
-совместимым middleware
. Его возвращает метод createMiddleware
модуля
lib/server/server-middleware
.
/** * @param * @param * @param * @returns */moduleexports { /* ... */ };
Пример использования:
app ;
Сборка по требованию
Помимо упрощения сборки статики в dev
-режиме с помощью ENB
в express
-приложениях,
можно собирать по требованию различные ресурсы, например, шаблоны.
Если nodejs
приложению в процессе работы требуется собирать шаблоны или локализацию (или что-нибудь еще),
то можно воспользоваться методом createBuilder
модуля lib/server/server-middleware
.
/** * @param * @param * @param * @returns */moduleexports { /* ... */ };
Пример использования:
var clearRequire = ;var enbBuilder = ;app ;
Подробное описание актуальных технологий
В алфавитном порядке.
Все технологии, включенные в пакет ENB
, находятся в папке techs
пакета. Подключаются из make-файла с помощью require('enb/techs/<tech-name>')
. Например, require('enb/techs/js')
. Подключаются к ноде указанием класса и опций: nodeConfig.addTech([ require('enb/techs/<tech-name>'), {/* [options] */} ]);
, либо без опций: nodeConfig.addTech(require('enb/techs/<tech-name>'));
.
Если при настройке технологии в опциях указана подстрока {lang}
, то будет создано столько копий технологии, сколько языков установлено для ноды или проекта (если у ноды не указаны языки).
Например:
nodeConfig;nodeConfig;
Эквивалентно:
nodeConfig;nodeConfig;nodeConfig;
file-copy
Копирует один таргет в другой.
Может, например, использоваться для построения _?.css
из ?.css
для development-режима.
Опции
- String sourceTarget — Исходный таргет. Обязательная опция.
- String destTarget — Результирующий таргет. Обязательная опция.
Пример
nodeConfig;
file-merge
Склеивает набор файлов в один.
Опции
- String[] sources — Список исходных таргетов. Обязательная опция.
- String target — Результирующий таргет. Обязательная опция.
- String divider — Строка для склеивания файлов. По умолчанию — "\n".
- Boolean sourcemap — Построение карт кода (source maps) с информацией об исходных файлах.
Пример
nodeConfig;
file-provider
Предоставляет существующий файл для make-платформы. Может, например, использоваться для предоставления исходного bemdecl-файла.
Опции
- String target — Таргет. Обязательная опция.
Пример
nodeConfig;
symlink
Создает симлинк из одного таргета в другой. Может, например, использоваться для построения _?.css
из ?.css
для development-режима.
Опции
- String fileTarget — Исходный таргет. Обязательная опция.
- String symlinkTarget — Результирующий таргет. Обязательная опция.
Пример
nodeConfig;
Как написать свою технологию
С версии 0.8 технологии рекомендуется писать с использованием хэлпера BuildFlow
.
Исходный код хэлпера: https://github.com/enb/enb/blob/master/lib/build-flow.js
В данном руководстве охвачены не все возможности BuildFlow
. Полный перечень методов с описанием находится
в JSDoc файла build-flow.js
.
Теория
Цель технологии — собирать таргет в ноде. Например, технология css
может собрать index.css
в ноде pages/index
на основе css
-файлов по уровням переопределения.
Каждая технология умеет принимать настройки.
Хэлпер BuildFlow
способствует тому, чтобы максимальное количество параметров было настраиваемым.
Технологии могут использовать результат выполнения других технологий. Например, список исходных css
-файлов
строится с помощью технологии files
.
В общем случае, технологии создавать несложно. Бывают необычные ситуации. В этом руководстве я постараюсь охватить и такие случаи.
Технология для склеивания файлов по суффиксу
В общем случае технология для склеивания файлов по нужному суффиксу выглядит следующим образом:
moduleexports = // Создаем инстанцию BuildFlow name'js' // Выбираем имя для технологии target'target' '?.js' // Имя опции для задания имени результирующего файла и значение по умолчанию // Указываем, какие суффиксы нас интересуют при сборке // Еще один хэлпер. Склеивает результат, обрамляя комментариями вида /* ... */ // в которых указывается путь к исходному файлу, из которого был сформирован фрагмент. ; // Создаем технологию с помощью хэлпера
Этот пример, конечно очень общий и слишком упрощенный.
Рассмотрим аналог этой технологии без использования justJoinFilesWithComments
:
var Vow = ; // Используемая в ENB библиотека промисовvar vowFs = ; // Работа с файловой системой на основе Vow moduleexports = name'js' target'target' '?.js' ;
Так как мы использовали метод useFileList
, в builder
пришел аргумент со списком файлов по указанному суффиксу.
Каждый use
-метод добавляет аргумент в builder
. Тип и содержимое аргументов зависят от того, какой use
-метод был
использован.
Добавим к получившейся технологии файлы интернационализации:
var Vow = ; // Используемая в ENB библиотека промисовvar vowFs = ; // Работа с файловой системой на основе Vow moduleexports = name'js' target'target' '?.js' // Определяем обязательную опцию lang для задания языка // Подключаем общую для всех языков интернационализацию, // используя метод useSourceText, который добавляет в // builder содержимое указанного файла в виде аргумента // Подключаем кейсеты конкретного языка; // здесь используется значение опции lang для того, // чтобы сформировать значение по умолчанию ;
Технология для склеивания нескольких целей
Рассмотрим готовый пример:
// В данном примере строится локализованный priv.jsmoduleexports = name'priv-js-i18n' target'target' '?.{lang}.priv.js' // Все эти цели подготавливаются другими технологиями: // Устанавливаем зависимость от имени файла // общей интернационализации // Устанавливаем зависимость от имени файла // конкретного языка // Устанавливаем зависимость от имени файла // priv-js файла // Пользуемся хэлпером для склеивания ;
Реализуем склеивание без хэлпера:
moduleexports = name'priv-js-i18n' target'target' '?.{lang}.priv.js' ;
Зависимости от файлов, не входящих в сборку
Например, нам надо добавить модульную систему в начало какого-нибудь файла и сохранить результат под новым именем:
var vowFs = ; // Подключаем модуль для работы с файловой системойvar path = ; // Подключаем утилиты работу с путями moduleexports = name'prepend-modules' target'target' '?.js' // Указываем обязательную опцию // Устанавливаем зависимость от содержимого цели, задаваемой опцией source ;
То же самое, но чуть-чуть иначе
Время от времени возникают ситуации, когда надо немного дополнить существующие технологии.
Например, нам нравится, как работает технология css
:
moduleexports = name'css' target'target' '?.css' ;
В каждой технологии, сделанной с помощью BuildFlow
, есть метод buildFlow()
, который можно вызвать для того,
чтобы создать новую технологию на основе функционала существующей.
В какой-то момент нам понадобилось вместе с суффиксами css
собирать еще и light.css
.
Для этого надо написать новую технологию, заимствуя функционал старой:
moduleexports = name'css-light' // Изменяем имя // Изменяем нужные параметры ;
Node API
Каждой технологии в init
приходит инстанция ноды, для которой необходимо собирать таргеты.
Через ноду технология взаимодействует с процессом сборки.
Основные методы класса Node:
node.getTargetName
// Возвращает имя таргета ноды без суффикса. Например, для ноды 'pages/index' результат — index.String Node::// Возвращает имя таргета ноды с суффиксом. Например, для ноды 'pages/index' с суффиксом 'js' результат — 'index.js'.String Node::
node.unmaskTargetName
// Демаскирует имя таргета ноды. Например, для ноды 'pages/index' и maskedTargetName='?.css', результат — 'index.css'.String Node::
node.resolvePath
// Возвращает абсолютный путь к таргету.String Node::
Пример
var fs = ;fs;
node.resolveTarget
// Оповещает ноду о том, что таргет собран. Опционально принимает результат сборки.// Результатом может быть любой объект, который может быть полезен другим технологиям для продолжения сборки.undefined Node::
Примеры
// #1thisnode; // #2 Получаем имя таргета динамически с помощью суффикса.thisnode; // #3 Получаем имя таргета путем демаскирования таргета.thisnode; // #4 Передаем значение.var target = thisnode targetPath = thisnode;delete requirecachetargetPath; // Избавляемся от кэширования в nodejs.thisnode;
node.rejectTarget
// Оповещает ноду о том, что таргет не может быть собран из-за ошибки.undefined Node::
Примеры
// #1thisnode; // #2 Получаем имя таргета динамически с помощью суффикса.thisnode;
node.requireSources
// Требует у ноды таргеты для дальнейшей сборки, возвращает промис.// Промис выполняется, возвращая массив результатов, которыми резолвились требуемые таргеты.// ВАЖНО: Не все технологии резолвят таргеты с результатом.// В данный момент резолвят с результатом технологии: levels, deps*, files.PromiseObject Node::
Пример
Например, нам надо объединить в один файл index.css
и index.ie.css
и записать в index.all.css
.
var vowFs = ;// ... { var _this = this; return thisnode; }// ...
node.relativePath
// Возвращает относительный путь к таргету относительно ноды.String Node::
node.getDir
// Возвращает полный путь к папке ноды.String Node::
node.getRootDir
// Возвращает полный путь к корневой папке проекта.String Node::
node.getLogger
// Возвращает инстанцию логгера для ноды.Logger Node::
Пример
thisnode;
node.getNodeCache
// Возвращает инстанцию кэша для таргета ноды.Cache Node::
Кэширование необходимо для того, чтобы избегать повторной сборки файлов, для которых сборка не требуется. Кэшируется время изменения исходных и конечных файлов после окончания сборки каждой технологии. Логика кэширования реализуется в каждой технологии индивидуально для максимальной гибкости.
С помощью методов Boolean needRebuildFile(String cacheKey, String filePath)
и Boolean needRebuildFileList(String cacheKey, FileInfo[] files)
производится валидация кэша.
С помощью методов undefined cacheFileInfo(String cacheKey, String filePath)
и undefined cacheFileList(String cacheKey, FileInfo[] files)
производится сохранение информации о файлах в кэш.
node.getSharedResources
Набор ресурсов, которые могут быть использованы в технологиях:
- JobQueue - пул дочерних процессов для выполнения "тяжелых" задач
Пример
Контент файла some-processor.js
:
module { var res = null; // Здесь какая-то нагруженная работа, возможно с использованием промисов return res;}
В технологии:
var jobQueue = thisnodejobQueue;// Выполнить таску в отдельном процессе, возвращается промис с результатомreturn jobQueue;