История создания БЭМ
Типичная верстка в Яндексе 2005 года
История БЭМ началась в 2005 году. Тогда, с точки зрения интерфейса, обычный проект Яндекса был набором статических HTML-страниц, которые использовались как основа для создания шаблонов на XSL.
HTML-страницы хранились в отдельной директории, которая имела подобную структуру:
about.html index.html … project.css project.js i/ yandex.png
Для каждой страницы создавался отдельный HTML-файл. В верстке использовались
id
иклассы
.Скрипты хранились в одном файле для всего проекта —
project.js
. JavaScript использовался как вспомогательный инструмент для оживления страницы, поэтомуproject.js
был небольшим.Картинки складывались в отдельную директорию. Необходимость поддержки IE 5 и отсутствие CSS3 в браузерах, вынуждало использовать картинки для реализации любого оформления, даже для закругленных уголков.
Стили и скрипты писались в файлах:
project.css
,project.js
. Для отделения стилей разных частей страницы использовались комментарии с указанием начала и конца:/* Content container (begin) */ #body { font: 0.8em Arial, sans-serif; margin: 0.5em 1.95% 0.5em 2%; } /* Content container (end) */ /* Graphical banner (begin) */ .banner { text-align: center; } /* Graphical banner (end) */
Сверстанные статические HTML-страницы нарезались в XSL-шаблоны. Если HTML изменялся, все правки было необходимо переносить вручную в XSL. И наоборот, изменения в шаблонах требовали правок в HTML (для поддержания статического HTML в актуальном состоянии).
Зарождение основ методологии
В 2006 году началась работа над первыми большими проектами - Яндекс.Музыка и Я.ру. Эти проекты с десятками страниц выявили основные недостатки текущего подхода к разработке:
невозможно внести изменения в код одной страницы, не затрагивая код другой;
сложно подбирать названия классам.
Типичный CSS того времени содержал длинный каскад:
/* Albums (begin) */
.result .albums .info
{
padding-right: 8.5em;
}
.result .albums .title
{
float: left;
padding-bottom: 0.3em;
}
.result .albums .album .listen
{
float: left;
padding: 0.3em 1em 0 1em;
}
/* Albums (end) */
Совместно использовались селекторы по тегам и идентификаторам:
/* Картинки на фоне (begin) */
#foot div
{
height: 71px;
background: transparent url(../i/foot-1.png) 4% 50% no-repeat;
}
#foot div div
{
background-position: 21%;
background-image: url(../i/foot-2.png);
}
#foot div div div
{
background-position: 38%;
background-image: url(../i/foot-3.png);
}
/* Картинки на фоне (end) */
Верстка большого проекта была неуправляемой. Чтобы избежать этого, нужно было определить правила работы с понятиями класса, тега, визуального компонента и др.
Появление блоков
Основное время разработчиков тратилось на создание HTML-структуры страницы и написание CSS-стилей для нее. JavaScript воспринимался лишь как сопутствующая технология.
Чтобы ускорить разработку, требовалось облегчить поддержку HTML и CSS отдельных компонентов страницы. Для этого мы ввели понятие блока.
Блоком называлась часть дизайна страницы или раскладки со своим специфическим и уникальным значением, определенным семантически или визуально.
В большинстве случаев любой компонент на странице (сложный или простой) рассматривался как блок. HTML-контейнер каждого блока получал уникальный CSS-класс с тем же именем, что и у блока.
Классам блоков мы добавили префиксы (b-
, c-
, g-
), чтобы отличать их от внутренних классов:
b- — block.
Независимый блок, может использоваться в любом месте страницы.
с- — control.
Контрол (независимый блок), с которым ассоциирован JavaScript-объект, обеспечивающий его функциональность. Может использоваться в любом месте страницы.
g- — global.
Глобальное определение, используется по необходимости. Количество сведено к минимуму.
Кроме префиксов использовались постфиксы, например:
-nojs — no javascript.
Стиль применяется в отсутствие JavaScript. Если JavaScript включен, то при загрузке страницы вызывается метод init()
в onload, и постфикс удаляется из всех классов. Таким образом «включался» JavaScript для блоков.
Появление элементов
В HTML-контейнере, формирующем блок, некоторые узлы получали четкий CSS-класс. Это не только облегчило создание стилистических правил, независящих от имени тега, но и позволяло присваивать семантически значимую роль каждому узлу. Такие внутренние узлы мы назвали элементами блока, или просто элементами.
Ключевое различие между блоком и элементом в тот момент:
элемент не может существовать вне контекста родительского блока;
из блока нельзя извлечь ни один элемент.
Если элемент способен существовать вне блока, он становится блоком.
Позже стало возможным вынимать некоторые элементы из блока, сохраняя при этом рабочее состояние самого блока.
Элементы с большим количеством кода выделялись комментариями.
/* Head (begin) */ .b-head { … } /* Logo (begin) */ .b-head .logo { … } .b-head .logo a { … } /* Logo (end) */ /* Right side (begin) */ .b-head .right { … } /* Info (begin) */ .b-head .info { … } .b-head .info .exit a { … } /* Info (end) */ /* Search (begin) */ .b-head .search { … } .b-head .search div div, .b-head .search div div i { … } /* Search (end) */ /* Right side (end) */ /* Head (end) */
Унификация файловой структуры проекта
Разработчики интерфейсов обычно поддерживают несколько проектов одновременно. Работать с разными проектами легче, если все они имеют одинаковую (или очень похожую) файловую структуру. Поэтому мы унифицировали структуры репозиториев разных проектов.
Начали с того, что CSS, JavaScript и картинки стали складывать в отдельные директории.
JavaScript применялся все чаще, в проект подключались дополнительные компоненты и библиотеки.
Типичная структура верстки проекта 2006 года:
index.html css/ yaru.css yaru-ie.css js/ yaru.js i/ yandex.png
Основной код для IE мы писали в общем CSS-файле, например, yaru.css
.
/* Common definitions (begin) */
body
{
font: 0.8em Arial, sans-serif;
padding: 0 0 2em 0;
background: #fff;
}
* html body
{
font-size: 80%;
}
Специфичные правила (временные решения), работающие только в IE, создавались в отдельном файле. В имя файла добавлялся специальный указатель ie
— yaru-ie.css
.
/* Common blocks (begin) */
/* Artist (begin) */
.b-artist .i i
{
top: expression(7 + (90 - this.parentNode.getElementsByTagName('img')[0].height)/2);
filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(src='../i/sticker-lt.png', sizingMethod='crop');
}
Зачатки общепортального фреймворка
При верстке нескольких проектов с похожим дизайном появлялись общие блоки.
Портал Яндекса в то время содержал больше 100 разных сервисов, выполненных в одном стиле. Для такого объема данных «Copy-Paste» из проекта в проект уже не подходил.
Появилось общее хранилище повторно используемых компонентов, которое называлось общая библиотека блоков или просто — Common
.
Первые блоки, которые вошли в Common
: шапка, подвал и стили для статического текста.
Файлы блоков хранились на выделенном внутреннем сервере разработчиков (common.cloudkill.yandex.ru в примере ниже).
Это было началом работы нашего общепортального фреймворка. Cтили из него подключались в основной проектный файл при помощи импортов непосредственно с сервера:
@import url(http://common.cloudkill.yandex.ru/css/global.css); @import url(http://common.cloudkill.yandex.ru/css/head/common.css); @import url(http://common.cloudkill.yandex.ru/css/static-text.css); @import url(http://common.cloudkill.yandex.ru/css/list/hlist.css); @import url(http://common.cloudkill.yandex.ru/css/list/hlist-middot.css); @import url(slider.css); /* Header (begin) */ /* Service (begin) */ .b-head .service h1 { … } .b-head .service h1, .b-head .service h1 a, .b-head .service h1 b { … }
Возникла проблема: большое количество импортов замедляло загрузку страницы. Было принято решение прекомпилировать стили (и позже JavaScript-файлы) перед выкладкой.
Компиляция заменяет @import
на содержимое внешних файлов (это называется inlining
) и оптимизирует код. Например, убирает ненужные браузеру пробелы и комментарии.
Наш внутренний инструмент для оптимизации вырос из простого Perl-скрипта в отдельный open-source-проект borschik.
Верстка независимыми блоками
К осени 2007 года правила верстки устоялись. Мы увидели практическую пользу от нового подхода, поэтому было решено рассказать об этом вне Яндекса.
На ClientSide'07 был сделан доклад про верстку независимыми блоками, которая на тот момент составляла основу наших HTML-страниц.
В докладе официально вводилось понятие блока:
Блоком будем называть фрагмент страницы, который описывается своей разметкой и стилями.
Более позднее описание.
Блоки делились на простые и составные.
В простые блоки нельзя вкладывать другие блоки, в составные — можно.
Это было неверно: мы неоднократно сталкивались с тем, что даже в самые простые блоки вкладывались другие, и приходилось переделывать верстку. В итоге мы пришли к противоположному принципу:
Любой блок должен позволять вкладывать в него другой блок, когда это возможно.
Правила независимости блоков
Сформировались первые правила независимости блока:
Для описания элемента используется
class
, но неid
.Каждый блок имеет префикс.
В таблице стилей нет классов вне блоков.
Важным решением был отказ от id
.
Теперь мы могли:
отображать на странице один и тот же блок несколько раз;
использовать на одном DOM-узле несколько классов (что нам пригодилось в дальнейшем).
Правила полной независимости блоков
С текущей схемой оставался ряд проблем с CSS:
CSS мог работать неправильно, если в страницу включался код, написанный сторонними разработчиками (например, библиотеки).
Блоки могли неправильно отображаться из-за конфликта имен элементов.
Селекторы по тегам могли охватывать больше HTML-элементов, чем было задумано.
Поэтому мы сформулировали правила более строгой независимости блоков под названием абсолютно независимые блоки (АНБ):
Не писать селекторы по тегам. Стили блоков и элементов описывать через селекторы классов.
Пример
.b-user b -> .b-user .first-letter
Всем классам внутри блока давать имена, начинающиеся с имени этого блока.
Пример
.b-user .first-letter -> .b-user-first_letter
Мы понимали, что наличие класса у каждого DOM-узла существенно увеличивает объем HTML-кода. На тот момент мы считали, что это дорого, и применяли такой подход в исключительных случаях.
Первые правила именования — префиксы
Так как распространенной проблемой в программировании является подбор имен, мы решили задавать имена блоков с помощью разных префиксов с разной семантикой:
b- — обычные блоки;
h- — обертки для нескольких блоков;
l- — раскладки;
g- — глобальные стили.
Появление модификации блоков
Работая с блоками, мы поняли, что они могут иметь разные состояния.
Например, «Кнопка» (блок button
) может быть:
маленькой;
нормальной;
большой.
Вместо того, чтобы создавать три разных блока, мы начали делать модификации.
Модификацию мы определили как особое состояние блока или как метку, несущую определенное свойство блоку.
Модификация определялась именем (например, size
) и значением (например, small
, normal
или big
).
Возможные варианты модификации:
Блок может изменить свой внешний вид в зависимости от того, где он находится. Это модификация от контекста.
Можно добавить блоку второй класс. Это модификация постфиксом, она не зависит от контекста.
Пример
class="b-block b-block-postfix"
Общепортальный фреймворк — Лего
Весной 2008 года была поставлена задача создать брендбук, описывающий наш портальный стиль. Решили начать работу с написания HTML/CSS кода.
Проект получил название Лего
.
Структура репозитория
На верхнем уровне репозиторий разделен по технологиям:
css/ html/ js/ xml/ xsl/
Директория каждой технологии имеет свою структуру.
CSS распределяется на следующие директории:
block
— общепортальные блоки;util
— блоки, которые имеют смысл вне Яндекса, их можно выложить в open source;service
— стили для конкретных сервисов Яндекса. Подключив их к сервису, можно отобразить шапку или подвал.
Пример
css/ block/ b-dropdown/ b-dropdown.css service/ auto/ block/ b-head-logo-auto.css head.css util/ b-hmenu/ b-hmenu.css
Структура директории HTML аналогична CSS:
html/ block/ b-dropdown.html service/ auto/ l-head.html util/ b-hmenu.html
JS находится в зачаточном состоянии и складывается в одну директорию:
js/ check-is-frame.js check-session.js clean-on-focus.js dropdown.js event.add.js event.del.js
У каждого сервиса есть XML-файл, использующийся для построения шапки:
xml/ block/ b-head-tabs-communication.xml common-services.ru.xml head-messages.ru.xml service/ auto/ head.xml
XSL блоков находится в одной директории. Каждому блоку соответствует один файл:
xsl/ block/ b-dropdown.xsl b-head-line.xsl i-common.xsl i-locale.xsl l-foot.xsl l-head.xsl
Лего подключается в проекты с помощью svn:externals.
При финальной сборке проекта код библиотеки полностью включается в проект, что можно сравнить со статической линковкой.
Такой подход позволяет выпускать версии сервисов с разными версиями Лего и переходить на новую версию тогда, когда это удобно команде проекта.
CSS-файлы
CSS-файлы, подключавшиеся на страницах, состояли из @import
'ов реализации блоков.
@import url(../../block/l-head/l-head.css);
@import url(../../block/b-head-logo/b-head-logo.css);
@import url(../../block/b-head-logo/b-head-logo_name.css);
@import url(block/b-head-logo-auto.css);
Эти @import
'ы писались вручную.
Правила именования
Именование файлов еще не устоялось - мы пробуем разные варианты.
Общепортальный фреймворк — Лего 1.2 (2008)
Структура репозитория
В рамках версии Лего 1.2, был произведен рефакторинг, и структура репозитория проекта изменилась.
common/ css/ js/ xml/ xsl/ example/ html/ service/ auto/ css/ xml/
Убрано разделение на util
и block
, общий CSS находится в common/css
.
От идеи выноса кода в open source на тот момент отказались и вернулись к ней только через два года.
common/ css/ b-dropdown/ arr/ b-dropdown.arr.css b-dropdown.arr.ie.css b-dropdown.css b-dropdown.ie.css
Всё, что находилось в опциональном CSS (файлах b-dropdown_arr.css
, b-dropdown_arr.ie.css
), вынесено в директории (arr/
). В основном файле блока стало меньше кода.
Правила именования
Файлы для IE переименованы: указатель специфичности файла для IE был частью имени файла, а стал суффиксом. Было -ie.css
, - стало .ie.css
. Расширения файлов теперь могут состоять из нескольких слов.
Для модификации постфиксом вместо дефиса начали использовать подчеркивание. Это позволило визуально отделить имя блока от имени модификатора, что позже пригодилось при реализации инструментов, упрощающих работу с кодом.
Лего 2.0. Появление БЭМ
В марте 2009 года вышла версия Лего 2.0.
Этим событием оканчивается верстка независимыми блоками и начинается БЭМ
.
БЭМ — аббревиатура от Блок, Элемент, Модификатор. Это три ключевые сущности, которые мы используем при разработке компонентов интерфейса.
Что же принципиально изменилось с выходом версии 2.0?
Основное изменение — мы вывели вперед блоки, а не технологии. Отныне блоки первичны, а технологии их реализации — вторичны.
Реализацию каждого блока разместили в отдельной директории, технологии — это файлы внутри нее. Также появилась документация к блоку — файл .wiki
внутри блока.
Независимый блок
Может быть использован в любом месте страницы.
В XML блок представлен тегом в пространстве имен lego
:
<lego:l-head>
<lego:b-head-logo>
HTML-класс блока соответствует имени этого тега:
<table class="l-head">
<div class="b-head-logo">
CSS-правила пишутся на класс:
.l-head
.b-head-logo
Все файлы (css
, js
, html
, xsl
), относящиеся к блоку, хранятся в его директории:
common/ block/ b-head-logo/ b-head-logo.css b-head-logo.xsl b-head-logo.js b-head-logo.wiki
Элемент
Составная часть блока, которая не может использоваться в отрыве от него.
В XML элемент представлен в пространстве имен lego
без префикса:
<lego:b-head-logo>
<lego:name/>
</lego:b-head-logo>
Класс в HTML соответствует имени этого элемента без префикса.
<div class="b-head-logo">
<span class="name">Авто</span>
</div>
CSS-правила пишутся на класс:
.b-head-logo .name { ... }
Файлы элемента хранятся в отдельной директории.
common/ block/ b-head-logo/ name/ b-head-logo.name.css b-head-logo.name.png b-head-logo.name.wiki
Имена файлов элементов пишутся через точку: b-head-logo.name.css
Модификатор
Определяет внешний вид, состояние и реже поведение блока.
В XML модификатор представлен атрибутом в пространстве имен lego
:
<lego:b-head-tabs lego:theme="grey">
В HTML используется дополнительный класс:
<div class="b-head-tabs b-head-tabs_grey">...</div>
CSS-правила пишутся на класс:
.b-head-tabs_grey { ... }
Файлы для модификатора находятся в отдельной директории. Имя директории модификатора начинается с подчеркивания:
common/ block/ b-head-logo/ _theme/ b-head-logo_gray.css b-head-logo_gray.png b-head-logo_gray.wiki
Декларация используемых блоков
Все компоненты Лего описываются в XML-файле.
<lego:page>
<lego:l-head>
<lego:b-head-logo>
<lego:name/>
</lego:b-head-logo>
<lego:b-head-tabs type="search-and-content"/>
Из него генерируются CSS-файлы.
@import url(../../common/block/global/_type/global_reset.css);
@import url(../../common/block/l-head/l-head.css);
@import url(../../common/block/b-head-logo/b-head-logo.css);
@import url(../../common/block/b-head-logo/name/b-head-logo.name.css);
@import url(../../common/block/b-head-tabs/b-head-tabs.css);
@import url(../../common/block/b-dropdown/b-dropdown.css);
@import url(../../common/block/b-dropdown/text/b-dropdown.text.css);
@import url(../../common/block/b-pseudo-link/b-pseudo-link.css);
@import url(../../common/block/b-dropdown/arrow/b-dropdown.arrow.css);
@import url(../../common/block/b-head-search/b-head-search.css);
@import url(../../common/block/b-search/b-search.css);
@import url(../../common/block/b-search/input/b-search.input.css);
@import url(../../common/block/b-search/sample/b-search.sample.css);
@import url(../../common/block/b-search/precise/b-search.precise.css);
@import url(../../common/block/b-search/button/b-search.button.css);
@import url(../../common/block/b-head-userinfo/b-head-userinfo.css);
@import url(../../common/block/b-user/b-user.css);
@import url(block/b-head-logo/b-head-logo.css);
@import url(block/b-head-search/b-head-search.css);
На примере этого файла видно, что сначала указывается общий код, а потом добавляются стили, чтобы привести Лего-блоки к дизайну проекта.
Из XML-декларации генерируются и JS-файлы.
include("../../common/block/i-locale/i-locale.js");
include("../../common/block/b-dropdown/b-dropdown.js");
include("../../common/block/b-search/sample/b-search.sample.js");
include("../../common/block/b-head-userinfo/user/b-head-userinfo.user.js");
А также XSL-файлы.
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:import href="../../common/block/i-common/i-common.xsl"/>
<xsl:import href="../../common/block/i-items/i-items.xsl"/>
<xsl:import href="../../common/block/l-head/l-head.xsl"/>
<xsl:import href="../../common/block/b-head-logo/b-head-logo.xsl"/>
<xsl:import href="../../common/block/b-head-logo/name/b-head-logo.name.xsl"/>
<xsl:import href="../../common/block/b-head-tabs/b-head-tabs.xsl"/>
<xsl:import href="../../common/block/b-dropdown/b-dropdown.xsl"/>
<xsl:import href="../../common/block/b-pseudo-link/b-pseudo-link.xsl"/>
<xsl:import href="../../common/block/b-head-search/b-head-search.xsl"/>
<xsl:import href="../../common/block/b-search/b-search.xsl"/>
<xsl:import href="../../common/block/b-search/input/b-search.input.xsl"/>
<xsl:import href="../../common/block/b-search/sample/b-search.sample.xsl"/>
<xsl:import href="../../common/block/b-search/precise/b-search.precise.xsl"/>
<xsl:import href="../../common/block/b-search/button/b-search.button.xsl"/>
<xsl:import href="../../common/block/b-head-userinfo/b-head-userinfo.xsl"/>
<xsl:import href="../../common/block/b-user/b-user.xsl"/>
<xsl:import href="../../common/block/b-head-userinfo/service/b-head-userinfo.service.xsl"/>
<xsl:import href="../../common/block/b-head-userinfo/setup/b-head-userinfo.setup.xsl"/>
<xsl:import href="../../common/block/b-head-userinfo/region/b-head-userinfo.region.xsl"/>
</xsl:stylesheet>
Мы перестали писать эти файлы руками, началась генерация кода.
Скорость селекторов (2009)
При реализации новой версии Яндекс.Почты была поставлена задача сделать ее быстрой.
Для решения задачи мы начали использовать XSL в браузере (и подгружать XML, необходимый для отрисовки данных на странице). Возникла проблема: трансформации отрабатывались быстро, но вставка в DOM полученного результата происходила очень медленно. При этом, отключение CSS решало проблему.
Выяснилось, что работу замедляют CSS-селекторы, которые при большом DOM-дереве и большой таблице стилей оказывают существенное влияние на скорость отрисовки браузером страницы.
Результаты исследования подробно описаны в статье.
Решение проблемы было уже готово — это абсолютно независимые блоки (АНБ).
Мы перевели все блоки в Лего
на АНБ-нотацию и с тех пор создаем их так, чтобы у каждого DOM-узла был свой class
, на который можно написать стили. Также мы не используем Tag Rules в CSS.
В классы элементов вносится имя блока, селекторы получаются простыми и быстрыми.
<div class="b-head-logo">
<span class="b-head-logo__name">
Авто
</span>
</div>
Стабилизация нотации
Постепенно мы пришли к тому, что нотация в коде и файловая структура устоялись и уже не меняются.
В именах файлов разделитель
.
был заменен на__
.Пример
b-block.elem.css
—>b-block__elem.css
Теперь они совпадают с CSS-селекторами.
Были реализованы модификаторы у элементов по аналогии с модификаторами блоков:
.b-block__elem_theme_green
по аналогии с.b-block_theme_green
.В имя файла модификатора и в его класс внесен тип модификатора.
Было:
.b-menu__item_current
Стало:
.b-menu__item_state_current
Причина этого изменения — работа с модификаторами из JavaScript.
БЭМ и open source (2010)
В 2010 году мы снова вернулись к идее open source. Мы создали организацию bem на GitHub.
Библиотека bem-bl
Мы начали выносить блоки из Лего в bem-bl, проводя одновременно с этим рефакторинг.
Параллельно с переносом блоков в новую библиотеку публиковали информацию про них.
Инструменты
Для работы с файлами по БЭМ-методам нам понадобились свои инструменты. Началась реализация инструментов bem-tools на JavaScript под Node.js.
Уровни переопределения
Возникло новое понятие — уровень переопределения. Так мы стали называть директории с реализацией блоков.
Например, в проекте может быть:
Публичная библиотека блоков с GitHub;
Внутренняя библиотека lego;
Блоки самого проекта.
Пример
bem-bl/ b-logo/ lego/ b-logo/ auto/ blocks/ b-logo/
На уровне переопределения можно задать другую схему именования директорий/файлов, отличную от нашей. Для этого нужно указать новый уровень в конфигурации:
.bem/ level.js
Например, вы можете задать другие разделители между именем блока и элемента, или не раскладывать все по директориям, а использовать плоскую структуру файлов.
Шаблонизатор BEMHTML
После экспериментов с разными шаблонизаторами, был разработан шаблонизатор BEMHTML, который позволяет:
Писать шаблоны в БЭМ-терминах.
Доопределять их на уровнях переопределения.
Исполнять эти шаблоны как на сервере, так и в браузере, поскольку шаблоны компилируются в простой и быстрый JavaScript.
Видео по BEMHTML:
Резюме
Появлению БЭМ в том виде, что мы имеем сейчас, предшествовал долгий период проб и экспериментов.
Хочется обратить ваше внимание, что на всех этапах своего развития это всё же был БЭМ.
Тот БЭМ, что мы используем сейчас, — не единственное верное решение.
Мы рекомендуем использовать БЭМ в ваших проектах в том объеме, в котором он принесет наибольшую пользу. Можно пробовать применять его только для верстки. Мы сами начинали именно с этого. Гибкость БЭМ-методологии позволяет настраивать ее под свои текущие процессы и организовывать работу над проектом.
Главное понять, какие плюсы БЭМ принесет в ваш проект, выбрать подходящую для вас схему и начать применять у себя!
Если у вас возникнут вопросы, обязательно задавайте их на нашем форуме.