Четыре вопроса про БЭМ

Последнее время меня всё чаще спрашивают про БЭМ. Потому я собрал самые частые вопросы и ответы на них в статью, чтобы было легче отвечать. Как обычно ¯\_(ツ)_/¯.

БЭМ?

Сперва стоит оговорить несколько важных моментов.

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

Во-вторых, и это важно, каждый трактует БЭМ по-своему. И у этого есть одна очевидная причина — БЭМ почти не ограничивает. По сути вся методология — это набор советов, или, если угодно, максим, которые просто определяют границы добра и зла. А то, в каких пропорциях их смешивать (и смешивать ли) — это уже на вашей совести. Однако, это не значит, что можно воровать-убивать в коде потому что «художник так видит».

Вопрос первый, про именование сущностей и выделение блоков

Спрашивает М. из, наверное, славного города Ш.:

Я уже в который раз на это натыкаюсь и не могу определиться, как правильно.

Допустим, мне нужно сделать список. Как я обычно делаю:

.list
  .list__item
  .list__item

Тут всё ок. Но что, если этот пример усложнить? Если list__item — это сложный блок, у которого есть свои элементы, то нужно создавать отдельный блок или делать это элементами list?

Я думал, что правильно именно так:

.list
  .list__item
    .list-item  // новый блок
      .list-item__title
      .list-item__button
  .list__item

Но вот это создание дополнительного блока из двух элементов меня всегда напрягало, поэтому я задумался, может быть правильнее будет так:

.list
  .list__item
    .list__title
    .list__button
  .list__item

Но с другой стороны list__title говорит о том, что это заголовок листа, а не одного элемента. И вот это меня постоянно вводит в ступор.

Дорогой М.!

Давай разберём по порядку.

Во-первых, про выделение элемента в отдельный блок. Это можно сделать, но на практике нужно очень редко. Обычно это сигнал к тому, что блок в целом перегружен, что ты пытаешься сделать его очень универсальным, и скорее всего можно сделать намного проще, если позволить себе некоторое дублирование, или же введение большего количества модификаторов.

Часто такой выделенный в самостоятельный блок элемент называется «служебным» или «приватным». Когда-то так делали ребята из Яндекса, например, в своей библиотеке компонентов.

Приватным он называется потому, что в силу его истории его можно использовать только внутри блока, из которого его выделили (в твоём случае только внутри list). Формально — это нарушение методологии, которая утверждает, что блок должен быть независимым. На практике же — это сделка с совестью, которая или позволяет тебе делать так, или не позволяет. Если ты не уверен, что чётко понимаешь себе плюсы и минусы такого решения, то лучше так не делать.

Во-вторых, вот такой код не очень крут:

.list
  .list__item
    .list-item  // новый блок
      .list-item__title
      .list-item__button
  .list__item

В большинстве случаев правильнее будет сделать так:

.list
  .list-item.list__item
    .list-item__title
    .list-item__button
  .list-item.list__item

То есть, в подобных случаях удобнее смиксовать элемент item блока list с тем приватным блоком, что ты выделил. Тогда list-item будет отвечать только за внешний вид самого элемента списка, а list__item — за его позиционирование в рамках всего списка (которое может меняться в зависимости от модификаторов).

Удобнее, потому что так у тебя не появляется лишней обёртки, а это помогает избежать в будущем проблем с позиционированием.

В-третьих, вот этот код тоже решает поставленную проблему:

.list
  .list__item
    .list__title
    .list__button
  .list__item

И тут тебя верно смущает тот факт, что list__title как бы намекает, что это «название списка». Но это зависит от того, как именно ты и твоя команда понимает БЭМ. Для меня, например, это «какое-то название внутри списка». Это вполне может быть и названием пункта, и названием целого списка и пр. Некоторые для устранения таких разночтений делают, например, list__item-title. Однако, это не всегда хорошо и если таких элементов появляется много, то это как раз звоночек о том, что list__item перегружен, и возможно стоит выделить его в отдельный приватный блок.

Или же не париться и делать более абстрактные элементы, типа list__title.

Как я уже говорил выше, БЭМ накладывает не так много ограничений, и скорее лишь указывает «как было бы правильнее». А как трактовать это — это уже зависит от команды. В каждом из решений есть свои изъяны:

  • list__item-title рождает новый уровень зависимости. Потому что получается, что есть отдельные элементы, которые привязаны к другим элементам. Но это решается просто игнорированием этого факта и принятием того, что это норма. Да, бывают элементы «заголовок пункта». И что? Не написано же, что это list__title-of-list__item. Просто заголовок какого-то пункта.
  • list-item__title рождает новый блок, который получается жёстко привязан к list и не может использоваться в отрыве от него. Но это решается принятием того, что у нас есть такие блоки. Иногда бывает намного удобнее декомпозировать таким образом, ничего не поделаешь. Как только мы принимаем, что у нас бывают подобные блоки, жить становится легче. Главное определить для себя, когда они нужны, а когда они излишни.
  • list__title же рождает непонятку, о которой ты написал. Но это решается бóльшим абстрагированием от общей сути блока. Это просто какое-то название внутри списка. Зато это даёт чуть больше гибкости. Например, у тебя может быть list__title_type_main и list__title_type_item. А может и не быть.
  • list-title, кстати, тоже решение. Оно, пожалуй, наиболее далёкое по смыслу от всего того, что есть тут. Но с другой стороны, если у тебя есть разные списки, то list-title вполне может быть таким элементом, который реализовывает заголовок внутри любого списка.

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

Вопрос второй, про общие ресурсы

Вопрошает И., из славного города К.:

Что делать и как быть, если нужно зашарить один объект между блоками? Например, картинку.

Дорогой И.!

Здесь сперва нужно определиться с тем, какое именно предназначение у этого объекта. Их может быть несколько, вот наиболее часто встречающиеся:

  1. Самостоятельная единица, которая может использоваться без привязки к конкретным блокам. Например, логотип или какое-то отдельное изображение на каком-нибудь лэндинге, которое встречается там несколько раз.
  2. Обычное изображение, которое по каким-то причинам пришлось использовать в двух блоках сразу.

Теперь про каждую подробнее.

Самостоятельная единица

В общем-то, формулирование проблемы уже описывает решение. Если это изображение самостоятельное, его вполне можно выделить в блок. Проблема, с которой сталкиваются новички, обычно в том, что «Но тут же нет вёрстки!». Ну и ладно ¯\_(ツ)_/¯. БЭМ про абстрактные блоки, которые «реализуются» в различных технологиях. Условно говоря, PNG, JPEG, WEBP и пр. — это всё эти самые технологии, в которых может быть реализован блок.

Дальше только возникнет вопрос, как именно это удобнее всего подключать в какую-то иную вёрстку. В идеале у такого блока должно быть представление в виде класса. То есть, это может быть, например, что-то такое:

<img src="./block.png" class="block">

Тогда это успешно ложится на абстракцию БЭМ-дерева. Плюс это можно расширять. Например так:

<img src="./_type/_mobile/block_type_mobile.png" class="block block_type_mobile">

Т. о. мы выделили отдельное изображение для этого «блока с изображением», которое будем показывать, например, только на мобильных. Дальше ему можно добавить микс и использовать в какой-либо композиции:

<div class="parent-block">
  <img src="./_type/_mobile/block_type_mobile.png" class="block block_type_mobile parent-block__image">
</div>

Во всех этих примерах класс на изображении не несёт, будто бы, никакого смысла, потому что мы не задали ему стилей. И тут наступает очередной момент, когда нужно или принять методологию в качестве непреложной истины, или пойти на сделку с совестью.

В первом случае нужно будет оставить классы как есть, потому что БЭМ в вёрстке — он о классах. И блока не существует в БЭМ-дереве, если у него нет класса.

Во втором случае можно убрать классы, поскольку то, что это за изображение, и так понятно по пути его подключения, за счёт того, что у нас БЭМ на файловой системе. Делать так или нет, зависит только от того, насколько вы уверенно чувствуете себя в коде и методологии, и от соглашений, принятых в команде.

Внезапное дублирование

Иногда так бывает, что дизайнеры рисуют-рисуют дизайн, и тут вдруг переиспользуют какую-то картинку, которая просто удачно смотрится в двух местах.

В таком случае сперва нужно задуматься, нельзя ли её выделить в какой-то отдельный блок. То есть, точно ли в этих двух местах используется одна и та же картинка, или это просто «визуально одинаковые сущности». Тут поможет банальный вопрос: «Если картинка изменится в одном месте, нужно ли будет менять её в другом?». Если да, то это скорее всего независимая сущность и её нужно выделять в блок так, как было описано выше.

Если же нет, то можно просто положить две одинаковые картинки в разные блоки, и ничего страшного в этом нет ¯\_(ツ)_/¯.

Если вас смущает тот факт, что у пользователя будет дважды скачиваться одна и та же картинка, то просто настройте свою сборку так, чтобы этого не было. Например, если всё собирается Вебпаком, то у лоадера для файлов есть настройки, которые позволяют менять имя изображения на хэш. Т. о., если где-то внезапно будут использоваться два одинаковых изображения, они получат один и тот же хэш, потому в итоговом бандле будет лежать только один файл, и пользователь не будет скачивать его второй раз, когда он потребуется.

Вопрос третий, про модификаторы

И., из почти славного города Ж., интересуется:

А что делать, если я создаю модификатор только с одним значением? Например,
у меня есть кнопка, у неё есть какой-то размер, но тут вдруг мне нужна кнопка побольше. И я создаю button_size_x.

Получается, что у меня есть какое-то значение по умолчанию, но есть и модификатор. Однако, у него только одно значение. Глупость же какая-то, не?

Дорогой И.!

На самом деле и правда получается глупость. Неопытный верстальщик в этом случае вообще создаёт булев модификатор, вроде button_big и успокаивается. Но это ещё бóльшая ошибка.

Во-первых, почему не булев? Потому что «размер» — это такой параметр, который может иметь разное значение. И если мы изначально делаем его булевым модификатором, мы закрываем себе возможности для расширения (тут можно было бы отослать читателя на статью про принцип открытости-закрытости, но я пока не буду). И когда завтра понадобится сделать кнопку маленького размера, придётся переписывать тот код, что уже написан. Или же придётся вводить button_small. Объяснение того, почему это неправильно, пусть будет домашним заданием для читателя.

Во-вторых, а что же делать? А решение на самом деле простое. Для того, чтобы понимать, как поступать в таких случаях, удобно представлять блок как функцию, а модификаторы — как параметры. В нашем случае получается какая-то вот такая функция:

function drawButton(size) {
  if (size) {
    // рисует кнопку указанного размера
  } else {
    // рисует кнопку какого-то иного размера
  }
}

Для упрощения понимания можно представлять, что size тут — это числовое значение. Если у читателя есть какой-то опыт программирования, он может сразу понять, что плохого в такой функции. А плохо то, что неизвестно, какого размера кнопка будет по умолчанию. Вместо явной параметризации мы получаем «параметризацию + какое-то иное поведение».

Чтобы избавиться от этого иного поведения программисты чаще всего делают просто:

function drawButton(size = 10) {
  // рисует кнопку указанного размера
}

И в таком случае по нотации функции уже понятно, что она сделает, если размер не передан — будет использовано значение 10.

Аналогично нужно поступить и в данном случае. Нужно имеющийся размер кнопки выделить в модификатор, который будет выставляться по умолчанию (с помощью шаблонизаторов это делается легче лёгкого), либо задаваться везде вручную, если вам так комфортнее.

В данном случае значением по умолчанию для модификатора size может быть, например, m. Но это уже сильно зависит от контекста.

Вопрос четвёртый, про скрипты

В редакцию пишет Л. из города-героя М.:

Вот элемент в форме, в шаблоне:

<div onclick="handleClick"></div>

В доисторические времена функция handleClick лежала рядом в этом же файле:

<script>
  function handleClick() { }
</script>

И подключалась прозрачно.

Когда на Луне обнаружился черный обелиск и в нем БЭМ, наступили наши времена, и я вынес скрипты в отдельный файл.

Файл подключается, и почти все гуд, кроме одного неудобства: он, похоже, подключается уже после всей вёрстки (или наоборот, до).

В момент, когда вёрстка парсится браузером, никакой функции handleClick он еще (или уже?) не знает. Я нашел корявый выход — у всех связанных со скриптами элементов делаю айдишники и функцию инициализации вешаю на onload событие.

Все работает, но необходимость вместо простого указания у блока в коде онклика устраивать пляски с инитами на домлоаде — какой-то адок. Нечитаемо, ошибкоемко, и громоздко, и нелаконично, и пространно, и вычурно, и многословно, и тумач букв.

Есть ли какой-то выход?

Дорогой Л.!

Это уже не столько про БЭМ, сколько про особенности сборки, но давай всё равно разберёмся.

Если бы мы говорили о каких-то фреймворках, в которых компонентный подход навязывается насильно (Ангуляр, Реакт и пр.), то там бы такой проблемы не возникло, потому что там компонент — это штука, которая обмазана скриптами донельзя, и потому скрипты не нужно привязывать к вёрстке. Скорее даже наоборот.

Но в твоём случае всё несколько иначе, потому что ты, видимо, привязываешь какую-то вёрстку к какому-то скрипту, и не можешь привязать их надёжно, чтобы они не разваливались.

Тот вариант, что ты предлагаешь — положить скрипты прямо в вёрстку — вполне рабочий, и в нём нет каких-то особых проблем. Если мы говорим об одной маленькой страничке, где есть пара блоков и небольшие скрипты для них, то это возможно куда более правильный путь, нежели выделять скрипты в отдельный файл и пр.

На практике же используется, обычно, несколько иной подход. А именно:

  1. Скрипты подключаются в самом конце тега body.
  2. Скрипты пишутся с оглядкой на то, что инстансов блока может быть больше одного.

Первый пункт позволяет нам быть уверенными в том, что скрипты начнут выполняться после того, как распарсится вся вёрстка. Иногда это не нужно, но чаще всего этого достаточно.

Второй пункт позволяет нам не плясать с идентификаторами, а использовать всё те же классы, которыми мы оперируем в БЭМ.

В общем случае получается что-то такое. Вёрстка:

<!-- ... -->
<body>
  <!-- ... -->
  <div class="block">
    <!-- ... -->
  </div>
  <!-- ... -->

  <script src="app.js"></script>
</body>
<!-- ... -->

JS-файл, при этом, скорее всего будет собираться как-то из других JS-файлов (из-за того, что у нас БЭМ на файловой системе), но предположим, что блок всего один. Тогда примерно вот таким будет JS-файл:

Array.from(document.querySelectorAll('.block')).forEach(initBlock);

function initBlock(node) {
  // какие-то действия над блоком
}

В зависимости от условий, в которых всё это применяется, могут быть какие-то изменения. Например, если по каким-то причинам нельзя подключать скрипты в конце документа, то скрипт может принимать такой вид:

document.addEventListener('DOMContentLoaded', () => Array.from(document.querySelectorAll('.block')).forEach(initBlock));

function initBlock(node) {
  // какие-то действия над блоком
}

Или, если есть уверенность, что это очень нужно (не стоит так делать без чёткого понимания разницы), то он может быть и таким:

window.addEventListener('load', () => Array.from(document.querySelectorAll('.block')).forEach(initBlock));

function initBlock(node) {
  // какие-то действия над блоком
}

Если скриптов на проекте много, то всё это можно спрятать в какую-то функцию, которая будет использоваться во всех файлах для инициализации блока. Типа такого:

// определённая где-то глобально функция
function initBlock(selector, fn) {
  Array.from(document.querySelectorAll(selector)).forEach(fn);
}

// локальное применение для конкретного блока
initBlock('.block', node => {
  // какие-то действия над блоком
});

Главное во всей этой истории то, что «какие-то действия над блоком» должны быть написаны так, чтобы подразумевалось, что таких блоков может быть любое количество на странице.

∗∗∗

Кажется, это были четыре наиболее часто задаваемых вопроса. Если вам кажется, что я что-то упустил или где-то неправ — пишите в комментарии. А пока вот несколько полезных ссылок по теме:

  • Описание методологии от Яндекса.
    Кажется, с каждой итерацией обновления этой документации, ребята из Яндекса всё дальше уходят от описания полного БЭМ-стека к более абстрактному описанию методологии. Это не может не радовать.
  • Принципы SOLID.
    Забавно, но в БЭМ, кажется, больше от программирования, чем от вёрстки. К слову, некоторые из этих принципов и их применение к БЭМ описаны в официальной документации.
  • Базовые понятия и принципы БЭМ.
    Мой коллега когда-то написал вот такую довольно академичную выдержку из своего доклада. Читать нужно аккуратно и вдумчиво, как будто это учебник по математике.
Блок кода, который тут только для того, чтобы Эгея подключила highlight.js, потому что иначе она не хочет этого делать. (╯°□°)╯︵ ┻━┻)
2018
1 комментарий
Muhammadamin

Может мой вариант тоже не очень обычно я делаю так
.list
__.list__item
____.list__subtitle
____.list__button
__.list__item

Игорь Адаменко

Это тоже вариант, но в таком случае не совсем понятно, почему у вас есть subtitle, но нет title. То есть, подзаголовок у списка есть, а заголовка — нет.

Это, кажется, может вызвать определённые проблемы в понимании сути блока по его декларации.

Популярное