У нас есть документация про технологии БЭМ платформы (BEMJSON, BEMTREE, BEMHTML, DEPS), но практически нет туториалов по использованию их всех вместе. Есть несколько исключений, но они достаточно объемные и сложноваты для восприятия.
Я написал простой пример, рассчитанный на тех, кто уже немного освоился в версткой на основе project-stub и хочет продолжить свое знакомство с платформой.
Пусть у нас есть следующий файл с данными data.json:
{
"user": "mathetes",
"company": "WebExcel",
"city": "Novosibirsk"
};
Как вариант, данные могут приходить из БД или через HTTP API — источник не играет роли.
Наша задача сгенерировать на основе этих данных HTML, который будет представлять собой страницу с логотипом в шапке, карточкой пользователя в основной части и копирайтом в подвале.
Первым шагом необходимо из исходных сырых данных получить BEMJSON, описывающий страницу. Для этого будем использовать технологию BEMTREE. При этом договоримся, что в качестве корневого блока, на основе которого будет строиться дерево, возьмем блок page
.
В результате должен получиться следующий BEMJSON:
{
block: 'page',
content: [
{
block: 'header',
content: {
block: 'logo'
}
},
{
block: 'main',
content: {
block: 'user',
content: 'тут-содержимое-карточки-пользователя'
}
},
{
block: 'footer',
content: '© 2015 SuperPuper'
}
]
}
BEMTREE-шаблон для блока page
должен построить шапку, основную часть и подвал:
block('page').content()(function() {
return [
{ block: 'header' },
{ block: 'main' },
{ block: 'footer' }
];
]);
По техзаданию в шапке должен быть логотип. Тогда шаблон шапки может выглядеть так:
block('header').content()(function(){
return { block: 'logo' };
});
В основной части нужна карточка пользователя. Так что нам потребуется доступ к данным из файла data.json. Но пока отложим этот момент и захардкодим какие-то тестовые данные:
block('main').content()(function() {
return {
block: 'user',
content: [
{
elem: 'name',
content: 'test name'
},
{
elem: 'company',
content: 'test company'
},
{
elem: 'city',
content: 'test city'
}
]
};
});
В подвале нужен копирайт:
block('footer').content()('© 2015 SuperPuper');
Теперь, когда мы знаем, какие потребуются шаблоны, нужно скомпилировать BEMTREE-бандл, который будет включать ядро самого шаблонизатора и код шаблонов.
В самом простом случае мы можем сохранить все шаблоны в один файл, установить пакет bem-xjst и с его помощью скомпилировать бандл:
bem-xjst -i path/to/templates.js -o bundle.bemtree.js
Но раз мы хотим следовать рекомендации БЭМ методологии и раскладывать каждый шаблон в папку соответствующего блока, то нам потребуется какой-то способ потом эти шаблоны собрать вместе. Для этого подойдет сборщик ENB.
Схема работы ENB
подробно описана в этом документе. Главное, что нас сейчас интересует — это то, что ENB
собирает файлы только тех сущностей, которые явно задекларированы.
Получить декларацию с перечислением всех нужных блоков можно двумя способами: в *.bemdecl.js
перечислить все нужные блоки (и не забывать добавлять и удалять их по мере разработки и рефакторинга), либо указать только корневой блок (в нашем случае page
), а блоки, которые нужны корневому и всем последующим, указывать в их собственных списках зависимостей — deps.js. Второй путь гораздо гибче: сохраняется прицип БЭМ-методологии о том, что блок сам знает о себе всё, при удалении блока автоматически будут удалены и его зависимости, а при добавлении они автоматически включатся в сборку.
Так как шаблон блока page
создает блоки header
, main
и footer
, мы явно укажем это в списке зависимостей в файле page.deps.js
:
({
shouldDeps: ['header', 'main', 'footer']
})
Если вы имели опыт работы с
project-stub
, где нужные файлы попадали в сборку автоматически, то необходимость указывать зависимости вручную может показаться странной. Дело в том, что там у нас на руках заранее был готовый BEMJSON-файл, по которому можно было получить список всех необходимых сущностей. А в данном случае мы планируем генерировать BEMJSON в процессе сборки на основе шаблонов. При этом шаблоны необходимо собрать заранее, а значит декларацию нужных блоков потребуется описать самостоятельно.
Отлично, теперь мы знаем как собрать шаблоны. Следующим шагом необходимо получить с их помощью BEMJSON на основе данных, а затем из BEMJSON сгенерировать HTML с помощью BEMHTML. В общем виде это выглядит так:
var data = require('path/to/data.json'),
BEMTREE = require('path/to/bundle.bemtree.js').BEMTREE,
BEMHTML = require('path/to/bundle.bemhtml.js').BEMHTML,
bemjson = BEMTREE.apply(data),
html = BEMHTML.apply(bemjson);
require('fs').writeFileSync('index.html', html);
Эти преобразования будут работать и в браузере, если подключить bundle.bemtree.js
и bundle.bemhtml.js
на страницу. Останется только вставить полученную HTML-строку в DOM.
Осталось разобраться, как все-таки сгенерировать карточку пользователя на основе данных из data.json
, вместо использования хардкода.
Как видно в примере кода выше, данные мы передаем в вызов BEMTREE.apply(data)
. При этом мы помним, что корневым блоком должен оказаться блок page
. Достичь этого можно следующим образом:
var data = require('path/to/data.json');
BEMTREE.apply({
block: 'page',
data: data // теперь данные попадут в контекст шаблона блока page
});
Модифицируем код шаблона так, чтобы пробросить данные для вложенных в page
блоков:
block('page').content()(function() {
this.data = this.ctx.data; // this будет общим для всех потомков page,
// так что они смогут использовать поле data
return [
{ block: 'header' },
{ block: 'main' },
{ block: 'footer' }
];
]);
Тогда финальный вид BEMTREE-шаблона блока main
окажется таким:
block('main').content()(function() {
var data = this.data;
return {
block: 'user',
content: [
{
elem: 'name',
content: data.user
},
{
elem: 'company',
content: data.company
},
{
elem: 'city',
content: data.city
}
]
};
});
Из соображений унификации в качестве корневого блока удобно использовать блок root
, который будет отвечать за пробрасывание данных вглубь дерева и создавать page
:
block('root').replace()(function() {
return {
block: 'page',
title: 'TODO',
head: [
{ elem: 'css', url: 'index.min.css' }
],
scripts: [
{ elem: 'js', url: 'index.min.js' }
],
mods: { theme: 'islands' }
};
});