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

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

682ce4f7b9504400a955f38f22c8dbcb.jpg

В статье мы расскажем, в чём преимущество вёрстки независимыми блоками и что такое уровни переопределения, познакомимся с готовыми библиотеками блоков и инструментами для автоматизации сборки. Покажем, как разные инструменты — например, 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 Отвечая на вопросы, касающиеся используемых технологий, получим собранную и сконфигурированную для сборки заготовку.Пройдемся по вопросам: 1-sssr-yo-bem-stub.png

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

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.Наш сборщик соберёт все необходимые зависимости, а по ним соберёт файлы нужных блоков и технологий.2-sssr-hello-world.png

Откройте инспектор в браузере и посмотрите на 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' } ] } 3-sssr-header.pngИспользуем блоки 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: 400 px; }

.checkbox { display: inline-block;

margin-left: 15 px;

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: 100 px;

background: #000; } Для блока sssr в файле ./desktop.blocks/sssr/sssr.styl: .sssr { &__header { position: fixed; z-index: 1; top: 0; box-sizing: border-box; width: 100%; padding: 10 px 10%; background: #f6f6f6; box-shadow: 0 0 0 1 px rgba (0,0,0,.1), 0 10 px 20 px -5 px rgba (0,0,0,.4);

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

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

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

a[rel='nofollow'], a[xhref], [name][server] { text-decoration: none; color: #038543; } } И для блока user — desktop.blocks/user/user.styl: .user { &__name { display: inline-block;

margin-right: 10 px;

text-decoration: none;

color: #000;

&: hover { text-decoration: underline;

color: #038543; } }

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

display: inline-block;

color: #8899a6; }

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

width: 30 px; height: 30 px;

border-radius: 3 px; } } Не будем останавливаться на вопросах 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: 18 px; line-height: 140%; position: relative; display: inline-block; box-sizing: border-box; width: 100%; margin-bottom: 15 px; padding: 15 px 5 px 5 px 15 px; border-radius: 3 px; background: #fff; box-shadow: inset 0 0 1 px rgba (0, 0, 0, .4);

&__footer { margin-top: 10 px; } &__image { display: block; width: 100%; border-radius: 3 px; } } Давайте посмотрим на результат: 4-sssr-mock.png

Шаблонизатор 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. Он должен отображаться как тег

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

block ('form')( tag ()('form'), js ()(true) ); Теперь мы можем редактировать шаблон блока в одном месте, переносить и реиспользовать этот блок с легкостью.Посмотрим на DOM-дерево в инспекторе — наш блок form теперь выводится как тег с классом i-bem. Этот класс говорит о том, что у блока есть реализация в JavaScript.

5-sssr-form-js.png

Мы описали то, как должны преобразовываться наши БЭМ-блоки в 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.

6-sssr-server-json.png

Теперь добавим преобразование этих данных в 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.

7-sssr-server-html.png

Клиентский 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:

Класс 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, введем что-нибудь в поле ввода, отметим нужные чекбоксы и попробуем отправить форму. Если все сделано верно, то под шапкой мы увидим результаты поиска по заданному запросу.8-sssr-server-ajax-no-static.png

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

{ «freeze_paths»: { «libs/**»:»: base64:», «libs/**»:»: encodeURIComponent:» } } И запустим сборку в режиме production: > YENV=production enb make && node desktop.bundles/index/index.node.js Отрыв страницу в браузере мы можем убедиться что картинки на странице заработали.8-sssr-server-ajax-static.png

Добавим интерактивности. Блок spin После нажатия на кнопку отправки формы у нас происходит какое-то действие, однако оно незаметно. Создается ощущение, что сервис «завис». Давайте исправим это и добавим блок spin, который будет служить индикатором процесса отправки запроса. Он уже есть в нашей BEMJSON-декларации. Исходный код блока находится в библиотеке bem-components и имеет собственное API. Протестируем его работу из консоли браузера: modules.require (['jquery'], function ($) { $('.spin').bem ('spin').setMod ('visible'); }); 9-sssr-server-spinner-test.png

Мы выставили булевый модификатор 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, а содержимое страницы — затеняться.10-sssr-server-spinner-mod.png

Проверка полей формы Сейчас, если оставить пустое поле ввода и убрать чекбоксы сервисов, форма все равно отправится. Давайте изменим это поведение и добавим метод 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 ('

© Habrahabr.ru