company_banner

Сайт с нуля на полном стеке БЭМ-технологий. Методология Яндекса

  • Tutorial
На прошлой неделе BBC рассказала, что для новой версии главной страницы использовала методологию БЭМ, созданную в Яндексе. По такому случаю мы решили поднять материалы мастер-класса «Разрабатываем сайт с нуля на полном стеке БЭМ-технологий» и рассказать вам, как начать использовать полный стек БЭМ-технологий в своих проектах.

БЭМ упрощает разработку сайтов, которые нужно быстро создавать и долго поддерживать. Эту технологию используют во фронтенде почти всех сервисов Яндекса, и она уже успела обрасти множеством библиотек и инструментов, которыми мы хотим с вами поделиться.



В статье мы расскажем, в чём преимущество вёрстки независимыми блоками и что такое уровни переопределения, познакомимся с готовыми библиотеками блоков и инструментами для автоматизации сборки. Покажем, как разные инструменты — например, autoprefixer, css-препроцессор Stylus или модульная система YModules — упрощают жизнь разработчика и создают по-настоящему удобную платформу, если встроить их в процесс разработки по БЭМ.

На живом примере мы объясним, в чём польза декларативного подхода, когда одни и те же идеи можно использовать как для CSS, так и для JavaScript. Отдельно остановимся на декларативных шаблонах BEMHTML и BEMTREE, которые позволяют преобразовывать данные в БЭМ-дерево, описанное в формате BEMJSON и, затем в HTML. Рассмотрим в деталях, как написать серверную часть приложения по БЭМ-методологии.

Мы будем использовать API Twitter'а для создания нашего проекта. В результате получим работающий сайт на полном стеке БЭМ-технологий и пошаговое статью-руководство, как все это можно воспроизвести.

Специально для мастер-класса мы написали мини-сервис, который занимается поиском по различным социальным сетям и выводит результат в упорядоченном виде. Мы выложили его на github в репозитории github.com/bem/sssrсмотрите, знакомьтесь.
А мы пойдём по порядку.

Теория


Что же такое БЭМ?
БЭМ (аббревиатура от слов — Блок, Элемент и Модификатор) — это методология разработки программ и интерфейсов, способ описания сущностей, не привязанный к конкретным технологиям реализации.
  • Блок — это отдельный компонент приложения. Он независим от других блоков и может содержать в себе другие блоки и элементы.
  • Элемент — это часть блока, отвечающая за отдельную функцию. Он не имеет смысла в отрыве от блока.
  • Модификатор — это свойство блока или элемента, отвечающее за его внешний вид или поведение. Модификаторы описывают состояние блока или элемента.

БЭМ предоставляет абстракцию над DOM-деревом. Блоки независимы друг от друга и инкапсулируют в себе всю функциональность и элементы. Не важно, какими HTML-тегами будет реализован блок — div или form, вы всегда можете изменить это или добавить дополнительные обёртки. Любые изменения не должны оказывать влияние на остальные блоки. Мы описываем приложение компонентами интерфейса, а не HTML-тегами.

Каждый блок лежит в своей папке в файловой системе, в которой сложены все технологии, описывающие блок, его элементы и модификаторы.

desktop.blocks/
    input/
        __box/         # элемент
        __clear/       # элемент
        __control/     # элемент
        _focused/      # модификатор
        _type/         # модификатор
        input.css      # css реализация блока
        input.js       # js реализация блока
        input.ru.md    # markdown документация
…

Если вам интересны подробности о том, как и почему появился БЭМ, читайте статью Виталия Харисова «История БЭМ» и смотрите видеозаписи доклада.
Подробное описание методологии БЭМ можно найти на нашем сайте.

Создание заготовки проекта


Установим все необходимое для работы.
Для начала нам понадобится терминал и система контроля версий git. Установить ее можно с сайта git-scm.com.
Почти все наши инструменты написаны на JavaScript, потому вам понадобится node.js или io.js.
Для создания заготовки нашего проекта используем генератор generator-bem-stub.
> npm install -g generator-bem-stub

После чего запустим сам генератор:
> yo bem-stub

Отвечая на вопросы, касающиеся используемых технологий, получим собранную и сконфигурированную для сборки заготовку.
Пройдемся по вопросам:



На скриншоте результаты ответов на вопросы. Первые три вопроса очевидны, после начинается интересное:
  • Choose a toolkit to build the project: (какой сборщик использовать) — мы используем инструмент ENB. Это утилита, которая будет собирать наш проект — склеивать стили, скрипты, шаблоны, компилировать и оптимизировать в соответствии с декларацией страницы, зависимостями блоков и файлами конфигурации.
  • Specify additional libraries if needed: (хотим ли мы использовать дополнительные библиотеки) – в нашем проекте мы будем использовать библиотеку блоков bem-components. В ней есть опциональные стилевые темы.

Пришло время рассмотреть, что такое уровни переопределения.

Уровень переопределения


Это набор реализаций блоков. Проект может иметь несколько уровней, на каждом из которых добавляется или изменяется реализация блоков. Конечная реализация блока собирается со всех уровней последовательно в заданном порядке.
Мы можем доопределять и переопределять стили, шаблоны, JavaScript-реализацию блоков на уровне переопределения своего проекта. При этом, мы ничего не меняем в исходных файлах библиотеки, позволяя сохранять наши изменения в случае её обновления.

Приведём пример, как это выглядит в файловой системе:

…
libs/
    bem-components/
        desktop.blocks/
            input/
                input.css
desktop.blocks/
    input/
        input.css
…

Создавая блок на уровне desktop.blocks нашего проекта, можно доопределить или переопределить нужные нам технологии.
В примере выше мы можем отредактировать стили блока input, добавив его реализацию в технологии CSS.

Итак, наша заготовка проекта готова. Перейдем в каталог проекта:
> cd sssr-tutorial

Вёрстка


Для начала создадим статический прототип нашей страницы. Для описания её структуры воспользуемся технологией BEMJSON.

В BEMJSON описывается БЭМ-дерево: порядок и вложенность блоков, названия и состояния БЭМ-сущностей, дополнительные произвольные поля.

Cоберём сгенерированный проект и посмотрим, что получилось. Для удобной работы с локально установленым пакетом ENB нужно выполнить следующую команду:

> export PATH=./node_modules/.bin:$PATH

Или вручную запускать команду enb из поддиректории ./node_modules/.bin/
Для сборки мы воспользуемся командой enb server:

> enb server

Теперь страницу можно открыть по адресу: http://localhost:8080/desktop.bundles/index/index.html.Наш сборщик соберёт все необходимые зависимости, а по ним соберёт файлы нужных блоков и технологий.



Откройте инспектор в браузере и посмотрите на DOM-дерево. Хоть мы ещё не написали ни строчки кода, но на этой странице уже есть сгенерированный HTML. Это потому, что используются шаблоны из наших библиотек. Например, шаблон блока page из библиотеки bem-core генерирует обвязку страницы (doctype, html, head, body и т.д.).

Наш проект содержит файл index.bemjson.js в папке ./desktop.bundles/index/:

({
    block: 'page',
    title: 'Hello, World!',
    styles: [
        { elem: 'css', url: 'index.min.css' }
    ],
    scripts: [
        { elem: 'js', url: 'index.min.js' }
    ],
    content: [
        'Hello, World!'
    ]
})

Этот файл представляет собой описание страницы в БЭМ-терминах. Корневой блок в нашем БЭМ-дереве — page. У него есть API — дополнительные ключевые слова — title, favicon и т.д. Шаблоны этого блока находятся в библиотеке bem-core.

Наше приложение состоит из двух основных частей — шапки и содержимого. Добавим в содержимое страницы блок sssr, в котором в виде элементов будут описаны части интерфейса. Для этого отредактируем ./desktop.bundles/index/index.bemjson.js:

({
    block: 'page',
    //…
    content: [
        {
            block: 'sssr',
            content: [
                {
                    elem: 'header'
                },
                {
                    elem: 'content'
                }
            ]
        }
    ]
});

В шапке, в свою очередь, будет расположена поисковая форма и название сайта с логотипом:

{
    block: 'sssr',
    content: [
        {
            elem: 'header',
            content: [
                {
                    elem: 'logo',
                    content: 'Social Services Search Robot:'
                },
                {
                    block: 'form',
                    content: [
                        {
                            elem: 'search'
                        },
                        {
                            elem: 'filter',
                            content: '[x] twitter'
                        }
                    ]
                }
            ]
        },
        {
            elem: 'content'
        }
    ]
}



Используем блоки input, button, spin и checkbox из библиотеки bem-components. В нашем проекте эта библиотека лежит в папке ./libs/bem-components. У каждого из этих блоков есть свой API, который можно посмотреть в документации.

Добавим необходимые блоки в BEMJSON:

{
    block: 'sssr',
    content: [
        {
            elem: 'header',
            content: [
                {
                    elem: 'logo',
                    content: [
                        {
                            block: 'icon',
                            mods: { type: 'sssr' }
                        },
                        'Social Services Search Robot:'
                    ]
                },
                {
                    block: 'form',
                    content: [
                        {
                            elem: 'search',
                            content: [
                                {
                                    block: 'input',
                                    mods: { theme: 'islands', size: 'm', 'has-clear' : true },
                                    name: 'query',
                                    val: '#b_',
                                    placeholder: 'try me, baby!'
                                },
                                {
                                    block: 'button',
                                    mods: { theme: 'islands', size: 'm', type: 'submit' },
                                    text: 'Найти'
                                },
                                {
                                    block: 'spin',
                                    mods: { theme: 'islands', size : 's' }
                                }
                            ]
                        },
                        {
                            elem: 'filter',
                            content: '[] twitter [] instagram'
                        }
                    ]
                }
            ]
        }
    ]
}

В этом фрагменте BEMJSON встречается поле mods. Оно указывает на используемые модификаторы и их значения. Поле mods содержит ключ: значениеmods: { type: 'sssr' }.

В BEMJSON можно использовать произвольные JavaScript-выражения. Добавим в поле content элемента filter конструкцию map для повторяющихся блоков checkbox:

//…
{
    elem: 'filter',
    content: ['twitter', 'instagram'].map(function(service) {
        return {
            block: 'checkbox',
            mods: {
                theme: 'islands',
                size: 'l',
                checked: service === 'twitter'
            },
            name: service,
            text: service
        };
    })
}
//…

Полный файл index.bemjson.js:

({
    block: 'page',
    title: 'Social Services Search Robot',
    favicon: '/favicon.ico',
    head: [
        { elem: 'meta', attrs: { name: 'description', content: 'find them all' }},
        { elem: 'css', url: '_index.css' }
    ],
    scripts: [{ elem: 'js', url: '_index.js' }],
    content: {
        block: 'sssr',
        content: [
            {
                elem: 'header',
                content: [
                    {
                        elem: 'logo',
                        content: [
                            {
                                block: 'icon',
                                mods: { type: 'sssr' }
                            },
                            'Social Services Search Robot:'
                        ]
                    },
                    {
                        block: 'form',
                        content: [
                            {
                                elem: 'search',
                                content: [
                                    {
                                        block: 'input',
                                        mods: { theme: 'islands', size: 'm', 'has-clear' : true },
                                        name: 'query',
                                        val: '#b_',
                                        placeholder: 'try me, baby!'
                                    },
                                    {
                                        block: 'button',
                                        mods: { theme: 'islands', size: 'm', type: 'submit' },
                                        text: 'Найти'
                                    },
                                    {
                                        block: 'spin',
                                        mods: { theme: 'islands', size : 's' }
                                    }
                                ]
                            },
                            {
                                elem: 'filter',
                                content: ['twitter', 'instagram'].map(function(service) {
                                    return {
                                        block: 'checkbox',
                                        mods: {
                                            theme: 'islands',
                                            size: 'l',
                                            checked: service === 'twitter'
                                        },
                                        name: service,
                                        text: service
                                    };
                                })
                            }
                        ]
                    }
                ]
            },
            {
                elem: 'content'
            }
        ]
    }
})

После того, как мы описали структуру интерфейса, нужно написать и доопределить стили для наших блоков. Все основные стили приезжают к нам с библиотекой bem-components. Так что нам нужно дописать совсем немного.

Для написания стилей мы используем CSS-препроцессор Stylus. Все файлы с расширением *.styl будут обработаны препроцессором и склеены в финальный CSS-файл. Также можно использовать расширение *.css для стилей, которые не нужно обрабатывать препроцессором.
Напишем стили для блока form в файле ./desktop.blocks/form/form.styl:

.form
{
    display: flex;

    &__search
    {
        margin-right: auto;
    }

    .input
    {
        width: 400px;
    }

    .checkbox
    {
        display: inline-block;

        margin-left: 15px;

        user-select: none;
        vertical-align: top;
    }
}

Для блока page в файле ./desktop.blocks/page/page.css:

.page
{
    font-family: Tahoma, sans-serif;

    min-height: 100%;
    margin: 0;
    padding-top: 100px;

    background: #000;
}

Для блока sssr в файле ./desktop.blocks/sssr/sssr.styl:

.sssr
{
    &__header
    {
        position: fixed;
        z-index: 1;
        top: 0;
        box-sizing: border-box;
        width: 100%;
        padding: 10px 10%;
        background: #f6f6f6;
        box-shadow: 0 0 0 1px rgba(0,0,0,.1), 0 10px 20px -5px rgba(0,0,0,.4);

        .button
        {
            margin-left: 10px;
        }
    }

    &__logo
    {
        font-size: 18px;
        margin: 0 0 10px;
    }

    &__content
    {
        padding: 10px 10%;
        column-count: 4;
        column-gap: 15px;
        transition: opacity .20s linear;
    }

    a[rel='nofollow'],
    a[xhref],
    [name][server]
    {
        text-decoration: none;
        color: #038543;
    }
}

И для блока userdesktop.blocks/user/user.styl:

.user
{
    &__name
    {
        display: inline-block;

        margin-right: 10px;

        text-decoration: none;

        color: #000;

        &:hover
        {
            text-decoration: underline;

            color: #038543;
        }
    }

    &__post-time
    {
        font-size: 14px;

        display: inline-block;

        color: #8899a6;
    }

    &__icon
    {
        position: absolute;
        right: 5px;
        bottom: 5px;

        width: 30px;
        height: 30px;

        border-radius: 3px;
    }
}

Не будем останавливаться на вопросах CSS-вёрстки очень подробно, пойдём дальше.

Нам осталось добавить блоки с найденными сообщениями. Опишем их в index.bemjson.js и для прототипирования воспользуемся возможностями JavaScript.

{
    elem: 'content',
    content: (function() {

        return 'BEM is extermly cool'.split('').map(function() {
            var service = ['twitter', 'instagram'][Math.floor(Math.random()*2)];

            return {
                service: service,
                user: [{
                    login: 'tadatuta',
                    name: 'Vladimir',
                    avatar: 'https://raw.githubusercontent.com/bem/bem-identity/master/sign/_theme/sign_theme_batman.png'
                }, {
                    login: 'dmtry',
                    name: 'Dmitry',
                    avatar: 'https://raw.githubusercontent.com/bem/bem-identity/master/sign/_theme/sign_theme_captain-america.png'
                },  {
                    login: 'sipayrt',
                    name: 'Jack Konstantinov',
                    avatar: 'https://raw.githubusercontent.com/bem/bem-identity/master/sign/_theme/sign_theme_ironman.png'
                }, {
                    login: 'einstein',
                    name: 'Slava',
                    avatar: 'https://raw.githubusercontent.com/bem/bem-identity/master/sign/_theme/sign_theme_robin.png'
                }][Math.floor(Math.random()*4)],
                time: Math.floor((Math.random()*12)+1) + 'h',
                img: service === 'instagram' ? 'http://bla.jpg' : undefined,
                text: [
                    'Блок — это независимый интерфейсный компонент. Блок может быть простым или составным (содержать другие блоки).',
                    'Элемент — это составная часть блока.',
                    'У блока или элемента может быть несколько модификаторов одновременно.'][Math.floor(Math.random()*3)]
            };
        }).map(function(dataItem) {
            return {
                block: 'island',
                content: [
                    {
                        elem: 'header',
                        content: {
                            block: 'user',
                            content: [
                                {
                                    block: 'link',
                                    mix: { block: 'user', elem: 'name' },
                                    url: 'https://www.yandex.ru',
                                    target: '_blank',
                                    content: dataItem.user.name
                                },
                                {
                                    elem: 'post-time',
                                    content: dataItem.time
                                },
                                {
                                    block: 'image',
                                    mix: { block: 'user', elem: 'icon' },
                                    url: dataItem.user.avatar,
                                    alt: dataItem.user.name
                                }
                            ]
                        }
                    },
                    {
                        elem: 'text',
                        content: dataItem.text
                    },
                    {
                        elem: 'footer',
                        content: [
                            {
                                block: 'service',
                                mods: { type: dataItem.service }
                            }
                        ]
                    }
                ]
            };
        });
    })()
}

и добавим стили для блока island в файл ./desktop.blocks/island/island.styl:

.island
{
    font-size: 18px;
    line-height: 140%;
    position: relative;
    display: inline-block;
    box-sizing: border-box;
    width: 100%;
    margin-bottom: 15px;
    padding: 15px 5px 5px 15px;
    border-radius: 3px;
    background: #fff;
    box-shadow: inset 0 0 1px rgba(0, 0, 0, .4);

    &__footer
    {
      margin-top: 10px;
    }
    &__image
    {
        display: block;
        width: 100%;
        border-radius: 3px;
    }
}

Давайте посмотрим на результат:



Шаблонизатор BEMHTML


Декларативная шаблонизация


В Яндексе очень любят декларативность — не только в CSS, но в шаблонах и в JavaScript'е.
Как выглядит декларативность в CSS:

.menu__item { display: inline-block; }

Для всех элементов item блока menu будет применен стиль display: inline-block;, т.е. мы декларируем, как должны быть обработаны
наши DOM-узлы, отобранные по условию:

условие { правила }

Мы отбираем все узлы DOM-дерева, соответствующие условию, и применяем к ним тело шаблона.

Для декларативной шаблонизации в Яндексе написали свой шаблонизатор BEMHTML. Подробнее о его архитектуре можно узнать из статьи Шаблонизация данных в bem-core.
Пример декларативного шаблона на BEMHTML:

block('menu').elem('item').tag()('span');

Отбираются все блоки БЭМ-дерева, соответствующие нашим условиям, потом к ним применяется тело шаблона:

(условия)(тело шаблона) 

BEMHTML написан на JavaScript. Его синтаксис — это чистый JavaScript. Можно использовать JavaScript-функции в подпредикатах и теле шаблона. Для production-режима шаблоны будут скомпилированы в оптимизированный JavaScript.
BEMHTML отвечает за то, как БЭМ-дерево преобразуется в HTML-строку. Входными данными является БЭМ-дерево или его фрагмент, описанный в технологии BEMJSON. На этот BEMJSON накладывается BEMHTML-шаблон. А выходные данные – это HTML-строка.

В общем виде шаблон выглядит следующим образом:

match(подпредикат1, подпредикат2, подпредикат3)(тело);

Подпредикаты — это условия, при выполнении которых применяется шаблон. Например:

match(подпредикат1, подпредикат2, подпредикат3)(тело);

Этот шаблон проверяет, является ли текущий блок блоком link, есть ли в контексте this.ctx переменная url, и является ли текущая мода модой tag. При соблюдении всех этих условий, к блоку будет применен тег a.

Мода


Мода — это шаг генерации выходного HTML. Каждая мода отвечает за свой кусочек получающегося HTML-кода. Мода default описывает набор и порядок прохождения остальных мод. На этой схеме видно, за что отвечает каждая мода:

Схема мод при генерации HTML

Рекомендуем вдумчиво прочитать документацию по BEMHTML, описанную в Cправочном руководстве по шаблонизатору BEMHTML.

Вернемся к нашему проекту. Нам нужен блок form. Он должен отображаться как тег <form> и иметь JavaScript-реализацию.
Если мы добавим еще один такой блок на страницу, нам придется редактировать эти параметры прямо в BEMJSON-файле. Это похоже на использование инлайновых стилей в HTML. Давайте все сделаем правильно и вынесем параметры блока в его шаблон:
./desktop.blocks/form/form.bemhtml:

block('form')(
    tag()('form'),
    js()(true)
);

Теперь мы можем редактировать шаблон блока в одном месте, переносить и реиспользовать этот блок с легкостью.

Посмотрим на DOM-дерево в инспекторе — наш блок form теперь выводится как тег <form> с классом i-bem. Этот класс говорит о том, что у блока есть реализация в JavaScript.



Мы описали то, как должны преобразовываться наши БЭМ-блоки в HTML. Теперь давайте рассмотрим, как будут получены и обработаны данные twitter'а

Архитектура приложения


Двухэтапная шаблонизация


Наше приложение будет работать по следующей схеме:
  • На первом этапе собираем данные с сервисов и строим БЭМ-дерево на основе этих данных;
  • На втором — преобразуем БЭМ-дерево (view-ориентированные данные) в DOM-дерево и отдаем HTML на клиентскую сторону.

BEMTREE


Мы говорили о том, как преобразовать БЭМ-дерево в HTML. Это задача frontend-сервера. А задачей построения БЭМ-дерева и насыщения его данными занимается шаблонизатор BEMTREE. Он совпадает по синтаксису с BEMHTML. Основное отличие — количество доступных стандартных мод. В BEMTREE есть только default и content.
Входными данными для BEMTREE выступают сырые данные, которыми насыщаются шаблоны блоков. На выходе мы получаем готовый фрагмент БЭМ-дерева, который передадим дальше на BEMHTML-шаблонизацию.

Сразу в бой. Напишем BEMTREE-шаблон для модификатора { type: 'twitter' }, блока island:
desktop.blocks/island/_type/island_type_twitter.bemtree

block('island').mod('type', 'twitter').content()(function() {
    var data = {
        postLink: '#',
        userName: 'user@name',
        userNick: 'user@nick',
        createdAt: '19 of July',
        avatar: '#avatar',
        text: 'message going here',
        type: 'twitter'
    };
    return [
        {
            elem: 'header',
            content: {
                block: 'user',
                content: [
                    {
                        block: 'link',
                        mods: { theme: 'islands' },
                        mix: { block: 'user', elem: 'name' },
                        url: data.postLink,
                        content: [data.userName, ' @', data.userNick]
                    },
                    {
                        elem: 'post-time',
                        content: data.createdAt.toString()
                    },
                    {
                        block: 'image',
                        mix: { block: 'user', elem: 'icon' },
                        url: data.avatar,
                        alt: data.userName
                    }
                ]
            }
        },
        {
            elem: 'text',
            content: data.text
        },
        {
            elem: 'footer',
            content: [
                {
                    block: 'service',
                    mods: { type: data.type }
                }
            ]
        }
    ];
});

В содержимое этого блока мы передаем блок image с необходимыми параметрами и примиксовываем элемент image блока island.
В дальнейшем мы заменим статический объект на данные, передаваемые на шаблонизацию. Но сначала посмотрим, каким образом будет организован серверный код, и как будут передаваться эти данные.

На сервере


Наше приложение будет работать на фреймворке express — отдавать HTML в ответ на поисковый запрос.

Напишем блоки, отвечающие за сбор данных с сервисов. Серверный код мы будем писать в файлы с расширением *.node.js, которые при сборке будут склеиваться в один файл. Его мы и будем запускать с помощью node.js.

Блок service_type_twitter


Для простоты работы с twitter'ом используем модуль twit. Установим его с помощью npm:

> npm i twit --save

Авторизационные данные, необходимые для работы с twitter'ом, мы вынесли в отдельный файл. Скопируем его содержимое себе в одноименный файл.

Отредактируем ./desktop.blocks/service/_type/service_type_twitter.node.js:

var twitter = require('twit'),
    config = require('./service_type_twitter.config'),
    twit = new twitter(config);

var query = '#b_',
    results = [];

twit.get('search/tweets', { q: query, count: 20 }, function(err, res) {

    if (err) {
        console.error(err);
        return [];
    }

    results = res.statuses.map(function(status) {
        var user = status.user;
        return {
            avatar: user.profile_image_url,
            userName: user.name,
            userNick: user.screen_name,
            postLink: 'https://twitter.com/' + user.screen_name,
            createdAt:  status.created_at,
            text: status.text,
            type: 'twitter'
        };
    });
    console.log(results);
});

Это приложение выполняет поиск по ключевому слову #b_ и выводит результат в консоль.
Пересоберем наш проект и запустим его с помощью node.js

> enb make 
> node ./desktop.bundles/index/index.node.js

Результатом выполнения должен быть список твитов в консоли.

Теперь нам нужно как-то передать результаты выполнения для дальнейшей работы — шаблонизации и передачи на клиент.
Для асинхронной работы с помощью промисов мы используем библиотеку vow.
Для организации серверного и клиентского JS-кода — модульную систему YModules.

Модульная система


Библиотека bem-core использует модульную систему ymodules.
Она позволяет обернуть код нашего блока в обертку-модуль и вызывать его при необходимости из других модулей.

Отредактируем файл service_type_twitter.node.js в соответствии с этими дополнениями:

modules.define('twitter', function(provide) {

var vow = require('vow'),
    moment = require('moment'),
    twitter = require('twit'),
    twitterText = require('twitter-text'),
    config = require('./service_type_twitter.config'),
    twit = new twitter(config);

    provide({
        get: function(query) {
            var dfd = vow.defer();

            twit.get('search/tweets', { q: query, count: 20 }, function(err, res) {

                if(err || !res.statuses) {
                    console.error(err);
                    dfd.resolve([]);
                }

                dfd.resolve(res.statuses.map(function(status) {
                    return {
                        avatar: status.user.profile_image_url,
                        userName: status.user.name,
                        userNick: status.user.screen_name,
                        postLink: 'https://twitter.com/' + status.user.screen_name,
                        createdAt:  moment(status.created_at),
                        text: twitterText.autoLink(twitterText.htmlEscape(status.text)),
                        type: 'twitter'
                    };
                }));
            });

            return dfd.promise();
        }
    });
});

Как видите, мы обернули весь код в конструкцию modules.define. Это декларация модуля twitter, который в дальнейшем будет доступен в нашем приложении через пространство имен modules.
Для асинхронной передачи результата мы возвращаем промис, в который, в зависимости от результатов выполнения запроса, передаем либо пустой массив, если была ошибка, либо массив с результатами поиска.
Для работы с датами добавим модуль moment.js.
Twitter возвращает нам в сообщениях простой текст, поэтому для выделения хэш-тегов и ссылок используем библиотеку twitter-text.
Кроме того, как уже говорилось выше, нам понадобится express.
Давайте установим эти модули:

> npm i vow moment twitter-text express --save

Блок server


За работу серверной части нашего приложения будет отвечать блок server. Добавим папку ./desktop.blocks/server/ и в ней создадим файл server.node.js.

Это будет express-приложение, которое слушает URL /search и отдает данные в соответствии с запросом.

modules.require(['twitter'], function(twitter) {

var fs = require('fs'),
    PATH = require('path'),
    express = require('express'),
    app = express(),
    url = require('url'),
    querystring = require('querystring'),
    Vow = require('vow');

app.get('/search', function(req, res) {

    var dataEntries = [],
        searchObj = url.parse(req.url, true).query,
        queryString = querystring.escape(searchObj.query),
        servicesEnabled = [];

    searchObj.twitter && servicesEnabled.push(twitter.get(queryString));

    Vow.all(servicesEnabled)
        .then(function(results) {
            res.end(JSON.stringify(results, null, 4));
        })
        .fail(function() {
            console.error(arguments);
        });
    });

    var server = app.listen(3000, function() {
        console.log('Listening on port %d', server.address().port);
    });

});

Создадим файл ./desktop.blocks/sssr/sssr.deps.js со следующим содержанием:

({
    shouldDeps: [
        { block: 'server' },
        { block: 'island', mods: { type: ['twitter'] }}
    ]
})

Здесь написано, что для работы блоку sssr нужны блоки server и island с модификатором type: 'twitter'.

Также добавим модификатор service_type_twitter в зависимости блока server. Для этого создадим файл ./desktop.blocks/server/server.deps.js:

({
    shouldDeps: [
        {
            block: 'service',
            mods: { type: ['twitter'] }
        },
        {
            block: 'sssr',
        }
    ]
})

Теперь все нужные нам блоки попадут в сборку. Пересоберем проект и запустим сервер:

> enb make && node ./desktop.bundles/index/index.node.js


По адресу http://localhost:3000/search?query=%23b_&twitter=on откроется страница с JSON-объектом данных, которые отдает блок service_type_twitter.



Теперь добавим преобразование этих данных в BEMJSON с помощью BEMTREE. Отредактируем server.node.js:

modules.require(['twitter'], function(twitter) {

var fs = require('fs'),
    PATH = require('path'),
    VM = require('vm'),
    express = require('express'),
    app = express(),
    url = require('url'),
    querystring = require('querystring'),
    moment = require('moment'),
    Vow = require('vow'),
    pathToBundle = PATH.join('.', 'desktop.bundles', 'index');

app.use(express.static(pathToBundle));

var bemtreeTemplate = fs.readFileSync(PATH.join(pathToBundle, 'index.bemtree.js'), 'utf-8');

var context = VM.createContext({
    console: console,
    Vow: Vow
});

VM.runInContext(bemtreeTemplate, context);
var BEMTREE = context.BEMTREE;

app.get('/search', function(req, res) {

    var dataEntries = [],
        searchObj = url.parse(req.url, true).query,
        queryString = querystring.escape(searchObj.query),
        servicesEnabled = [];

    searchObj.twitter && servicesEnabled.push(twitter.get(queryString));

    Vow.all(servicesEnabled)
        .then(function(results) {

            // Склеиваем результаты поиска в один массив,
            // понадобится при добавлении сервисов
            Object.keys(results).map(function(idx) {
                dataEntries = dataEntries.concat(results[idx]);
            });

            // Сортируем ответы по дате
            dataEntries.sort(function(a, b) {
                return b.createdAt.valueOf() - a.createdAt.valueOf();
            });

            // Формируем BEMJSON из ответов с помощью BEMTREE шаблонов
            BEMTREE.apply(dataEntries.map(function(dataEntry) {
                dataEntry.createdAt = moment(dataEntry.createdAt).fromNow();
                return {
                    block: 'island',
                    data: dataEntry,
                    mods: { type: dataEntry.type }
                };
            }))
            .then(function(bemjson) {
                // Возвращаем отформатированный JSON
                res.end(JSON.stringify(bemjson, null, 4));
            });

        })
        .fail(function() {
            console.error(arguments);
        });
    });

    var server = app.listen(3000, function() {
        console.log('Listening on port %d', server.address().port);
    });

});

Скомпилированный BEMTREE-шаблон запускается в отдельном пространстве имен, куда прокидывается модуль vow, необходимый для работы шаблонизатора.

После того, как выполнятся все промисы, массив результатов склеивается в плоский список и сортируется по дате.

Затем в BEMTREE.apply() передается этот массив, каждый элемент которого преобразуется в объект с полями, описывающими БЭМ-сущность и данные, которые мы теперь можем использовать в наших BEMTREE-шаблонах.

Отредактируем файл ./desktop.blocks/island/_type/island_type_twitter.bemtree:

block('island').mod('type', 'twitter').content()(function() {
    var data = this.ctx.data;
    return [
        // и дальше без изменений
    ];
});

В this.ctx.data лежат данные, которые мы передали в BEMTREE.apply().

Пересоберем проекта и снова откроем страницу http://localhost:3000/search?query=%23b_&twitter=on. В браузере должен отображаться BEMJSON, сформированный с помощью BEMTREE.

Осталось преобразовать BEMJSON в HTML с помощью BEMHTML.apply(). Для этого добавим в server.node.js следующий код:

var BEMHTML = require(PATH.join('../../' + pathToBundle, 'index.bemhtml.js')).BEMHTML;
//…
BEMTREE.apply(dataEntries.map(function(dataEntry) {
    dataEntry.createdAt = moment(dataEntry.createdAt).fromNow();
    return {
        block: 'island',
        data: dataEntry,
        mods: { type: dataEntry.type }
    };
}))
.then(function(bemjson) {
    if (searchObj.json) {
        return res.end(JSON.stringify(bemjson, null, 4));
    }
    res.end(BEMHTML.apply(bemjson));
});
//…

Если обновить нашу страницу в браузере, мы получим HTML, который и будем в дальнейшем использовать на клиенте — подгружать с помощью AJAX.

Если использовать ключ json=on — откроется содержимое BEMJSON-файла — http://localhost:3000/search?query=%23b_&twitter=on&json=on.



Клиентский JavaScript с i-bem.js


Для декларативной работы с JavaScript в Яндексе написали специализированный JavaScript-фреймворк для веб-разработки в рамках БЭМ-методологии – i-bem.js. Он является частью bem-core. i-bem.js — это реализация блока i-bem в технологии js. Он позволяет делать другие блоки и использует jQuery для нормализации API браузеров.

О том, что такое i-bem.js и как он работает можно прочитать в подробном Руководстве пользователя.

Что мы получаем от использования этого фреймворка:
  • хелперы для работы с предметной областью БЭМ;
  • декларативность;
  • возможность доопределения блоков.

Блоки с js-представлением


Блоки бывают как с js-представлением, так и без него. Для того, чтобы указать, что блок имеет js-представление, в BEMHTML используется мода js, а в BEMJSON — поле js:

// bemhtml
block('form').js()(true);

// bemjson
{
    block: 'form',
    js: true
}

// bemjson with js params
{
    block: 'form',
    js: {
        p1: 'v1',
        p2: 'v2'
    }
}

Поле js позволяет использовать как булевы значения, так и объект параметров, которые можно будет использовать при написании js-реализации блока. Наш пример будет отрендерен в подобный HTML:

<div class="form i-bem" data-bem="{form: {p1: 'v1', p2 : 'v2'}}"></div>

Класс i-bem говорит о том, что на этом узле DOM-дерева есть блок с js-представлением. А в дата-атрибуте data-bem
передается объект, ключами которого являются имена блоков с js-представлением, а значениями — параметры, передаваемые этим блокам.

Пишем клиентский js


Блок form


Создадим файл ./desktop.blocks/form/form.js и опишем минимальную функциональность:

modules.define('form', ['i-bem__dom'], function(provide, BEMDOM) {

provide(BEMDOM.decl(this.name, {
    onSetMod: {
        js: {
            inited: function() {
                this.bindTo('submit', this._onSubmit);
            }
        }
    },

    _onSubmit: function(e) {
        e.preventDefault();
        this.emit('submit');
    },

    getVal: function() {
        return this.domElem.serialize();
    }
}));

});

В bem-core все блоки объявляются как модули. i-bem — это ядро фреймворка. i-bem__dom — доопределение ядра, отвечающее за работу с DOM браузера. Мы объявили модуль form, в зависимости которого добавили модуль i-bem__dom, поскольку блок будет иметь DOM-представление. Этот модуль будет передан в коллбэк как объект BEMDOM. С его помощью мы декларируем блок form. Своего рода конструктором нашего блока будет служить функция, вызываемая в момент установки модификатора js в значение inited — он будет установлен автоматически благодаря i-bem.js. Кроме того, у нашего блока есть приватный обработчик _onSubmit, отвечающий за реакцию на отправку формы, и публичный метод getVal, который возвращает результат сериализации формы.

В методе _onSubmit() мы вызываем e.preventDefault(), чтобы избежать перезагрузки страницы и после этого генерируем БЭМ-событие submit, которое в дальнейшем будет использоваться в коде других блоков. Таким образом мы только что создали публичное API блока form. Оно состоит из публичного метода и БЭМ-события.

Блок sssr


Теперь создадим блок, который будет загружать запрашиваемые данные и отображать их на странице.
./desktop.blocks/sssr/sssr.js:

modules.define('sssr', ['i-bem__dom', 'jquery'], function(provide, BEMDOM, $) {

provide(BEMDOM.decl(this.name, {
    onSetMod: {
        js: {
            inited: function() {
                this.findBlockInside('form').on('submit', this._sendRequest, this);
            }
        }
    },
    _sendRequest: function() {
        $.ajax({
            type: 'GET',
            dataType: 'html',
            cache: false,
            url: '/search/',
            data: this.findBlockInside('form').getVal(),
            success: this._onSuccess.bind(this)
        });
    },
    _onSuccess: function(html) {
        BEMDOM.update(this.elem('content'), html);
    }
}));

});

Пройдемся по коду блока. В начале мы объявили модуль sssr с зависимостями от i-bem__dom, поскольку блок имеет DOM-представление, и jquery для работы с AJAX.
В момент инициализации блока мы подписываемся на событие submit блока form. При возникновении этого события выполняется приватный метод _sendRequest, отправляющий AJAX-запрос. Когда ответ от сервера будет получен, выполнится обработчик _onSuccess, который обновит содержимое элемента sssr__content полученными результатами.

Остается создать шаблон, который подскажет i-bem.js, что у блока sssr есть js-представление:

// desktop.blocks/sssr/sssr.bemhtml

block('sssr').js()(true);

Итак, мы получили первую, пока очень примитивную и недоработанную версию нашего приложения. Для его запуска нужно собрать файлы с помощью нашего сборщика и запустить файл index.node.js из собранного бандла:

$ enb make && node ./desktop.bundles/index/index.node.js

Теперь мы можем протестировать его работу. Для этого перейдем на страницу localhost:3000, введем что-нибудь в поле ввода, отметим нужные чекбоксы и попробуем отправить форму. Если все сделано верно, то под шапкой мы увидим результаты поиска по заданному запросу.



Как вы видите у нас не загрузилась статика, потому что для сервера пути до картинок неизвестны. Чтобы это исправить нам нужно зафризить картинки с помощью borschik. Для этого добавим файл конфигурации .borschik в корень нашего проекта:

{
    "freeze_paths": {
        "libs/**": ":base64:",
        "libs/**": ":encodeURIComponent:"
    }
}

И запустим сборку в режиме production:

> YENV=production enb make && node desktop.bundles/index/index.node.js

Отрыв страницу в браузере мы можем убедиться что картинки на странице заработали.



Добавим интерактивности. Блок spin


После нажатия на кнопку отправки формы у нас происходит какое-то действие, однако оно незаметно. Создается ощущение, что сервис «завис». Давайте исправим это и добавим блок spin, который будет служить индикатором процесса отправки запроса. Он уже есть в нашей BEMJSON-декларации. Исходный код блока находится в библиотеке bem-components и имеет собственное API. Протестируем его работу из консоли браузера:

modules.require(['jquery'], function($) {
    $('.spin').bem('spin').setMod('visible');
});




Мы выставили булевый модификатор spin_visible в значение true и должны увидеть вращающийся спинер рядом с полем ввода.

Этот хак допустим для тестирования, но использовать его в js-коде блоков не стоит.

Добавим стили для этого блока в файл ./desktop.blocks/sssr/sssr.styl:

.sssr
{
    .spin
    {
        margin-left: 1em;
        vertical-align: middle;
    }
}

Сделаем так, чтобы индикатор загрузки показывался программно. Отредактируем ./desktop.blocks/sssr/sssr.js:

modules.define('sssr', ['i-bem__dom', 'jquery'], function(provide, BEMDOM, $) {

    provide(BEMDOM.decl(this.name, {
        onSetMod: {
            js: {
                inited: function() {
                    this.findBlockInside('form').on('submit', this._doRequest, this);
                }
            },
            loading: function(modName, modVal) {
                console.log('visible: ', modVal);
                this.findBlockInside('spin').setMod('visible', modVal);
            }
        },

        // …

        _doRequest: function() {
            this.setMod('loading');
            this._sendRequest();
        },

        _onSuccess: function(html) {
            this.delMod('loading');
            BEMDOM.update(this.elem('content'), html);
        }
    }))
})

На одни и те же модификаторы можно повесить как JS-функциональность, так и CSS-правила стилевого оформления. Давайте сделаем так, чтобы содержимое страницы затенялось, пока идет загрузка. Для этого отредактируем ./desktop.bundles/sssr/sssr.styl:

.sssr
{
    .spin
    {
        margin-left: 1em;
        vertical-align: middle;
    }

    &_loading .content
    {
        opacity: 0.5;
    }
}

Протестируем наше приложение: localhost:3000. Во время отправки запроса и загрузки данных
должен показываться блок spin, а содержимое страницы — затеняться.



Проверка полей формы


Сейчас, если оставить пустое поле ввода и убрать чекбоксы сервисов, форма все равно отправится. Давайте изменим это поведение и добавим метод isEmpty():
./desktop.blocks/form/form.js:

isEmpty: function() {
    return !this.findBlockInside('input').getVal().trim() ||
        this.findBlocksInside('checkbox').every(function(checkbox) {
            return !checkbox.hasMod('checked');
        });
}

Мы проверяем значение поля input и модификатор checkbox_checked и возвращяем результат проверки.
Теперь нужно добавить проверку, которую мы только что написали, в блок sssr перед отправкой запроса:
./desktop.blocks/sssr/sssr.js:

modules.define('sssr', ['i-bem__dom', 'jquery'], function(provide, BEMDOM, $) {

    provide(BEMDOM.decl(this.name, {
        onSetMod: {
            js: {
                inited: function() {
                    this.findBlockInside('form').on('submit', this._doRequest, this);
                }
            },
            loading: function(modName, modVal) {
                this.findBlockInside('spin').setMod('visible', modVal);
            }
        },

    _doRequest: function() {
        if (this.findBlockInside('form').isEmpty()) {
            return;
        }
        this.setMod('loading');
        this._sendRequest();
    },

    _sendRequest: function() {
        //…

})

Мы добавили в _doRequest() дополнительную проверку формы на заполненность полей ввода.

Сделаем так, чтобы форма не отправлялась повторно, если запрос уже идет. Для этого перепишем метод _sendRequest() и добавим методы clear() и _updateContent().

./desktop.blocks/sssr/sssr.js:

modules.define('sssr', ['i-bem__dom', 'jquery'], function(provide, BEMDOM, $) {

    provide(BEMDOM.decl(this.name, {
        onSetMod: {
            js: {
                inited: function() {
                    this.findBlockInside('form').on('submit', this._doRequest, this);
                }
            },
            loading: function(modName, modVal) {
                this.findBlockInside('spin').setMod('visible', modVal);
            }
        },

    _doRequest: function() {
        if (this.findBlockInside('form').isEmpty()) {
            return;
        }
        this.setMod('loading');
        this._sendRequest();
    },

    clear: function() {
        this._xhr && this._xhr.abort();
        this._updateContent('');
        this.delMod('loading');
    },

    _sendRequest: function() {
        this._xhr && this._xhr.abort();
        this._xhr = $.ajax({
            type: 'GET',
            dataType: 'html',
            cache: false,
            url: '/search/',
            data: this.findBlockInside('form').getVal(),
            success: this._onSuccess.bind(this)
        });
    },

    _onSuccess: function(result) {
        this.delMod('loading');
        this._updateContent(result);
    },

    _updateContent: function(html) {
        BEMDOM.update(this.elem('content'), html);
    }
}));
})

Автообновление при изменении полей ввода


Давайте сделаем так, чтобы при изменении поискового запроса или чекбоксов, наш сервис сам отправлял запрос и
обновлял содержимое. Для этого отредактируем блок form и добавим обработчик события change на блоке input:

modules.define('form', ['i-bem__dom'], function(provide, BEMDOM) {

    provide(BEMDOM.decl(this.name, {
        onSetMod: {
            js: {
                inited: function() {
                    this.bindTo('submit', this._onSubmit);
                    this.findBlockInside('input').on('change', this._onChange, this);
                }
            }
        },

        _onChange: function() {
            this.emit('change');
        },

        // …
})

Это событие change мы будем слушать в блоке sssr, для этого отредактируем файл ./desktop.blocks/sssr.js:

modules.define('sssr', ['i-bem__dom', 'jquery'], function(provide, BEMDOM, $) {

    provide(BEMDOM.decl(this.name, {
        onSetMod: {
            js: {
                inited: function() {
                    this.findBlockInside('form').on('submit change', this._doRequest, this);
                }
            },
    // …
}));
})

Добавим подобный обработчик на изменения чекбоксов, для этого отредактируем файл ./desktop.blocks/form.js:

modules.define('form', ['i-bem__dom'], function(provide, BEMDOM) {

    provide(BEMDOM.decl(this.name, {
        onSetMod: {
            js: {
                inited: function() {
                    this.bindTo('submit', this._onSubmit);
                    this.findBlockInside('input').on('change', this._onChange, this);
                    BEMDOM.blocks.checkbox.on(this.domElem, 'change', this._onChange, this);
                }
            }
        },

        // …
})

Можно проверить отправку запросов при изменении формы с помощью консоли браузера:



Сейчас при вводе слова мы можем отправить множество лишних запросов после ввода каждого нового символа. Давайте сделаем задержку на отправку запроса. Для этого воспользуемся модулем debounce из пакета bem-core. Добавим его в зависимости к блоку sssr в файле sssr.deps.js:

({
    shouldDeps: [
        { block: 'server' },
        { block: 'island', mods: { type: ['twitter'] }},
        {
            block: 'functions',
            elem: 'debounce'
        }
    ]
})

И добавим метод-обертку для задержанной отправки запроса. Обратите внимание, что мы добавили functions__debounce в зависимости модульной системы и получаем его в виде debounce:

modules.define('sssr', ['i-bem__dom', 'jquery', 'functions__debounce'], function(provide, BEMDOM, $, debounce) {

provide(BEMDOM.decl(this.name, {
    onSetMod: {
        js: {
            inited: function() {
                this.findBlockInside('form').on('submit change', this._doRequest, this);
                this._debounceRequest = debounce(this._sendRequest, 500, this);
            }
        },
        loading: function(modName, modVal) {
            this.findBlockInside('spin').setMod('visible', modVal);
        }
    },

    _doRequest: function(e) {
        this.setMod('loading');
        if (this.findBlockInside('form').isEmpty()) {
            this._clear();
            return;
        }
        e.type === 'change' ? this._debounceRequest(): this._sendRequest();
    },

    _clear: function() {
        this._xhr && this._xhr.abort();
        this._updateContent('');
        this.delMod('loading');
    },

    _sendRequest: function() {
        this._xhr && this._xhr.abort();
        this._xhr = $.ajax({
            type: 'GET',
            dataType: 'html',
            cache: false,
            url: '/search/',
            data: this.findBlockInside('form').getVal(),
            success: this._onSuccess.bind(this)
        });
    },

    _onSuccess: function(result) {
        this.delMod('loading');
        this._updateContent(result);
    },

    _updateContent: function(html) {
        BEMDOM.update(this.elem('content'), html);
    }

}));

});

Открыв страницу приложения в браузере мы можем убедиться, что запросы уходят с задержкой. Это хорошо — мы оптимизировали код и уменьшили количество запросов к серверу.

Автообновление


Добавим в наше приложение возможность автообновления по заданному временному интервалу. Будем включать автообновление с помощью модификатора блока sssr и передавать параметры в объекте params.

index.bemjson.js:

{
    block: 'sssr',
    mods: { autorefresh: true },
    js: {
        url: '/search/',
        refreshInterval: 10000
    },
    // ...
}

Добавим необходимые файлы для модификатора блока: ./desktop.blocks/sssr/_autorefresh/sssr_autorefresh.js:

modules.define('sssr', ['tick'], function(provide, tick, Sssr) {
    provide(Sssr.decl({ modName: 'autorefresh' }, {
        onSetMod: {
            loading: function(modName, modVal) {
                // призовем методы блока
                this.__base.apply(this, arguments);
                // если происходит загрузка — обнуляем,
                // когда загрузка закончится — стартуем таймер
                modVal ? this._clearTimer(): this._setTimer();
            }
        },
        _setTimer: function() {
            this._counter = 0;
            tick.on('tick', this._onTick, this);
        },
        _onTick: function() {
            // проверяем время и отсылаем вовремя запрос
            (++this._counter * 50) % this.params.refreshInterval || this._sendRequest();
        },
        _clearTimer: function() {
            tick.un('tick', this._onTick, this);
        },
        getDefaultParams: function() {
            return {
                refreshInterval: 10000
            };
        }
    }));
});

Мы воспользовались this.__base для доопределения модификатора sssr_loading. Модуль tick позволяет нам выполнять нужные действия с заданным интервалом. Один цикл tick составляет 50 мс. Проверяя значение модификатора sssr_loading, мы либо очищаем таймер, либо устанавливаем его.

Значение refreshInterval берется из параметров блока sssr, если они указаны. Мы можем указать значения по умолчанию в методе getDefaultParams. При отстутствии значения в параметрах блока оно будет браться из объекта, возвращаемого этим методом.

Осталось дописать файл с зависимостями блока sssr. Отредактируем desktop.blocks/sssr/sssr.deps.js:

({
    shouldDeps: [
        'server',
        {
            block: 'functions',
            elem: 'debounce'
        },
        {
            block: 'island',
            mods: { type: ['twitter'] }
        }
    ]
})

Давайте протестируем наше приложение. Оно должно обновляться каждые 10 секунд.

Оптимизация и рефакторинг


Кэширование поиска блоков


Если наши блоки не заменяются динамически в ходе работы, мы можем закэшировать их в памяти, чтобы не запускать поиск по
блокам каждый раз.
Результат выполнения выражения this.findBlockInside('form') можно сохранить в переменную this._form и обращаться к ней. Тоже самое можно сделать и для блока spin.
То есть мы закешировали все необходимые блоки момент инициализации блока sssr и можем ими пользоваться в дельнейшей работе, не запуская каждый раз лишние операции поиска.

modules.define('sssr', ['i-bem__dom', 'jquery', 'functions__debounce'], function(provide, BEMDOM, $, debounce) {

provide(BEMDOM.decl(this.name, {
    onSetMod: {
        js: {
            inited: function() {
                this._spin = this.findBlockInside('spin');
                this._form = this.findBlockInside('form')
                    .on('submit', this._doRequest, this);
                this._debounceRequest = debounce(this._sendRequest, 500, this);
            }
        },
        loading: function(modName, modVal) {
            this._spin.setMod('visible', modVal);
        }
    },

    _doRequest: function(e) {
        this.setMod('loading');
        if (this._form.isEmpty()) {
            this._clear();
            return;
        }
        e.type === 'change' ? this._debounceRequest(): this._sendRequest();
    },

    _clear: function() {
        this._abortRequest();
        this._updateContent('');
        this.delMod('loading');
    },

    _abortRequest: function() {
        this._xhr && this._xhr.abort();
    },

    _sendRequest: function() {
        this._abortRequest();
        this._xhr = $.ajax({
            type: 'GET',
            dataType: 'html',
            cache: false,
            url: this.params.url,
            data: this._form.getVal(),
            success: this._onSuccess.bind(this)
        });
    },

    _onSuccess: function(result) {
        this.delMod('loading');
        this._updateContent(result);
    },

    _updateContent: function(result) {
        BEMDOM.update(this.elem('content'), result);
    }

}));

});

Кроме того, мы вынесли повторяющийся код, совершающий отмену запроса, в отдельный метод _abortRequest().

Отложенная инициализация


Если на странице находится много блоков, автоматическая их инициализация может привести к увеличению времени загрузки и объема памяти, которую потребляет браузер.
Вместо этого мы можем воспользоваться инициализацией блоков по событию. Такая инициализация называется ленивой или live-инициализацией. Подробное описание ленивой инициализации в документации по i-bem.js.

В нашем случае нет небоходимости инициализировать блоки sssr и form сразу, они могут быть проинициализированы по требованию. Давайте выразим это в коде:
./desktop.blocks/sssr/sssr.js:

modules.define('sssr', ['i-bem__dom', 'jquery', 'functions__debounce'], function(provide, BEMDOM, $, debounce) {

provide(BEMDOM.decl(this.name, {

    onSetMod: {
        js: {
            inited: function() {
                this._form = this.findBlockInside('form');
                this._spin = this.findBlockInside('spin');
                this._debounceRequest = debounce(this._sendRequest, 500, this);
            }
        },

        loading: function(modName, modVal) {
            this._spin.setMod('visible', modVal);
        }
    },

    _clear: function() {
        this._abortRequest();
        this._updateContent('');
        this.delMod('loading');
    },

    _doRequest: function(needDebounce) {
        if (this._form.isEmpty()) {
            this._clear();
            return;
        }
        this.setMod('loading');
        needDebounce? this._debounceRequest() : this._sendRequest();
    },

    _sendRequest: function() {
        this._abortRequest();

        this._xhr = $.ajax({
            type: 'GET',
            dataType: 'html',
            url: this.params.url,
            data: this._form.getVal(),
            cache: false,
            success: this._onSuccess.bind(this)
        });
    },

    _abortRequest: function() {
        this._xhr && this._xhr.abort();
    },

    _onSuccess: function(result) {
        this._updateContent(result);
        this.delMod('loading');
    },

    _updateContent: function(html) {
        BEMDOM.update(this.elem('content'), html);
    }

}, {

    live: function() {
        this.liveInitOnBlockInsideEvent('submit change', 'form', function(e) {
            this._doRequest(e.type === 'change');
        });
    }

}));

});

Добавим live-инициализацию к блоку form:
./desktop.blocks/form/form.js:

modules.define('form', ['i-bem__dom'], function(provide, BEMDOM) {

provide(BEMDOM.decl(this.name, {

    onSetMod: {

        js: {
            inited: function() {
                this._input = this.findBlockInside('input');
                this._checkboxes = this.findBlocksInside('checkbox');
            }
        }

    },

    // …

    isEmpty: function() {
        return !this._input.getVal().trim() ||
            this._checkboxes.every(function(checkbox) {
                return !checkbox.hasMod('checked');
            });
    }

}, {

    live: function() {
        var ptp = this.prototype;

        this
            .liveBindTo('submit', ptp._onSubmit)
            .liveInitOnBlockInsideEvent('change', 'input', ptp._onChange)
            .liveInitOnBlockInsideEvent('change', 'checkbox', ptp._onChange);
    }

}));

});

Закэшировав результаты поиска блоков
input и checkbox, мы улучшили быстродействие, избавившись от лишних операций findBlockInside.

В результате


Мы собрали поисковый агрегатор по социальным сервисам, используя полный стек БЭМ-технологий. Мы попробовали разобраться в технологиях от двухэтапной шаблонизации на сервере до клиентского фреймворка i-bem.js, познакомились с шаблонизаторами BEMTREE для построения БЭМ-дерева и BEMHTML для преобразования БЭМ-дерева в HTML. В репозитории sssr есть примеры реализации блоков service__type__* с использованием API Instagram и Яндекс.Фото. Надеемся, что эта статья будет для вас полезной и поможет по шагам пройти весь путь написания проекта. Мы постарались сделать ее простой и понятной, чтобы читателю было легче самому разобраться с нашими технологиями и попробовать их в проекте.

Мы с радостью выслушаем ваше мнение о материале и готовы помочь вам там, где вы все еще испытываете трудности. Комментарии и предложения ждем по электронной почте info@bem.info.

Яндекс

408,68

Как мы делаем Яндекс

Поделиться публикацией
Комментарии 36
    +11
    Ах, вы ж мои зайки! Спасибо огромное! Я уже долгое время присматриваюсь к БЕМ, но собирать документацию по-кусочкам не было времени. А здесь все вместе! Однозначно, в избранное!
      +3
      На здоровье. Рады что пригодилось.
      +16
      Странное впечатление оставляет текст. С одной стороны, вроде всего много, а с другой — смотришь в каждый раздел и думаешь: «а почему здесь именно это? почему тут не bemhtml, а bemjson? они взаимозаменяемы?».
      Нет какой-то общей карты, что-ли, типа: «вот bem, у него есть следующие плюшки:
      • bemjson — может то, то и то. Лучше использовать там-то
      • bemhtml — ...
      »
      Всё как-то надёргано, разрознено, непонятно за что браться и с какой стороны подходить. Опять же, почему enb, а не bem-tools? Какие части bem можно безболезненно исключить из использования?
      Как можно встроить БЭМ в текущий проект?
      И картинки отвалились :(
        +3
        Картинки поправил. Благодарю.

        • bemjson — это абстракция над DOM-деревом, описывает BEM-дерево. Позволяет уйти от использования html при описании интерфейса.
        • bemhtml — это шаблонизатор, преобразует bemjson в html


        Кажется что все это в тексте есть.
          +11
          Вы технологию хорошо знаете, поэтому вам всё очевидно. Я много про БЭМ читал, и на оф. сайте, и на хабре, и в клубе яндекса, вроде. И с самого начала не хватало объяснений что это даёт рядовому разработчику.
          Плюс видно, что подход строился грамотно, всё разбито на модули, всё слабосвязано, но документация, бррр.
          Вот например, открываю про BEMJSON (https://ru.bem.info/technology/bemjson/v2/bemjson/). Пытаюсь понять, что вообще такое BEMJSON. Нет определения. Листаю ниже — опять нет. А, во, нашёл, в конце раздела «общие понятия». Почему оно спрятано? От кого? То же самое в разделе про BEMTREE. В BEMHTML определение вообще вынесено на страницу 2 уровня.
          Плюс в документах с самого начала прописывается ограничение: «Предполагается что читатель знаком с БЭМ». Ну, если я читаю про БЭМ, наверное я с ним не знаком.

          Было бы круто, если бы появилась какая-то вводная статья, которая просто рассказывает про то, что такое БЭМ, из чего он состоит (шаблонизаторы, определённая организация файловой системы, преобразование исходников (прогон через borshik и csso), сервер разработки, библиотека компонентов [надеюсь я ничего не забыл]), какие даёт преимущества и что позволяет достичь.
            +3
            Спасибо. Ваш комментарий обязательно учтем в подготовке будущих материалов. Пока что можем предложить чаще заглядывать на ru.bem.info, задавать вопросы на форуме — мы стараемся часто отвечать и все все разъяснять, подписаться на наш твиттер, если вам удобно читать новости в твиттере. Как только у нас выйдет что-то похожее, вы будете в курсе.
              +3
              тоже также не мог ничего понять из документации, возвращался к ней много раз, пока не начал чего то пробовать сделать. понимаю приходило в процессе. теперь мне документация кажется понятной и логичной.
                +3
                И мне было трудно в начале, постоянно обращался к документации. И сейчас продолжаю. С опытом стал лучше понимать процесс. Пробуйте. Если будут вопросы, пишите на ru.bem.info/forum. Мы постоянно его мониторим и обязательно ответим и поможем.
          +23
          Очень ловкое вступление: BBC использует БЭМ! А сейчас мы вам расскажем как сделать сайт на полном БЭМ-стэке. Правда BBC использовала от БЭМа только именование классов и ни кусочка стэка. При всём уважении к автору, статье и БЭМу.
            +1
            В качестве примеров более полного использования БЭМ-стека можно привести Мегафон и Альфа-Лабораторию.
            +2
            Хорошо помог разобраться вебинар
            https://ru.bem.info/forum/issues/193/

            За статью — Спасибо!
              +2
              Было бы интересно услышать об опыте применения БЭМ от тех, кто специализируется на небольших и средних проектах. Особенно интересует какие конкретные проблемы помог решить/создать БЭМ в вашем проекте.
                +2
                  +1
                  Судя по тому, что у меня хватило сил посмотреть, у них была проблема в полной отсутствии методологии. А может ли БЭМ помочь тем, кто уже использует какой-то препроцессор, не кидает весь код css в один файл и знает о том как разбивать js на модули?

                  Просто например bemhtml мне кажется довольно специфичным — если у меня и есть какие-то блоки(по терминологии БЭМ) в проекте, я не могу понять какие преимущества он несет по сравнению со старым подходом, когда какой-то реюзабельный элемент UI отправлялся в отдельный файл, который инклудился в нужных местах с нужными параметрами.
                    +2
                    В то время как все это начиналось у нас, кажется у всех небольших компаний была такая же ситуация — с отсутствием всего хорошего в разработке. В докладе я старался упоминать года, что бы был какой то тайм лайн.

                    Сейчас конечно все по другому. Модульный подход сегодня прослеживается во всем и уже считается нормой. Но, по моему опыту и судя по проблемам которые мне описывают люди желающие перейти на БЭМ стек, его отличает от всего, как минимум, очень гибкий и крутой шаблонизатор, который реально позволяет максимально реиспользовать код блока, на множестве проектов. Как максимум — на самом деле в БЭМ стеке целый набор всего с помощью чего кажется можно сделать все. Я не могу подобрать слова что бы оправдать «максимально реиспользовать», но в докладе был описан опыт использования другого шаблонизатора и те проблемы с которыми мы столкнулись. Я уверен что у большинства небольших компаний/веб студий у которых есть хоть какой то поток производства схожих продуктов, есть эти проблемы. Может быть аналогией крутости будет, что-то типа «блок написаный в БЭМ стеке — это как Bower пакет, только включающий в себя все технологии нужные в реализации этого блока. Причем блок можно легко переопределять/дописать не меняя базовый код». %) (Надеюсь мою аналогию ребята из яндекса поправлять или дополнят). Вот в наших процессах так же легко реиспользовать код другими подходами не получалось.

                    Ну про «декларативность, все написано в едином стиле, код легко передавать другим разработчикам и тд.» уже все наслышаны я думаю :).

                    Просто начни рассуждать как может изменяться/трансформироваться твой UI элемент если ты его захочешь реиспользовать на разных частях сайта или разных проектах.
                      0
                      А в режиме не-конвеера инвестиции в изучение БЭМ окупаются? Вот скажем небольшая команда — до 5 человек. Сайты делаются за 3 месяца-год. Повторное использование компонентов маловероятно — сегодня пишем систему документооборота, завтра сайт агентства недвижимости, послезавтра агрегатор статистики с каких-то сервисов. Причем как правило требования к дизайну либо отсутствуют вообще или заказчик предоставляет свой дизайн. На какие части БЭМ советуете обратить внимание в таком случае?
                        +2
                        Конечно окупятся. Как в любое другое про методологию, стандартизацию и тд. В любом случае в ваше коде есть компоненты которые хочется реиспользовать. Взять хотя бы bem-components, это же все нужно на любом сайте. Можно еще придумать что же общего в продуктах что вы делаете — меню, пагинатор, слайдеры, соц виджеты тоже можно представить как блок для этого есть даже либа, css сетка (причем она с js функциональностью), редактор текста можно завернуть блок и удобно подключать на проект, можно завернуть Яндекс карты в блок сделать тебе удобные методы для работы с ними, или есть у тебя в препроцессор css используется какие нибудь функции для easing’гов — можно написать и для этого блок. Причем если представить это все как библиотеку блоков которая будет подключаться к новому проекту — в сборку попадает только то что нужно. А на уровне нового проекта совсем не нужно писать код заново, ну может только стили css поправить.

                        Ну и даже в команде из 5 сотрудников есть тянучка кадров, я думаю команде и начальству понравится что старый код с приходом нового сотрудника будет не забыт. Методология проста и сейчас всем понятно зачем это все нужно.
                          0
                          Спасибо за развернутый ответ.
                          +1
                          И я всегда, когда рассказываю про плюсы, забываю рассказать о том что идет очень много всего «из коробки» вместе со стеком БЭМ, очень нужные вещи в любом процессе — минификации css/js, автопрефиксер, фриз статики… В общем все это описано в небольшом конфиге. Этот конфиг, практически без изменений, у нас используетя во всех проектах — от лендингов до больших сервисов.
                  +2
                  И ещё, в вдогонку. Напишите, если есть возможность, как интегрировать БЭМ в уже существующий проект? Насколько я понимаю, в Яндексе это уже проходили.
                  Просто есть много проектов, которые разрабатываются уже N месяцев/лет и на которых можно поэкспериментировать, потихоньку, аккуратно переводя их на новую технологию и смотря её применимость. А разрабатывать проект с 0 на том, в чём ты не особо понимаешь рискованно. Можно и сроки сорвать и репутацию испортить.
                  Или БЭМ подходит только если весь проект на нём?
                    +1
                    Я бы порекомендовал сгенерировать себе проект, с помощью generator-bem-stub и начать его использовать в качестве станка для вёрстки и быстрого прототипирования. Начать с bemjson и css, постепенно привыкнуть к i-bem и шаблонизаторам. Это не требует внесения изменения ваших проектов. Со временем вы сами поймёте, что и на сколько применимо в вашем случае.
                      +1
                      Спасибо
                        +1
                        Зачем вводить в проект еще один шаблонизатор?
                        Имеет смысл писать с нуля, лапша не нужна.
                          0
                          Для начала весь текущий код можно положить в один большой блок page, а новые писать отдельно по блокам. Затем, для безболезненной интеграции, я бы начал весь CSS код разносить по блокам (по отдельным папочкам на файловой системе). Для сборки взял бы любой удобный сборщик (gulp, grunt, enb). Тоже самое с JS-кодом.
                          БЭМ — это методология, предлагающая пользоваться независимыми компонентами. Не обязательно пользоваться инструментами, чтобы использовать БЭМ. Если у вас интерфейс разбит на независимые блоки — это уже БЭМ.
                          +10
                          Я безнадежно отстал от жизни и до сих пор верстаю на чистом HTML…
                            0
                            Несколько попыток приобщиться к БЭМу не увенчались успехом. Не могу принять его за избыточность и из за конфликта с самим собой. Теория завораживает, практика раздражает.
                              0
                              Мне проблема видится в том, что если вы уже и так знаете как решать основные задачи и проблемы в верстке и фронтенде, то вы просто будете учиться делать то же самое просто по другому. И не факт что лучше — в конце концов ваши предпочтения были сформированы во много теми задачами, что вы решали, а значит они уже хороши потому что подходят вам.
                              0
                              А что за шрифт на скриншоте?
                              0
                              Сделал все в точности как в примерах, а вместо json отдается пустой массив. Что могло пойти не так? В консольной версии json прекрасно отдается.
                                0
                                Привет, выложи репозиторий с проектом, смогу посмотреть в чем причина.
                                  0
                                  А я разобрался уже. Я не очень внимателен был и в итоговый урл у меня попал символ & вместо &. Поэтому и отдавался пустой массив. Странно, что отдебажить его не смог, но сейчас уже всё в порядке, спасибо.
                                    +2
                                    0_о
                                    Какая разница между & и &?
                                      0
                                      Возможно, он имел в виду &amp; вместо &, либо наоборот, а хабрапарсер всё сломал.
                                +2
                                Рассмотрел внимательнее сайт ya.ru. а так же обратился к сайту ru.bem.info. Может ли автор уточнить несколько вопросов касательно верстки по БЭМу на примере собственного сайта ya.ru?

                                Например, 30% стилей содержат вложенные классы, далее цитата БЭМ «Методология БЭМ допускает использование таких селекторов, но рекомендует по-максимуму его сократить.». Скажите пожалуйста, какая НЕОБХОДИМОСТЬ была использовать так часто вложенные селекторы на довольно простой странице?

                                Например:
                                :link, .b-widget__title *:visited, .link:visited, .link__search:visited {
                                text-decoration: none;
                                }
                                .i-ua_interaction_yes[data-interaction=«pointer»] .link, .i-ua_interaction_yes[data-interaction=«pointer»] .link:focus {
                                outline: medium none;
                                }
                                :link, .b-widget__title *:visited, .link:visited, .link__search:visited {
                                color: #0d44a0;
                                }


                                Далее, присуствуют элементы, стили которых описаны через аттрибут style:



                                Как вы можете объснить подобные вещи в собственной верстке НЕ ПО БЕМу?

                                И наконец, вопрос «Можно ли объединять тег и класс в селекторе? Например, button.button», никакого конкретного ответа на сайте ru.bem.info нет, но при этом на сайте ya.ru теги используются для описания стилей:

                                a.button:link, a.button:visited, a.button:hover {
                                text-decoration: none;
                                }

                                .dump pre {
                                background-color: rgba(255, 255, 255, 0.8);
                                border-top: 3px solid rgba(0, 0, 0, 0.2);
                                font-family: «Monaco»,«Andale Mono»,«Lucida Console»,«Bitstream Vera Sans Mono»,«Courier New»,Courier,monospace;
                                line-height: 1.5;
                                padding: 10px;
                                }

                                wbr {
                                display: inline-block;
                                }

                                Знаете, после анализа стилей верстки автора БЭМа невольно задаешься вопросами, а использует ли сам автор верстку по БЭМу? и если это и есть верстка по БЭМу, то чем она отличается от верстки не по БЭМу?
                                  0
                                  Тот факт, что БЭМ изобрели в Яндексе не гарантирует, что все разработчики в любой момент времени используют его в полной мере.
                                  Причин может быть много. Например, разработчик недавно вышел на работу и еще не разобрался. Или проект старый, но работает и переписывать его просто из любви к искусству не хватает рук. И так далее.

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

                                  a.button:link, a.button:visited, a.button:hover {
                                      text-decoration: none;
                                  }
                                  


                                  Проект использует общую библиотеку блоков (со свойственным всем долгоживущим общим решениям количеством legacy), но оформление ссылок на ya.ru отличаются от их оформления на остальном портале. Здесь разработчики просто повышают вес селекторов.
                                  
                                  .dump pre {
                                      background-color: rgba(255, 255, 255, 0.8);
                                      border-top: 3px solid rgba(0, 0, 0, 0.2);
                                      font-family: «Monaco»,«Andale Mono»,«Lucida Console»,«Bitstream Vera Sans Mono»,«Courier New»,Courier,monospace;
                                      line-height: 1.5;
                                      padding: 10px;
                                  }
                                  
                                  wbr {
                                      display: inline-block;
                                  }
                                  


                                  Выглядит как стили для оформления отладочной информации, используемой в девелопменте. Думаю, странно предъявлять к ним какие-либо требования (другой вопрос, что если я прав на счет их предназначения, то их бы в идеале вообще вырезать при сборке в прод).

                                  В целом же ya.ru следует БЭМ-методологии гораздо ближе, чем ряд других сервисов Яндекса. Однако это обычное дело: все знают, что долго сидеть за компьютером и кушать жареное вредно, а заниматься спортом — полезно, но далеко не всегда следуют хорошим рекомендациям ;)

                                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                Самое читаемое