Сайт с нуля на полном стеке БЭМ-технологий. Методология Яндекса
На прошлой неделе 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: 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; } } Давайте посмотрим на результат:
Шаблонизатор 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 описывает набор и порядок прохождения остальных мод. На этой схеме видно, за что отвечает каждая мода:
Рекомендуем вдумчиво прочитать документацию по BEMHTML, описанную в Cправочном руководстве по шаблонизатору BEMHTML.
Вернемся к нашему проекту. Нам нужен блок form. Он должен отображаться как тег