Сражаясь с БЭМ: 10 основных ошибок и как их избежать

в 23:19, , рубрики: css, html, html5, javascript, БЭМ, методология разработки, пользовательские интерфейсы, метки:

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

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

Разработчик во мне смотрел на это прагматично. И, в конечном счете, модульный способ построения пользовательского интерфейса перевесил правую сторону моего мозга, которая утверждала: "Но он недостаточно хорош!". В таких случаях, я предпочитаю функциональность над внешней формой. В любом случае, хватит пустых разговоров. Представляю вашему вниманию 10 проблем, с которыми я поборолся и несколько советов, как разрешить их.

1. "Что делать с "внучатыми" селекторами (и не только)?"

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

<div class="c-card">

    <div class="c-card__header">
        <!-- Появляется внук… -->
        <h2 class="c-card__header__title">Title text here</h2>
    </div>

    <div class="c-card__body">

        <img class="c-card__body__img" src="http://some-img.png" alt="description">
        <p class="c-card__body__text">Lorem ipsum dolor sit amet, consectetur</p>
        <p class="c-card__body__text">Adipiscing elit.
            <a href="/somelink.html" class="c-card__body__text__link">Pellentesque amet</a>
        </p>

    </div>
</div>

Как вы могли заметить, имена могут быстро выйти из под контроля и, чем больше вложенности, тем безобразнее становится имя класса элемента. Я использовал короткий блок с названием c-card и короткие имена body, text, link, но попробуйте представить, что получится, если первоначальный блочный элемент будет назван c-drop-down-menu.

Я считаю, что двойное нижнее подчеркивание должно появляться только однажды, в имени селектора. БЭМ расшифровывается как Блок_Элемент--Модификатор, а не Блок__Элемент__Элемент--Модификатор. Также избегайте многоуровневого наименования элементов. Если у вас получаются уровни пра-пра-пра-правнуков, то, вероятно, вам стоит пересмотреть структуру компонентов.

Именование БЭМ не привязано строго к DOM, так что не имеет значения на скольких уровнях находится вложенный элемент. Соглашение об именах в первую очередь помогает вам увидеть отношения с блоком на верхнем уровне — в нашем случае, c-card.

Как бы я рассматривал один и тот же карточный компонент:

<div class="c-card">
    <div class="c-card__header">
        <h2 class="c-card__title">Title text here</h2>
    </div>

    <div class="c-card__body">

        <img class="c-card__img" src="http://some-img.png" alt="description">
        <p class="c-card__text">Lorem ipsum dolor sit amet, consectetur</p>
        <p class="c-card__text">Adipiscing elit.
            <a href="/somelink.html" class="c-card__link">Pellentesque amet</a>
        </p>

    </div>
</div>

Это значит, что все вложенные элементы будут затронуты только карточным блоком. Также, мы бы могли переместить текст и изображения в c-card__header или даже внести новый элемент c-card__footer при этом не ломая семантической структуры.

2. "Какие наименования использовать?"

К этому времени вы возможно заметили использование c- в моих примерах. Этот префикс расшифровывается как "компонент (component)" и лежит в основе всех имен моих БЭМ классов. Идея позаимствована из техники наименований Гарри Роберта, которая улучшает читаемость кода.

Система, которую я принял, и множество префиксов, которые будут встречаться в примерах в этой статье:

Тип Префикс Примеры Описание
Компонент (Component) c- c-card, c-checklist Формируют основу приложения и содержат всю косметику для отдельных компонентов.
Модуль макета (Layout module) l- l-grid, l-container У этих модулей нет никакой косметики, используются только для позиционирования компонентов c- и для построения макета приложения.
Помощники (Helpers) h- h-show, h-hide Эти классы имеют одну функцию, часто используют !important для повышения их специфичности. (В основном используются для позиционирования или видимости.)
Состояния (States) is-, has- is-visible, has-loaded Показывают различные состояния, которые могут быть у компонентов c-. Более детальное описание можно найти ниже в 6 проблеме.
JavaScript хуки js- js-tab-switcher Они указывают на то, что поведение JavaScript привязано к компоненту. Стили с ними не ассоциируются; используются только для облегчения манипуляций со скриптами.

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

Вы бы могли также взять и другие префиксы, такие как qa- для тестов на качество (quality-assurance), ss- для различных хуков на стороне сервера (server-side) и т.д. Но список выше — уже хорошее начало, и ввести новые имена вы можете после того, как освоитесь с этой техникой.

Вы увидите хороший пример использования этого стиля наименований в следующей проблеме.

3. "Как мне называть врапперы (wrappers)?"

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

В нашем карточном примере, если бы мы хотели расположить список из четырех c-cardов, я бы использовал следующую разметку:

<ul class="l-grid">
    <li class="l-grid__item">
        <div class="c-card">
            <div class="c-card__header">
                […]
            </div>
            <div class="c-card__body">
                […]
            </div>
        </div>
    </li>
    <li class="l-grid__item">
        <div class="c-card">
            <div class="c-card__header">
                […]
            </div>
            <div class="c-card__body">
                […]
            </div>
        </div>
    </li>
    <li class="l-grid__item">
        <div class="c-card">
            <div class="c-card__header">
                […]
            </div>
            <div class="c-card__body">
                […]
            </div>
        </div>
    </li>
    <li class="l-grid__item">
        <div class="c-card">
            <div class="c-card__header">
                […]
            </div>
            <div class="c-card__body">
                […]
            </div>
        </div>
    </li>
</ul>

Теперь у вас должно быть твердое представление о том, как расположение модулей и имена компонентов должны сочетаться вместе.

Не бойтесь использовать немного более продвинутой разметки, дабы потом у вас не было головной боли. Никому не станет лучше от того, что вы сократите пару <div> тегов!

В некоторых ситуациях, это не возможно. Если, например, ваша сетка не собирается вас слушаться, или вы просто хотите какое-нибудь имя со смыслом для вашего родительского элемента, то что вам делать? Я, как правило, выбираю слово container или list, в зависимости от ситуации. Применяя это к нашему карточному примеру, я бы мог использовать <div class="l-cards-container">[…]</div> или <ul class="l-cards-list">[…]</ul>, в зависимости от используемого случая. Главное — соответствовать вашему соглашению об именовании.

4. "Кросскомпонентные… Компоненты?"

Еще одна часто встречающаяся проблема — компоненты, чьи стили или позиционирование затрагиваются родительским контейнером. Различные решения этой проблемы детально описаны у Simurai. Я вам расскажу только о том подходе, который я считаю самым эффективным.

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

<button class="c-button c-button--primary">Click me!</button>

Если у вас нет никаких различий между обычным компонентом кнопки, то тогда проблем нет, мы просто переместим ее вот так:

<div class="c-card">
    <div class="c-card__header">
        <h2 class="c-card__title">Title text here</h3>
    </div>

    <div class="c-card__body">

        <img class="c-card__img" src="http://some-img.png">
        <p class="c-card__text">Lorem ipsum dolor sit amet, consectetur</p>
        <p class="c-card__text">Adipiscing elit. Pellentesque.</p>

        <!-- Наш вложенный компонент кнопки -->
        <button class="c-button c-button--primary">Click me!</button>

    </div>
</div>

Однако, что происходит, когда присутствуют небольшие различия в стилях — например, мы хотим ее немного уменьшить, закруглить углы, но только тогда, когда она является частью компонента c-card?

Кросскомпонентный класс показался мне наиболее надежным решением:

<div class="c-card">
    <div class="c-card__header">
        <h2 class="c-card__title">Title text here</h3>
    </div>

    <div class="c-card__body">

        <img class="c-card__img" src="http://some-img.png">
        <p class="c-card__text">Lorem ipsum dolor sit amet, consectetur</p>
        <p class="c-card__text">Adipiscing elit. Pellentesque.</p>

        <!-- Мой *старый* кросскомпонентный подход -->        <button class="c-button c-card__c-button">Click me!</button>

    </div>
</div>

Это то, что известно на сайте БЭМ, как "микс (mix)". Однако, я изменил свое отношение к этому подходу, следуя некоторым замечательным комментариям от Esteban Lussich.

В примере выше, класс c-card__c-button пытается изменить одно или несколько существующих свойств c-button, но зависит от источника вызова (или даже от специфики), для успешного применения. Класс c-card__c-button будет работать только в том случае, если он определен после блока c-button, который может быстро выйти из под контроля, если вы будете строить больше таких кросскомпонентов. (Полагаться на !important, конечно, можно, но я бы не стал!).

Оформление по настоящему модульного UI элемента должно быть полностью независимо от родительского контейнера — оно должно выглядеть одинаково независимо от того, где вы его расположите. Добавление класса из другого компонента для задания стиля, как это делает способ с "миксами", нарушает открытые/закрытые принципы компонентно-ориентированного дизайна — т.е., не должно быть никакой зависимости от другого модуля для эстетики.

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

<button class="c-button c-button--rounded c-button--small">Click me!</button>

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

Другой способ — пойти к вашему дизайнеру и сказать ему, что кнопки должны быть совместимы с остальными, и избегать этой проблемы вообще.

5. "Модификатор или новый компонент?"

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

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

Можно легко переполнить проект модулями и сделать все в виде компонентов. Я рекомендую начать с модификаторов, если вам покажется, что управление каким-либо компонентным CSS файлом становится слишком сложным, то, возможно, наступило время разделить некоторые модификаторы. Хороший показатель, когда вам приходится сбрасывать все у блока CSS для стилизации нового модификатора — это, как по мне, самое время для создания нового компонента.

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

6. "Как управлять состояниями?"

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

У вас два варианта: или использовать хук автономного состояния, или использовать БЭМ-подобное именование модификатора на уровне компонента:

<!-- хук автономного состояния -->
<div class="c-card is-active">
    […]
</div>

<!-- модификатор БЭМ -->
<div class="c-card c-card--is-active">
    […]
</div>

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

Имеет смысл придерживаться стандартного набора хуков состояния. Крис Пирс собрал хороший список, который я рекомендую посмотреть.

7. "Когда лучше не добавлять новый класс к элементу"

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

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

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

Ввиду глобального характера CSS, присвоение класса ко всему дает нам полный контроль над рендером. Первоначальный дискомфорт стоит преимуществ полностью модульной системы.

8. "Как вкладывать компоненты?"

Допустим. что мы хотим отобразить чек-лист в нашем компоненте c-card. Пример того, как не стоит это делать:

<div class="c-card">
    <div class="c-card__header">
        <h2 class="c-card__title">Title text here</h3>
    </div>

    <div class="c-card__body">

        <p>I would like to buy:</p>

        <!-- Вложенный компонент -->
        <ul class="c-card__checklist">
            <li class="c-card__checklist__item">
                <input id="option_1" type="checkbox" name="checkbox" class="c-card__checklist__input">
                <label for="option_1" class="c-card__checklist__label">Apples</label>
            </li>
            <li class="c-card__checklist__item">
                <input id="option_2" type="checkbox" name="checkbox" class="c-card__checklist__input">
                <label for="option_2" class="c-card__checklist__label">Pears</label>
            </li>
        </ul>

    </div>
    <!-- .c-card__body -->
</div>
<!-- .c-card -->

У нас тут есть несколько проблем. Первая — двухуровневый селектор, о котором мы узнали в первом разделе. Второе — все стили, примененные к c-card__checklist__item, относятся только к этому конкретному случаю, из-за чего их нельзя использовать снова.

Я предпочитаю разбить список на модуль разметки, предметы чек-листа на их собственные компоненты, что позволяет использовать их самостоятельно в другом месте. Это позволяет вернуться нашему префиксу l- к игре:

<div class="c-card">
    <div class="c-card__header">
        <h2 class="c-card__title">Title text here</h3>
    </div>

    <div class="c-card__body"><div class="c-card__body">

        <p>I would like to buy:</p>

        <!-- Намного лучше - модуль разметки -->
        <ul class="l-list">
            <li class="l-list__item">

                <!-- Многоразовый вложенный компонент -->
                <div class="c-checkbox">
                    <input id="option_1" type="checkbox" name="checkbox" class="c-checkbox__input">
                    <label for="option_1" class="c-checkbox__label">Apples</label>
                </div>

            </li>
            <li class="l-list__item">

                <div class="c-checkbox">
                    <input id="option_2" type="checkbox" name="checkbox" class="c-checkbox__input">
                    <label for="option_2" class="c-checkbox__label">Pears</label>
                </div>

            </li>
        </ul>
        <!-- .l-list -->

    </div>
    <!-- .c-card__body -->
</div>
<!-- .c-card -->

Это спасает вас от необходимости повторять эти стили, а также значит, что мы можем использовать l-list и c-checkbox в других местах нашего приложения. Это требует немного больше разметки, но взамен мы получаем читаемость, инкапсуляцию и возможность повторного использования. Возможно, вы заметили, что это — основные темы!

9. "Не появятся ли у компонентов миллион классов?"

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

Пример из четырех классов, с помощью которых стилизуется кнопка:

<button class="c-button c-button--primary c-button--huge  is-active">Click me!</button>

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

Однако, если от этого у вас начинает болеть голова, вы можете взглянуть на технику Сергея Зароуски. Фактически, мы будем использовать .className [class^="className"], [class*=" className"] в таблице стилей для имитации дополнительной функциональности в CSS. Если этот синтаксис кажется вам знакомым, то это потому, что Icomoon использует похожий способ для управления селекторами иконок.

С этой техникой, ваш вывод может выглядеть примерно так:

<button class="c-button--primary-huge is-active">Click me!</button>

Я не знаю, намного ли снижение производительности при использовании class^= и class*= больше, чем при использовании индивидуальных классов, но в теории, это классная альтернатива. Для себя мне хватает варианта с несколькими классами, но мне кажется, что этот способ определенно заслуживает упоминания для тех, кто предпочитает альтернативу.

10. "Можем ли мы изменить реакцию типа компонента?"

Эту проблему поставил мне Arie Thulank, и к которой я пытался найти 100% решение.

Примером может быть выпадающее меню, которое превращается в набор вкладок в заданный момент, или закадровая боковая навигация, которая превращается в панель меню.

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

Для этих двух примеров я склоняюсь к тому, чтобы просто сделать компонент c-navigation, так как изменение в заданный момент — это то, что он делает. Но это заставило меня задуматься, что на счет списков изображений, которые превращаются в карусель на больших экранах? Это проблемный случай для меня, и, так как он хорошо документирован и прокомментирован, я думаю, что в идеале стоит создать отдельный одноразовый компонент для этого типа интерфейса, с понятным названием (таким как c-image-list-to-carousel).

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

<ul class="c-image-list@small-screen c-carousel@large-screen">

В дальнейшем это будет жить в ваших медиа-запросах для соответствующих размеров экрана.
Совет: Вам придется отключать @ с помощью обратной косой черты, как тут:

.c-image-list@small-screen {
    /* стили тут */
}

У меня не было причин для создания таких компонентов, но этот способ выглядит очень дружелюбным по отношению к разработчику. Другому человеку должно быть достаточно просто понять ваши намерения. Но я не пропагандирую такие имена, как small-screen и large-screen — они использованы только для улучшения читаемости.

Заключение

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

Обо всех ошибках (грамматических, лексических и т.д.) пишите в комментарии, буду рад их исправить. Спасибо за внимание

Автор: kirillbykov2

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js