EN
tadatuta
tadatuta
17 ноября 2015

У нас есть документация про технологии БЭМ платформы (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' }
    };
});

Попробовать описанный выше подход можно на основе репозитория-заготовки.

belozer
#belozer
18 ноября 2015

Очень не хватает таких туториалов :) Спасибо!

mathetes
#mathetes
18 ноября 2015

@tadatuta Владимир! Благодарю за описание процесса.
Ваш пост и ответ Виталия Харисова представляют общий контекст взаимодействия БЭМ технологий. Но пока не вижу руководства как начать.
Поэтому прошу вас дать инструкцию по практическому созданию конкретного блока.
Например, блока nav.
Куда я должен его прописать, чтобы он прошел все уровни генерации?
Допустим у меня есть bundle index и в нем лежит файл index.deps.js котором уже перечислены другие блоки. Есть я пропишу в него "nav" а в bemhtml опишу логику отображения. будет ли этого достаточно?
Как мне разместить мой блок и как обеспечить вложенность блоков, чтобы блок nav оказался внутри блока header?

vithar
#vithar
18 ноября 2015

@mathetes вам не надо ничего делать с index.deps.js.

Вам достаточно использовать nav в page и прописать его в page.deps.js, смотрите, как это сделано в bem.info

mathetes
#mathetes
19 ноября 2015

Получается, что вызов блоков производится в родительских блоках в папке common.blocks.
Так я вижу в примере bem.info и заготовке tadatuta.
Таким образом формируется структура DOM и не нужно в одном файле писать портянку как это делается вручную при описании bemjson.
Правильно я понял?

vithar
#vithar
19 ноября 2015

Да. Каждый блок знает, как сформировать себя из данных (bemtree) и сгенерировать HTML (BEMHTML)

JiLiZART
#JiLiZART
20 ноября 2015

А как разруливать ситуацию если много страничек и на каждой разные данные. В root городить условия?

tadatuta
#tadatuta
20 ноября 2015

@JiLiZART Можно, например, так:

block('root').replace()(function() {
    return {
        block: 'page',
        mods: { view: this.ctx.view } // где view указывает на нужную страницу
    };
});

Либо уже в самом page:

block('page').content()(function() {
    return { block: this.data.view }; // при условии, что view пробросили в глобальную data
});

Тогда для каждого типа страницы создается свой собственный блок.

Ну и всякие промежуточные варианты типа:

block('page').content()(function() {
    return [
        { block: 'header' },
        { block: this.data.view },
        { block: 'footer' }
    ];
});
rustam-mh
#rustam-mh
30 декабря 2015

когда использую vm для загрузки .bemtree.js то в контексте можно указать глобальный объект для всех блоков

var context = VM.createContext({
    console: console,
    Vow: Vow,
    ...
    glob: glob
});
...
block('logo')(
    content()(function(){
         return [
            {
                 elem:'name'
                 content: glob.sitename
            },
            .....
         ]
    })
)

а как сделать по другому ?

tadatuta
#tadatuta
30 декабря 2015

@rustam-mh
В моем предыдущем комментарии есть ответ: можно положить необходимые данные в произвольное поле корневого блока, а затем в шаблоне этого блока положить их в this, тогда данные будут достпны всем потомкам:

var data = require('./path/to/data.json');
require('./bemtree-bundle.js').BEMTREE.apply({ block: 'root', data: data });
block('root').def()(function() {
    this.data = this.ctx.data;
    return applyNext();
});

block('child').content()(function() {
    return this.data;
});

При этом удобно вводить специальный корневой блок именно для пробрасывания данных и не создавать для него отдельный DOM-узел, чтобы при необходимость мочь применять шаблоны для произвольной части дерева. Поэтому можно использовать replace(), чтобы сразу заменять корневой блок на необходимый:

block('root').replace()(function() {
    this.data = this.ctx.data;
    return { block: 'page' };
});
v-bornov
#v-bornov
28 февраля 2016

Все-таки непонятно как реализовывать страницы сайта, чтоб на каждой были свои блоки, подключались свои стили и скрипты.

С этим ответом ознакомился https://ru.bem.info/forum/716/#comment-158405153
Получается, что различные страницы будут реализованы в рамках одного бандла.
В этом бандле мы задаем только один файл *.bemdecl.js

exports.blocks = [{ name: 'root' }];

А уже в root.bemtree.js мы в зависимости от контекста будем указывать модификатор для блока page или указывать для его содержимого определенные блоки.
Но как при таком подходе разделять скрипты и стили? Они все соберутся в одном большом бандле, а это не всегда хорошо.

А полагаю, что каждая страница должна быть представлена отдельным бандлом. Ну или не каждая страница, а каждый раздел сайта. Чтоб для этого раздела собирались свои стили и скрипты. А как это правильно сделать?

Правильно ли реализовывать каждую страницу через модификатор блока page?
Если страниц будет много, то много будет модификаторов у блока page, будет каша.

Или правильнее каждую страницу реализовывать отдельным блоком?

tadatuta
#tadatuta
28 февраля 2016

@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 один общий бандл и генерация страниц на основе массива записей в модели.

А в случае с динамикой может быть совершенно произвольная комбинация того, что (и в какой момент) нужно загрузить на клиент в зависимости от запроса.

DjonyBastone
#DjonyBastone
13 апреля 2017
block('page').content()(function() {
    return [
        { block: 'header' },
        { block: this.data.view },
        { block: 'footer' }
    ];
});

Это то что нужно.
Но для особо одаренных объясните пожалуйста:

  1. { block: this.data.view } что это за блок? где он рождается? понятно что это функция - "показать данные", а от куда они придут?
  2. Планируется несколько страниц. Каким образом показать сборщику различия в контенте? Опять - { block: this.data.view }, а где мне это содержимое закодить?
  3. Некоторые страницы статичные, должны собраться без внешних данных, только из блоков своей библиотеки и bem-components. Как дать понять сборщику, о разном содержании страниц?

2 и 3 вопрос различается этим - для динамики/для статики

tadatuta
#tadatuta
13 апреля 2017

В плане сборки динамика и статика может либо не отличаться вообще, если собирать по bemdecl-у, либо отличаться шагом автоматической генерации декла из bemjson-а.
Главное, что нужно понимать, что сборка для динамики должна включать все возможные варианты блоков, которые могут быть использованы в рантайме, т.е. их все следует перечислить в соответствующих bemdecl-ах.

А пример того, как подсунуть данные можно посмотреть в bem-express: https://github.com/bem/bem-express/blob/master/server/index.js#L67

DjonyBastone
#DjonyBastone
13 апреля 2017

Хорошо.
Попытаюсь по-другому сформулировать.

  • Существует файл index.bemdecl.js в папке - desktop.bundles/index. Имеет содержимое - exports.blocks = [{ name: 'root' }];
  • Существует файл contact.bemdecl.js в папке - desktop.bundles/contact. Имеет содержимое - exports.blocks = [{ name: 'root' }];

Планируется:

  • Общее содержание страниц:
block('page').content()(function() {
    return [
        { block: 'header' },
        { block: 'footer' }
    ];
});

То есть общее, это { block: 'header' }, { block: 'footer' }

  • Различающееся содержание страниц:
    index.bemdecl.js имеет:
block('content').content() {
    { block: 'video' },
    { block: 'text' },
    { block: 'map' }
});

contact.bemdecl.js имеет:

block('content').content() {
    { block: 'about' },
    { block: 'map' }
});

Считаю что block('content') - имеет разное содержимое. Следовательно он и есть - { block: this.data.view }
Следующий код дает понять, что содержимое меняется в зависимости от страницы, но так же существуют общие блоки:

block('page').content()(function() {
    return [
        { block: 'header' },
        { block: this.data.view },
        { block: 'footer' }
    ];
});

Этот код лежит в файле page.bemhtml.js и является шаблоном блока page находящийся common.blocks/page/

  1. В каком файле и директории, мне нужно подсунуть данные?
  2. desktop.bundles/contact/ и desktop.bundles/index/ - какой набор файлов должны содержать, и с каким содержимым?

Залил на гитхаб bem-bemtree-static-project-stub с добавленными кастомными блоками, для разбора полетов.
Или может ссылкой поделитесь с реализацией, где можно найти решения на данные вопросы!?

Словами, без кода не понятно. Код без слов, тоже малопонятен. Парадокс! )))) Код с разъяснением на примерах понятен. Документация с примерами сразу понимается, если там не абстракция (b_e.ctx и т.д.).

tadatuta
#tadatuta
13 апреля 2017

Ну так-то я к предыдущим своим словам привел ссылку на код ;)

Под { block: this.data.view } подразумевается не то, что там будет один блок с разным контентом, а просто разные блоки. Чтобы не путать их с остальными, мы их именуем с префиксом page-. Т.е. будут блоки page-index и page-contact, которые и опишут разное между страницами.

И именно они будут указаны в соответствующих бандлах помимо root.

Теперь понятнее или все-таки нужно показать в коде?

PS: обновил bem-bemtree-static-project-stub, есть смысл апнуться.

DjonyBastone
#DjonyBastone
14 апреля 2017

bem-bemtree-static-project-stub обновил. Там появился пример .bem/bemjson.js логика примерно понятна - entity.block подменяет значения на нужное.
Даже после просмотра вебинара Трехзвенная архитектура веб-сервисов на bem-xjst структура проекта не понятна.
Ясно что вариативность может быть разная, т.к. это JS.
Но хотя бы рабочий вариант для многостраничника покажите пожалуйста.
Не ясно:

  1. Структура проекта под разные страницы.
    common.bloks/
     page-index
     page-contact
     page-еще 80 блоков разных страниц
    
    это подразумевается?
    Или всё таки где-то в bundles держим эти декларации?
  2. Блок root - он остается неизменным и его структура переопределяется кодом из вашего примера .bem/bemjson.js ?
  3. В каком файле храним код из примера .bem/bemjson.js?
  4. Какой код должны содержать файлики desktop.bundles/*/*.bemdecl.js ?

Лирика:
Конечно было бы круто на сайте bem.info иметь пример проекта для bemtree в разделе "учебные материалы", по примеру Создаём свой проект на БЭМ.

DjonyBastone
#DjonyBastone
17 апреля 2017

cc @tadatuta

Володя пожалуйста помоги с примером реализации.

Честно, я пробовал, перечитал всю документацию, пересмотрел видео. Да, я научился... разобрался немного в ym, даже javascript стал немного понимать (при чтении + пару скриптов написал), писать рабочие шаблоны bemhtml, но не смог реализовать с bemtree требуемое выше.