У нас есть документация про технологии БЭМ платформы (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' }
};
});
Попробовать описанный выше подход можно на основе репозитория-заготовки.
Очень не хватает таких туториалов :) Спасибо!
@tadatuta Владимир! Благодарю за описание процесса. Ваш пост и ответ Виталия Харисова представляют общий контекст взаимодействия БЭМ технологий. Но пока не вижу руководства как начать. Поэтому прошу вас дать инструкцию по практическому созданию конкретного блока. Например, блока nav. Куда я должен его прописать, чтобы он прошел все уровни генерации? Допустим у меня есть bundle index и в нем лежит файл index.deps.js котором уже перечислены другие блоки. Есть я пропишу в него "nav" а в bemhtml опишу логику отображения. будет ли этого достаточно? Как мне разместить мой блок и как обеспечить вложенность блоков, чтобы блок nav оказался внутри блока header?
@mathetes вам не надо ничего делать с index.deps.js.
Вам достаточно использовать nav в page и прописать его в page.deps.js, смотрите, как это сделано в bem.info
Получается, что вызов блоков производится в родительских блоках в папке common.blocks. Так я вижу в примере bem.info и заготовке tadatuta. Таким образом формируется структура DOM и не нужно в одном файле писать портянку как это делается вручную при описании bemjson. Правильно я понял?
Да. Каждый блок знает, как сформировать себя из данных (bemtree) и сгенерировать HTML (BEMHTML)
А как разруливать ситуацию если много страничек и на каждой разные данные. В
root
городить условия?@JiLiZART Можно, например, так:
Либо уже в самом
page
:Тогда для каждого типа страницы создается свой собственный блок.
Ну и всякие промежуточные варианты типа:
когда использую vm для загрузки .bemtree.js то в контексте можно указать глобальный объект для всех блоков
а как сделать по другому ?
@rustam-mh В моем предыдущем комментарии есть ответ: можно положить необходимые данные в произвольное поле корневого блока, а затем в шаблоне этого блока положить их в
this
, тогда данные будут достпны всем потомкам:При этом удобно вводить специальный корневой блок именно для пробрасывания данных и не создавать для него отдельный DOM-узел, чтобы при необходимость мочь применять шаблоны для произвольной части дерева. Поэтому можно использовать
replace()
, чтобы сразу заменять корневой блок на необходимый:Все-таки непонятно как реализовывать страницы сайта, чтоб на каждой были свои блоки, подключались свои стили и скрипты.
С этим ответом ознакомился https://ru.bem.info/forum/716/#comment-158405153 Получается, что различные страницы будут реализованы в рамках одного бандла. В этом бандле мы задаем только один файл *.bemdecl.js
А уже в root.bemtree.js мы в зависимости от контекста будем указывать модификатор для блока page или указывать для его содержимого определенные блоки. Но как при таком подходе разделять скрипты и стили? Они все соберутся в одном большом бандле, а это не всегда хорошо.
А полагаю, что каждая страница должна быть представлена отдельным бандлом. Ну или не каждая страница, а каждый раздел сайта. Чтоб для этого раздела собирались свои стили и скрипты. А как это правильно сделать?
Правильно ли реализовывать каждую страницу через модификатор блока page? Если страниц будет много, то много будет модификаторов у блока page, будет каша.
Или правильнее каждую страницу реализовывать отдельным блоком?
@v-bornov В зависимости от задачи имеет смысл комбинировать все перечисленные варианты.
В частности в репозитории https://github.com/tadatuta/bem-bemtree-static-project-stub в ветке
master
предполагается подход один бандл — одна страница.В нем же, но в ветке https://github.com/tadatuta/bem-bemtree-static-project-stub/tree/one-bundle-gulp-watcher один общий бандл и генерация страниц на основе массива записей в модели.
А в случае с динамикой может быть совершенно произвольная комбинация того, что (и в какой момент) нужно загрузить на клиент в зависимости от запроса.
Это то что нужно. Но для особо одаренных объясните пожалуйста:
{ block: this.data.view }
что это за блок? где он рождается? понятно что это функция - "показать данные", а от куда они придут?{ block: this.data.view }
, а где мне это содержимое закодить?2 и 3 вопрос различается этим - для динамики/для статики
В плане сборки динамика и статика может либо не отличаться вообще, если собирать по bemdecl-у, либо отличаться шагом автоматической генерации декла из bemjson-а. Главное, что нужно понимать, что сборка для динамики должна включать все возможные варианты блоков, которые могут быть использованы в рантайме, т.е. их все следует перечислить в соответствующих bemdecl-ах.
А пример того, как подсунуть данные можно посмотреть в
bem-express
: https://github.com/bem/bem-express/blob/master/server/index.js#L67Хорошо. Попытаюсь по-другому сформулировать.
index.bemdecl.js
в папке -desktop.bundles/index
. Имеет содержимое -exports.blocks = [{ name: 'root' }];
contact.bemdecl.js
в папке -desktop.bundles/contact
. Имеет содержимое -exports.blocks = [{ name: 'root' }];
Планируется:
То есть общее, это
{ block: 'header' }
,{ block: 'footer' }
index.bemdecl.js
имеет:contact.bemdecl.js
имеет:Считаю что
block('content')
- имеет разное содержимое. Следовательно он и есть -{ block: this.data.view }
Следующий код дает понять, что содержимое меняется в зависимости от страницы, но так же существуют общие блоки:Этот код лежит в файле
page.bemhtml.js
и является шаблоном блокаpage
находящийсяcommon.blocks/page/
desktop.bundles/contact/
иdesktop.bundles/index/
- какой набор файлов должны содержать, и с каким содержимым?Залил на гитхаб bem-bemtree-static-project-stub с добавленными кастомными блоками, для разбора полетов. Или может ссылкой поделитесь с реализацией, где можно найти решения на данные вопросы!?
Словами, без кода не понятно. Код без слов, тоже малопонятен. Парадокс! )))) Код с разъяснением на примерах понятен. Документация с примерами сразу понимается, если там не абстракция (b_e.ctx и т.д.).
Ну так-то я к предыдущим своим словам привел ссылку на код ;)
Под
{ block: this.data.view }
подразумевается не то, что там будет один блок с разным контентом, а просто разные блоки. Чтобы не путать их с остальными, мы их именуем с префиксомpage-
. Т.е. будут блокиpage-index
иpage-contact
, которые и опишут разное между страницами.И именно они будут указаны в соответствующих бандлах помимо
root
.Теперь понятнее или все-таки нужно показать в коде?
PS: обновил
bem-bemtree-static-project-stub
, есть смысл апнуться.bem-bemtree-static-project-stub
обновил. Там появился пример.bem/bemjson.js
логика примерно понятна -entity.block
подменяет значения на нужное. Даже после просмотра вебинара Трехзвенная архитектура веб-сервисов на bem-xjst структура проекта не понятна. Ясно что вариативность может быть разная, т.к. это JS. Но хотя бы рабочий вариант для многостраничника покажите пожалуйста. Не ясно:bundles
держим эти декларации?root
- он остается неизменным и его структура переопределяется кодом из вашего примера.bem/bemjson.js
?.bem/bemjson.js
?desktop.bundles/*/*.bemdecl.js
?Лирика: Конечно было бы круто на сайте bem.info иметь пример проекта для bemtree в разделе "учебные материалы", по примеру Создаём свой проект на БЭМ.
cc @tadatuta
Володя пожалуйста помоги с примером реализации.
Честно, я пробовал, перечитал всю документацию, пересмотрел видео. Да, я научился... разобрался немного в ym, даже javascript стал немного понимать (при чтении + пару скриптов написал), писать рабочие шаблоны bemhtml, но не смог реализовать с bemtree требуемое выше.