Предупрежу заранее, что я совершенно не считаю себя экспертом HTML/CSS/JS. Но, как архитектору, мне всегда была интересна организация и систематизация кода в самых разных его проявлениях, в том числе и представленных в виде CSS. Особенно сильно этот интерес был подогрет БЭМом, при первом знакомстве с которым подсознание отреагировало когнитивным дискомфортом. А поскольку БЭМ-стиль в проектах у меня стал появляться все чаще, я почувствовал острую необходимость осмыслить, наконец, свое отношение к организации стилей. Таким образом и появился данный топик-размышление, топик-дискуссия. Я понимаю, что взялся за пограничную задачу, поскольку далеко не всем верстальщикам знакомы тонкости объектно-ориентированного дизайна, а большинство архитекторов не написали ни одного CSS-стиля. И, как результат, мне пришлось неуклюже балансировать, чтобы было понятно всем. Но 'этот риск еще больше подогрел мой интерес к теме :)
Вопросы к БЭМ
Начать я решил с того, что мне было не совсем понятно в БЭМ:
1. Многие считают, что техника априори верна всегда и везде, поскольку за ней стоит Яндекс. Почему то, что подходит крупной компании с гигантским штатом и небольшим количеством однотипных проектов должно так же хорошо работать, скажем, в небольшой команде, которая осуществляет заказную разработку самых разнообразных сайтов? Возьмем в качестве примера подходы из jQueryUI, KendoUI и т.п. Они растиражированы на сотни тысяч проектов и в этом плане имеют гораздо большую зрелость. Причем сам Яндекс на сайте bem.info ясно обрисовывает условия применения, но многие даже не доходят до этого параграфа, ограничиваясь первыми строками и успокоительным: «это же придумали в Яндексе».
2. Почему считается что принудительная контекстная независимость — это хорошо и тут же вводится эта самая зависимость элемента от блока, если пользоваться терминологией БЭМ? Предположим, я хочу, чтобы все кнопки на сайте у меня выглядели не так, как это хочется автору блока, а в соответствии с единым, продуманным стилем. Если это некоторая стандартная кнопка, то в одном и том же контексте она должна выглядеть одинаково. В другом, как раз, может быть разной. В сайдбаре — поменьше, в теле страницы — побольше, на большом экране — текстом и иконкой, на смартфоне — иконкой. Но если это стандартная кнопка в сайдбаре, то в таких ситуация она должна и выглядеть должна стандартно. Здесь под стандартом подразумевается удачное решение, которое нашли дизайнер вместе с UX-специалистом, а не верстальщик отдельно взятого блока.
Чтобы проиллюстрировать свои сомнения я взял первые попавшиеся формы ввода с сайта Яндекса: создание почтового ящика, регистрация и фидбека.
Не знаю, кто и что увидел на этих картинках, а я вижу полную анархию. Дизайном этих форм занимались, очевидно, не те, кто должен заниматься, а лепили авторы блоков так, как каждый из них это видит. Почему единый стандарт на то, как должны выглядеть поля ввода и кнопки в формах — это плохо?
Яндекс на сайте bem.info пишет: «Каждый новый проект или элемент интерфейса не должны писаться с нуля. Если где-то внутри компании уже выполнялась похожая задача, нужно максимально повторно использовать полученный в результате код. У кода не должно быть контекстной зависимости, его нужно уметь легко переносить в другое место». Давайте посмотрим на пример такого реюзинга.
Два блока логина на одной странице. Если бы это делал я, неправильным способом, то написал бы модуль единожды, а потом, в зависимости от контекста, немного поправил стили, чтобы в сайдбаре он выглядел уменьшенным. Здесь, же я вижу два разных модуля и двух разных авторов. Один считает, что надо «вспомнить пароль», другой предлагает его «напомнить». Первый считает, что линк «зарегистрироваться» не является частью этого функционального блока и вынес его за визуальный контур, второй с ним не согласен. Они расходятся даже в том, как озаглавить этот блок. Автор попап-версии резонного полагает, что пользователь должен осуществить «вход», минималист же хочет, чтобы посетитель сделал «маркет». Они даже реализовывали по-разному, один — табличной версткой, другой — слоями. Повторного использования — ноль. Зато оба использовали БЭМ.
3. Почему отсутствие лаконичности в названиях классов считать преимуществом? И как может нравится это — <div class=«menu__item menu__item_position_first menu__item_state_current»>? В последней верстке, которую я лишил БЭМ объем страницы упал в два раза. Я в данном случае не говорю, что это достаточная причина отказаться от БЭМ и смысл появления этого оверхеда понятен. Просто констатирую, что за такой раздутый синтаксис с низкой энтропией я бы снял балл.
4. Почему нужно отказывать от многих возможностей CSS? Будь то селекторы с использованием идентификаторов, контекстов и т.п.
Первая реакция на БЭМ была такова, что я расшифровал эту аббревиатуру как Бардак Эвентуальный Метастазирующий. Но все эти недовольства попахивали просто очередным частным мнением, которое не очень убеждало меня самого, что уж говорить об убедительности для других. И тогда я призвал себя осмыслить саму концепцию классов CSS в архитектурном ключе, как мне привычней.
Design by contract
Начал я системной роли классов. Частное, как правило, является менее важным, чем целое, поскольку последнее — суть их сумма. Так и здесь, участие CSS-классов во взаимодействии с остальной частью системы может быть гораздо важней, чем любые улучшения в области одной только верстки. А участие их здесь весьма серьезное. По сути, названия классов — это единственная крепкая связь между HTML и JS. Основная роль JS, так или иначе, иметь дело с DOM-моделью. А какой самый лучший способ добраться до нужных узлов? Селекторы с участием имен классов. Все остальные варианты не масштабируются. В обходите childNodes/parentNode? Завтра там может оказаться враппер и код перестанет работать. Используете названия тегов? Завтра <input> станет <textarea>, <i> станет <em>, <pre> заменят на <code> и скрипты перестанут работать. Имя класса — это тот фундаментальный интерфейс взаимодействия, который мы можем поставить в HTML и использовать в JS не боясь за дальнейшую эволюцию системы.
Я здесь намеренно употребил слово «интерфейс», поскольку хотел провести аналогию с объектно-ориентированными практиками. Надо же как-то отрабатывать заголовок. В свое время Бертран Мейер в рамках разработки языка Eiffel озвучил парадигму проектирования Design by contract. Я не буду сильно углубляться в ее детали, ограничусь только ключевой концепцией. Объекты несут бремя «контрактных» обязательств и взаимодействуют между собой через эти самые контракты. Например, есть некий виджет, который может принимать другие объекты drag'n'drop-ом. Он всем сообщает: «я могу это, для этой цели у меня есть, скажем, функция CanDrop, которой вы передаете в качестве аргумента объект, а я возвращаю логическое значение, принимаю такого типа объекты или нет». Таким образом в его контракте прописано наличие этой функции, ее сигнатура и суть. Системе абсолютно не важно, взял на себя этот контракт виджет для аплоада файлов, дерево с возможностью перетаскивать узлы или это такая продвинутая корзина товаров, куда их можно переносить из каталога мышкой. При операции drag'n'drop-а ей достаточно только знать о том, что некий виджет несет эти контрактные обязательства. И когда вы потащите файл в корзину товаров, она вызовет метод CanDrop корзины получит фейл и просигнализирует вам о невозможности данной операции.
В дальнейшем суть контрактного подхода активно развивалась и большая часть современных паттернов проектирования основывается на использовании интерфейсов объектов. А интерфейс объекта — суть и есть контракт, который перечисляет все методы, которые данный объект может выполнить. Так что речь идет о весьма зрелом и знакомом многим программистам подходе.
Попробую теперь переложить эту идею на HTML+CSS+JS. Представим себе для начала два простых контракта — container и item, они же имена CSS-классов. Первый говорит нам, что он берет на себя обязательства по работе с item. Ему абсолютно все равно, какой за ним стоит тег или DOM-узел. Это может быть элемент списка, пункт меню, закладка на таб-контроле. Он может работать с кем угодно, если этот кто-то готов исполнять контракт item. Таким образом мы укрощаем задачу по принципу «разделяй и властвуй», разбивая на отдельные функциональные области. Работая, например, с меню мы разделяем всё его многообразие функций на те, что отвечают за организацию пунктов меню по принципу container+item, и все остальные.
Для контракта item можем определить некоторую функцию getParent() { return $(this.dom).closest(".container"); }. Для контейнера целую россыпь функций вроде remoteItem, upItem, downItem и т.п. Используя в селекторах только .container и .item вы знаете, что одни и те же функции вы можете использовать для списков, меню, закладок и любых других видов коллекций. Или, другими словами, вы пишите функции по работе со списками и контейнерами один раз, в дальнейшем их просто используете. Если вы добавили новую функцию в свою библиотеку, то она автоматически может быть использована везде, где контейнеры были организованы таким образом. Это замечательно работает в программировании, так почему бы этому принципу не сработать и здесь?
Пример
Давайте попробуем экстраполируем этот пример, скажем, на табы.
<div class="tabbed">
<ul class="_tabs container selectable">
<li class="tab item selected">…</li>
<li class="tab item">…</li>
…
</ul>
<div class="_pages container selectable">
<div class="panel item selected">…</div>
<div class="panel item">…</div>
…
</div>
</div>
Интерфейс tabbed берет на себя обязательства предоставить доступ к контейнерам _tabs и _pages. Это значит, что если у вас на руках есть объект, реализующий этот контракт, то вы вправе вызвать методы getTabs и getPages, чтобы получить доступ к соответствующим контейнерам. Про последние мы знаем, что они реализуют контракт container, а значит имеют на руках все необходимые функции для работы с коллекцией закладок, в том числе обрабатывать first/last/odd/even и прочие типовые стили для списков. То есть изобретать заново функцию для удаления таба будет не нужно.
К контейнерам добавился еще один контракт — selectable. Он может брать на себя обязательства по предоставлению функции для определения текущего выбранного элемента, менять выбранный элемент и т.п. Например, функции:
getSelected: function() {
return $(this.dom).find(".item.selected").first();
},
deselect: function() {
$(this.dom).getSelected().removeClass("selected") ;
},
select: function (item) {
this.deselect(); $(item.dom).addClass("selected");
}
И опять же, эти функции будут одинаково хорошо работать для списков, выпадающих список, панелбаров, слайдеров и всех прочих типов виджетов, где предполагается наличие выбранного элемента, а не только для табов. Когда вы находите и исправляете ошибку в этих функциях, то вы исправляете ее везде.
Если вы, скажем, захотите добавить возможность перетаскивать табы drag'n'drop-ом, то просто добавите контракты drag/drop, что позволит сразу задействовать все те типовые функции, которые были для этой цели уже подготовлены.
Но стоит только написать в БЭМ-стиле tabbed_tabs__item, как краски сразу потускнеют. Ведь весь тот код, который всегда хорошо работал с селектором ".item", для самых разных виджетов, перестал работать. У вас были скины, с самыми различными отображениями табов, но они теперь тоже не работают. Мне могут возразить, что имеют место специальные расширения для того же jQuery, которые помогут разобраться со всем этим синтаксическим мусором. Но мне пока не понятно, зачем создавать проблемы, а потом их решать.
Стандарты
Естественно, все эти «контракты», как и интерфейсы в объектно-ориентированном дизайне, должны быть систематизированы и разложены по полочкам. В программировании это зачастую решается за счет использования пространств имен. Но делается это один раз, а потом используется сколь угодно долго и на любом количестве проектов.
На базе таких контрактов гораздо проще ввести внутренние стандарты и реализовать валидаторы, которые будут держать качество кода на постоянном контроле. Это позволит всем участникам процесса, в том числе и вновь подключившимся, мыслить в одном ключе. В начале — императивно, а потом уже по привычке. Ведь как в объектно-ориентированном дизайне контракты не вводятся просто так, а являются следствием продуманного акта, так и в CSS имена классов не должны возникать спонтанно, необдуманно. Ведь БЭМ хоть и пытается контролировать структурный аспект разработки, но совершенно упускает системный. Кто-то табы назовет tabs, кто-то tab-control, иной tab-widget. Например, форма логина у Яндекса из примера в начале статьи называется «b-domik». Это же так мило, назвать ее не login, signup или еще как-нибудь осмысленно, а «би-домик». Вспоминается старое институтское «пи с домиком и пи с душечкой». Верх креатива, но ад для валидации и рефакторинга. Если вы внезапно захотите найти все формы логина и добавить туда новые опции (скажем, идентификацию через какой-нибудь новый социальный сервис), то вам предстоит отловить этот би-домик, узнать, что в почте яндекса он уже зовется b-mail-domik, ссылка «Войти в почту» c титульной страницы уже содержит форму логина с классом b-popupa__wrap. А сколько вам еще подготовлено «открытий чудных»…
Мне кажется, что верстальщики вообще не должны выдумывать имена классов. Обеспечение интерфейсов между HTML и JS должно осуществляться проектировщиками. Они разрабатывают контракты, систематизируют и доносят эту информацию до тружеников HTML/CSS. Это дает и масштабируемость JS-кода, и реюзинг, и рефакторинг, и валидацию в соответствии с корпоративными стандартами, а значит — качество. Мне кажется этого вполне достаточно для того, чтобы был повод поразмышлять.
CSS-препроцессоры
Под конец замечу, что такие стили достаточно легко и органично описываются в духе LESS/SASS/Stylus-препроцессоров:
.tabbed {
…
._tabs {
….
.item {
….
.selected {
…
}
}
}
…
}
Таким образом можно легко собирать и повторно использовать различные скины. Причем, при компиляции все это породит семантически похожие на БЭМ селекторы, поскольку нет никакой существенной разницы между .tabbed ._tabs .item {…} и .tabbed_tabs__item {…}.
Автор: Guderian