Заметки Андрея Романова

Пишу о жизни, учёбе и работе

Почему CSS-модули не могут заменить БЭМ

Часто слышу, как разработчики говорят «БЭМ не нужен, ведь есть CSS-модули». Это не так.

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

CSS-модули это инструмент, который решает только проблему изоляции стилей. Все остальные проблемы остаются нерешёнными: вам всё ещё нужны какие-то правила для разделения интерфейса на независимые части и всё ещё нужно придумывать названия классов. Поэтому CSS-модули можно и нужно применять вместе с БЭМом.

Эволюция выглядит так:

/* Классический БЭМ с длинными именами классов для обеспечения изоляции */

.shop-cart-button {}
.shop-cart-button_size_small {}
.shop-cart-button_size_large {}


/* CSS-модули с неограниченной свободой творчества в именах классов */

.button {}
.small {}
.large {}
/* или */
.button {}
.is-small {}
.is-large {}
/* или */
.button {}
.size-small {}
.size-large {}


/* БЭМ и CSS-модули */

.button {}
.button_size_small {}
.button_size_large {}

Сразу отвечу на вопрос «а чем плох пример с классами .button, .small и .large?». Он плох тем, что классы .small и .large сами по себе не несут информации о том, к чему они относятся. Нельзя понять, стилизуют ли они отдельный элемент или описывают состояние существующего элемента. Также такие названия классов рано или поздно снова приведут вас к проблеме уникальности имён. Например, вы пишете стили для модального окна. Вам нужно стилизовать полупрозрачный оверлей поверх страницы и само модальное окно. Оба этих элемента могут быть в двух состояниях: виден или скрыт. Кажется, что класс .visible отлично подходит, но проблема в том, что для оверлея и для окна этот класс должен содержать разные стили. Можно придумать костыль в виде селекторов .overlay.visible и .window.visible, но это именно костыль, потому что вы увеличиваете специфичность. С БЭМом всё просто и без ненужного роста специфичности: .overlay_visible и .window_visible.

Какие каналы читать в Телеграме

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

For Web — полезности для разработчиков интерфейсов: фронтенд, дизайн и программирование. Признаюсь, слукавил: этот канал я не читаю, я в него пишу.

Веб-стандарты — ежедневные новости и события фронтенда.

Breakfast.js — ежедневная утренняя порция фронтенд-новостей от Дмитрия Мананникова.

devSchachtChannel — анонсы новых статей и подкастов от devSchacht.

Иван Акулов про фронтенд — заметки про фронтенд, UX и смежные темы.

webo_ru — Виталий Харисов из Яндекса об оптимизации сайтов. Есть английская версия.

Code Hipsters — «квинтэссенция людей и технологий, призванная разрушать стереотипы». Фронтенд, машинное обучение, большие данные, философия программирования.

Про руководство разработчиками — руководитель службы разработки интерфейсов Олег Мохов из екатеринбуржского Яндекса о своей работе и её особенностях.

HR отвечает — Вероника Ильина о поиске работы, хороших резюме, собеседованиях и специфике работы эйчаров.

Всё о программировании (с Козулей) — Владислав Козуля о карьере, стартапах и своём опыте разработки.

isqualog — Софья Ильинова о фронтенде, дизайне, работе и жизни.

Internet 9000 — Сергей Сурганов о дизайне и технологиях.

TechSparks — Андрей Себрант с новостями хайтека.

Записки Коха — Артур Кох о email-разработке и смежных темах.

Evil Martians — Злые марсиане о стартапах, веб-разработке, интернет-бизнесе, бэкенде, фронтенде, мобильной разработке, devops и data science.

запуск завтра — техдир «Медузы» Самат Галимов о своих буднях и технологиях.

Про Сидней/Atlassian — Дмитрий Сорин об опыте переезда в Австралию и работе в Atlassian.

Интерфейсы без шелухи — Антон Жиянов о продуктоводстве, интерфейсах, здравом смысле и разработке софта.

Чёт приуныл — Ян Хацкевич с анонсами новых статей из своего личного блога о дизайне.

Design without cats — Андрей Болонев о дизайне без котиков. Мысли, цитаты из статей, реальные кейсы.

Design & Productivity — Костя Горский про дизайн, продуктивность и жизнь. Мало ссылок и много мыслей.

GOV — о дизайне государства.

Канал Ильи Бирмана — заметки, находки и советы о дизайне.

Л — Людвиг Быстроновский о дизайне, искусстве, продуктивности.

Бюро Горбунова — дизайн-советы, наблюдения и книги.

No Flame No Game — Аня Булдакова о продуктовом менеджменте.

Главред — Максим Ильяхов с советами и статьями о тексте, редактуре, информационном стиле и рекламе.

ГЗОМ — о тексте, тонкостях и пунктирностях русского языка, грамматике и стилистике, работе редактора, работе мозга — затейливо и с внятной аргументацией.

Мильчин-канал — инъекции «Справочника издателя и автора» Аркадия Мильчина и Людмилы Чельцовой. Канал для редакторов, дизайнеров и зануд.

Исправил — Никита Юкович о русском языке и о том, как не ошибаться.

Angry Translator — Даниил Орловский о тонкостях английской грамматики, типичных и нетипичных ошибках, лексике.

Сразу нет — Иван Колпаков из «Медузы» о журналистике.

Разумная внимательность — Рахим Давлеткалиев об осознанности, медитации, разуме и реальности.

За бугром — размышления иммигранта о жизни в США.

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

Че Ченел — Илья и Стася Чекальские о путешествиях, переездах и жизни в Польше.

Тинькофф-журнал — новые cтатьи об управлении деньгами. Как экономить, вкладывать, защищать свои права и общаться с банками.

А что читаете вы?

Визуальная семантика, или чем разметка отличается от стилей

Иногда новички во фронтенде не понимают истинного предназначения разметки (HTML) и стилей (CSS). Однажды в комментариях к одной из публикаций Форвеба возник вопрос:

Что в 2017 лучше использовать для вёрстки таблиц, HTML-таблицы или CSS-гриды?

Что не так с этим вопросом? Давайте разберёмся.

Разметка — про семантику

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

Стили — про внешний вид

Голый текст это хорошо, но ещё лучше, когда его можно оформить — задать размер шрифта и интерлиньяж, ограничить ширину, выделить заголовки, покрасить ссылки в нужный цвет. За всю эту внешнюю красоту отвечают стили (CSS), и они чаще всего никак не влияют на семантику, то есть на смысл текста. Они лишь дают возможность изменить его внешний вид.
Хороший пример — CSS Zen Garden. Один и тот же HTML-документ, смысл которого не меняется, оформлен более чем двумястами разными способами.

Если разметка не отвечает за визуальное представление, что в HTML делают элементы вроде <i> или <strong>?

Дело в том, что эти элементы тоже несут смысл. Например, так определяется элемент <strong> в спецификации HTML:

Элемент <strong> означает важность, серьёзность или срочность его содержимого.

Заметьте, ни слова не сказано о выделении содержимого жирным начертанием шрифта. Жирное начертание для <b> и <strong>, фоновое выделение для <mark> и другое подобное оформление задаётся стандартыми стилями браузера (user-agent stylesheet). Эти стили никак не относятся к семантике разметки. Более того, в разных условиях одни и те же HTML-элементы могут быть визуально представлены по-разному (например, элементы форм на Windows и macOS). Несмотря на разный внешний вид, кнопки на Windows и кнопки на macOS несут один и тот же смысл.

Разделяйте семантику и представление

Вернёмся к вопросу из начала статьи:

Что в 2017 лучше использовать для вёрстки таблиц, HTML-таблицы или CSS-гриды?

Таблицы нужно размечать как таблицы, то есть тегами <table>, <td> и так далее. Как они будут выглядеть на экране у пользователя — дело ваше, вы полностью контролируете их внешний вид через стили. Ничего не мешает разметить HTML-таблицу и стилизовать её CSS-гридами или флексами.

Запомните: HTML — про семантику, CSS — про внешний вид.

Что за атрибут inert и зачем он нужен?

Как можно догадаться из названия, этот атрибут помечает элемент как инертный (или неактивный, но не путайте с disabled). Для таких элементов (и всего дерева их потомков) отключается срабатывание пользовательских событий (например, фокус по нажатию tab, выделение текста, клики). Ассистивные технологии вроде экранных читалок просто игнорируют такие элементы. Также спецификация рекомендует разработчикам браузеров игнорировать инертные элементы при поиске по содержимому страницы.

По сути, этот атрибут сочетает в себе поведение tabindex=”-1”, aria-hidden и pointer-events: none. Им следует помечать скрытые модальные окна, выпадающие меню, невидимые слайды карусели и другие подобные элементы интерфейса. Это улучшит доступность ваших интерфейсов: при навигации с клавиатуры или при использовании экранных читалок инертные элементы просто будут игнорироваться.

Полезные ссылки:
— атрибут inert в спецификации whatwg;
история появления, способы применения, описание пробелов в спецификации и полифил;
выпуск A11ycasts с Робом Додсоном, посвящённый атрибуту inert.

Зависимости в компонентном вебе

Обложка статьи

14 апреля я побывал в Екатеринбурге на конференции DUMP 2017. Больше всего мне понравился доклад Владимира Гриненко об управлении зависимостями в компонентном вебе, так что я вкратце перескажу здесь его суть.

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

/* application.js */
import Button from '../../ui/button.js';
import Link from '../../ui/link.js';
/* application.css */
@import "../../ui/button.css";
@import "../../ui/link.css";

Недостатки такого подхода:

  • многословность;
  • на каждую технологию (скрипты, стили, etc) нужно писать свои импорты;
  • пути к файлам захардкожены, меняется путь — нужно переписывать импорты.

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

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

/* было, application.js */
import Button from '../../ui/button.js';
import Link from '../../ui/link.js';

/* было, application.css */
@import "../../ui/button.css";
@import "../../ui/link.css";

/* стало, application.decl.js */
['button', 'link']

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

Алгебра деклараций

Декларации зависимостей можно легко объединять, вычитать и находить их пересечение. Например, это может быть полезно для выноса общих зависимостей в один файл:

/* login.js */
['header', 'input', 'checkbox', 'button', 'footer']

/* landing.js */
['header', 'slider', 'gallery', 'button','footer']

/* пересечение, которое можно вынести в common.js */
['header', 'button', 'footer']

Композиция

Зависимости между компонентами тоже можно объявлять в виде деклараций. Было:

/* header.js */
import Button from '../../ui/button.js';
import Link from '../../ui/link.js';
// ...

export default class Header {}
/* header.css */
@import '../../ui/button.css';
@import '../../ui/link.css';

Стало:

/* header.deps.js */
['button', 'link'/*, ... */]

/* header.js */
export default class Header {}

Декларативное множественное наследование

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

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

Уровни переопределения

Ещё одна мощнейшая штука, которая есть в БЭМе — уровни переопределения. Наглядная демонстрация принципа:

Иллюстрация уровней переопределения в БЭМ

Расписывать здесь суть уровней переопределения я не стану, ибо она уже описана в документации БЭМа. Приведу реальный случай, в котором уровни переопределения сильно облегчают жизнь.

Яндекс любит проверять разные продуктовые гипотезы и проводить эксперименты. Например, команда поиска может выдвинуть такую гипотезу: «пользователи станут чаще переходить по рекламным ссылкам, если рекламу показывать на красном фоне». Чтобы проверить эту гипотезу, нужно провести А/Б-тестирование, то есть одной половине пользователей показать рекламупо-старому, а другой половине показать рекламу на красном фоне. После этого нужно сравнить результаты и проверить, действительно ли во втором случае пользователи кликали чаще.

Скорее всего, вы бы решили эту задачу так: в коде, отвечающем за показ рекламного блока, добавили бы условие наподобие «если пользователь входит в такую-то группу, добавляем сюда красный фон». Проблема в том, что таких экспериментов может быть очень много, и при таком подходе весь ваш код будет состоять из сплошных условий, которые ещё и нужно не забывать удалять. Явно немасштабируемый подход.

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

Возвращаясь к теме декларации зависимостей, в случае с уровнями переопределения мы, опять же, выигрываем:

/* было */
@import "common/button.css";
@import "project/button.css";
@import "experiment/button.css";
/* стало */
['button']

А как это использовать?

Для Реакта есть bem-react-core, для всего остального есть bem-sdk.

Ctrl + ↓ Earlier