Pull to refresh
787.52
Яндекс
Как мы делаем Яндекс

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

Reading time 29 min
Views 107K
На прошлой неделе 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.
Tags:
Hubs:
+62
Comments 37
Comments Comments 37

Articles

Information

Website
www.ya.ru
Registered
Founded
Employees
over 10,000 employees
Location
Россия