История БЭМ началась в 2005 году. Тогда, с точки зрения интерфейса, обычный проект Яндекса был набором статических HTML-страниц, которые использовались как основа для создания шаблонов на XSL.
HTML-страницы хранились в отдельной директории, которая имела подобную структуру:
about.html
index.html
…
project.css
project.js
i/
yandex.png
id
и классы
.project.js
. JavaScript использовался как вспомогательный инструмент для оживления страницы, поэтому project.js
был небольшим.Стили, как и скрипты, находились в одном файле — project.css
. Для отделения стилей разных частей страницы использовались комментарии с указанием начала и конца:
/* 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;
}
.banner a
{
text-decoration: none;
}
/* 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;
}
.result .albums .album .buy
{
float: left;
padding: 0.4em 1em 0 1.6em;
}
.result .albums .info i
{
font-size: 85%;
}
/* Albums (end) */
Одновременное использование id
и тегов
:
/* Картинки на фоне (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);
}
#foot div div div div
{
background-position: 54%;
background-image: url(../i/foot-4.png);
}
#foot div div div div div
{
background-position: 71%;
background-image: url(../i/foot-5.png);
}
#foot div div div div div div
{
background-position: 87%;
background-image: url(../i/foot-6.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-страниц.
В докладе официально вводилось понятие блок
:
<blockquote>Блоком будем называть фрагмент страницы, который описывается своей разметкой и стилями.</blockquote>
Более позднее описание.
Блоки делились на простые и составные.
<blockquote>В простые блоки нельзя вкладывать другие блоки, в составные — можно.</blockquote>
Это было очень наивное деление: мы неоднократно сталкивались с тем, что даже в самые простые блоки вкладывались другие, и приходилось переделывать верстку. В итоге мы пришли к противоположному принципу:
<blockquote>Любой блок должен позволять вкладывать в него другой блок, когда это возможно.</blockquote>
Сформировались первые правила независимости блока:
class
, но не id
.Важным решением был отказ от id
. Теперь мы могли:
С текущей схемой оставался ряд проблем с CSS:
Поэтому мы сформулировали правила более строгой независимости блоков под названием абсолютно независимые блоки (АНБ):
Никогда не опираться на элементы - только на классы.
.b-user b -> .b-user .first-letter
Всем классам внутри блока давать имена, начинающиеся с имени этого блока.
.b-user .first-letter -> .b-user-first_letter
Мы понимали, что наличие класса у каждого DOM-узла существенно увеличивает объем HTML-кода. На тот момент мы считали, что это дорого, и применяли такой подход в исключительных случаях.
Так как распространенной проблемой в программировании является подбор имен переменным. Мы решили задавать имена блоков с помощью разных префиксов с разной семантикой:
Работая с блоками, мы поняли, что они могут иметь разные состояния. Например, блок Кнопка
может быть представлен в трех состояниях: маленькая, нормальная и большая.
Вместо того, чтобы создавать три разных блока, мы начали делать модификации
одного.
Модификацию
мы определили как особое состояние блока или как метку, несущую определенное свойство блоку. Модификация состояла из имени (например, 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.
При финальной сборке проекта код библиотеки полностью включается в проект, что можно сравнить со статической линковкой.
Такой подход позволяет выпускать версии сервисов с разными версиями Лего
и переходить на новую версию тогда, когда это удобно команде проекта.
Файлы, подключавшиеся на страницах, состояли из @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
, был произведен рефакторинг, и структура репозитория проекта изменилась.
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
),
вынесено в директории (arr/b-dropdown.arr.css
).
В основном файле блока стало меньше кода.
Файлы для IE переименованы: указатель специфичности файла для IE был частью имени файла, а стал суффиксом. Было -ie.css
, - стало .ie.css
.
Расширения файлов теперь могут состоять из нескольких слов.
Для модификации постфиксом вместо дефиса начали использовать подчеркивание. Это позволило визуально отделить имя блока от имени модификатора, что позже пригодилось при реализации инструментов, упрощающих работу с кодом.
В марте 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>
.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">
.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>
Мы перестали писать эти файлы руками, началась генерация кода.
При реализации новой версии Яндекс.Почты была поставлена задача сделать ее быстрой.
Для решения задачи мы начали использовать 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.
В 2010 году мы снова вернулись к идее open source. Мы создали организацию bem на GitHub.
Мы начали выносить блоки из Лего
в bem-bl,
проводя одновременно с этим рефакторинг.
Параллельно с переносом блоков в новую библиотеку публиковали информацию про них.
Для работы с файлами по БЭМ-методам нам понадобились свои инструменты. Началась реализация инструментов bem-tools на JavaScript под Node.js.
Возникло новое понятие — уровень переопределения
. Так мы стали называть директории с реализацией блоков.
Например, в проекте может быть:
bem-bl/
b-logo/
lego/
b-logo/
auto/
blocks/
b-logo/
На уровне переопределения можно задать другую схему именования директорий/файлов, отличную от нашей. Для этого нужно указать новый уровень в конфигурации:
.bem/
level.js
Например, вы можете задать другие разделители между именем блока и элемента, или не раскладывать все по директориям, а использовать плоскую структуру файлов.
После экспериментов с разными шаблонизаторами, был разработан шаблонизатор BEMHTML, который позволяет:
Видео по BEMHTML:
Появлению БЭМ в том виде, что мы имеем сейчас, предшествовал долгий период проб и экспериментов.
Хочется обратить ваше внимание, что на всех этапах своего развития это всё же был БЭМ.
Тот БЭМ, что мы используем сейчас, — не единственное верное решение. Мы рекомендуем использовать БЭМ в ваших проектах в том объеме, в котором он принесет наибольшую пользу. Можно пробовать применять его только для верстки. Мы сами начинали именно с этого. Гибкость БЭМ-методологии позволяет настраивать ее под свои текущие процессы и организовывать работу над проектом.
Главное понять, какие плюсы БЭМ принесет в ваш проект, выбрать подходящую для вас схему и начать применять у себя!
Если у вас возникнут вопросы, обязательно задавайте их на нашем форуме.