Об’єктноорієнтований CSS: чому елементарні правки призводять до несподіваних наслідків та як із цим боротися

Усі статті, обговорення, новини про Front-end — в одному місці. Підписуйтеся на телеграм-канал!

Я — Євгеній Вінійчук, у розробці вже понад 7 років. У компанії Youshido виріс від Trainee до Lead Front-end Developer. Створював сайти та застосунки як для стартапів, так і для бізнес-гігантів — Viasat, Jacobs, Pepsi, Tuborg тощо. Зараз працюю Senior Frontend Developer в ІТ-компанії ButterflyMX. У цьому матеріалі розкажу про те, чому об’єктноорієнтований підхід є невіддільною частиною написання CSS та як розбивання сторінки на блоки полегшує роботу ІТ-спеціалісту, а користування ресурсом — читачеві сайту.

Раптовий злам програми — одна з найбільш поширених проблем розробників-початківців. Часом мінімальні зміни призводять до несподіваних наслідків. Змінивши вигляд сторінки профілю користувача, можна нашкодити відображенню коментарів на іншій сторінці. Зменшити ймовірність виникнення такої ситуації дозволяє об’єктноорієнтований CSS (OOCSS) — методологія, що розглядає елементи сторінки як незалежні будівельні блоки.

Значна частина проблем стається через відсутність структури та стратегії під час розв’язання задачі. Розгляньмо типовий приклад рішення «в лоб». Нехай нам потрібно зверстати сторінку. Починаємо з «шапки», де є кнопка «Sign In»:

Для її стилізації можна написати щось на кшталт такого:

#header .button {
  background: black;
  color: white;
  margin-left: 10px;
  padding: 5px;
  border: 2px solid transparent;
  border-radius: 20px;
}

Продовжуючи працювати над сторінкою, натрапляємо на блок, де зустрічаємо вже знайому кнопку:

Оскільки кнопка в блоці Get out, тоді логічним буде:

#get-out .button {
  color: black;
  padding: 0 10px;
  border: 2px solid black;
  border-radius: 20px;
  margin-top: 20px
}

Цей код буде працювати, проте виникають такі питання:

— А якщо така кнопка потрібна ще на декількох сторінках?

— А якщо усім кнопкам такого типу треба, наприклад, поміняти поля?

Не відповівши на ці фундаментальні питання з самого початку, згодом отримаємо помилки в місцях, де точно їх не очікували. Такий код стає сплутаним, а його підтримка ускладнюється. Ось один із реальних прикладів того, як виглядатиме код без структури з часом (зверніть увагу на рівень вкладеності класу .icon):

Приклад заплутаного коду, який писався без попереднього аналізу структури

Щоб покращити можливості масштабування та зменшити рівень складності, пропонується методологія OOCSS. В її основі лежать наступні ідеї.

Розділення структурних та стильових характеристик елементів

Цей принцип про те, що стильові характеристики елемента (колір, фон, рамка тощо) мають описуватися окремо від розмірних характеристик (ширина/висота/поля тощо). У такий спосіб ми налаштовуємо каркас елемента, а потім одягаємо поверх нього різні «вбрання». Це дозволяє уникнути дублювання коду та повторно використовувати елемент, додаючи йому нового функціоналу разом із появою нових потреб.

Розглянемо приклад проблеми та її розв’язання:

.button-green {
  padding: 0 20px;
  height: 42px;
  text-align: center;
  color: green;
  border: 1px solid green;
  border-radius: 20px;
}

.button-red {
  padding: 0 20px;
  height: 42px;
  text-align: center;
  color: red;
  border: 1px solid red;
  border-radius: 20px;
}

Вище ми описали дві кнопки. В цьому випадку, щоб змінити загальну характеристику для всіх кнопок (наприклад, поля), нам потрібно модифікувати кожен клас. Тобто зараз маємо ситуацію, коли елементи, який мають спільну характеристику, існують окремо як повністю незлагоджені. Якщо використати правило про розділення структури та зовнішнього вигляду, матимемо наступне:

.button {
  padding: 0 20px;
  height: 42px;
  text-align: center;
  border-radius: 20px;
}

.button-green {
  border: 1px solid green;
  border-radius: 20px;
}

.button-red { 
  color: red;
  border: 1px solid red;
}

Тепер ми розділили структуру від стилізації. До того ж чітко простежується, що кнопки успадковують спільні характеристики від «материнської», не виникає питання «міняючи поля зеленої кнопки, чи повинні змінитися поля червоної?».

Розділення на структурні та контентні елементи

Згідно з методологією, контентними елементами виступають параграфи, зображення, кнопки та інші, що вкладають в структурні елементи (контейнери). Ідея полягає в тому, що стилізація контентних елементів має не залежати від батьківського елемента. Тобто намагатися писати стилі контентних елементів без залежності від того, в якому контейнері вони лежать.

Розглянемо приклад проблеми та її вирішення:

#header {
  background: #fcfcfc;
}

#header .nav {
  padding: 0 20px;
}

#header .nav .nav-item { 
  color: #000000;
  font-size: 14px;
  font-weight: 500;
  margin: 0 20px;
}

У цьому прикладі ми стилізували навігацію та її елементи в «шапці» сторінки. Нехай, нам потрібна така ж навігація також в футері. Як бути тоді? Нашвидкуруч можемо зробити щось таке:

#header {
  background: #fcfcfc;
}

#header .nav,
#footer .nav {
  padding: 0 20px;
}

#header .nav .nav-item
#footer .nav .nav-item { 
  color: #000000;
  font-size: 14px;
  font-weight: 500;
  margin: 0 20px;
}

#footer {
  background: #000000;
}

#footer .nav .nav-item {
  color: #ffffff
}

Задачу вирішено, але маємо декілька проблем. По-перше, це продубльовані стилі для хедера та футера. По-друге, навігація виявилась тісно зв’язана із контейнером, в якому знаходиться, що унеможливлює її використання поза його межами. Такі стилі буде важко масштабувати з ростом проєкту через надмірну зв’язність. Покращмо це рішення, відділивши контейнери (хедер та футер) від контенту (навігації):

#header {
  background: #fcfcfc;
}

#footer {
  background: #000000;
}

.nav {
  padding: 0 20px;
}

.nav-item { 
  font-size: 14px;
  font-weight: 500;
}

.nav-item-white {
  color: #ffffff;
}

.nav-item-black {
  color: #000000;
}

Код вище дозволяє використовувати елементи навігації незалежно від того, де саме вони будуть розташовані. До того ж ми використали перше правило про розділення структури елементів та його зображення. Цей код значно легше підтримувати завдяки зменшеному рівню складності.

Переваги OOCSS

Структурованість

Використання OOCSS спонукає спочатку проаналізувати весь дизайн та виділити функціональні елементи, поставити собі питання «В яких станах ці елементи можуть перебувати?» та розробити елементи, з яких потім будувати додаток. Тобто ми фокусуємося на конкретних елементах, приділяючи їм більше уваги в певний момент часу та розв’язуємо задачу комплексно.

Низька зв’язність

Завдяки розділенню відповідальності, елементи залежать один від одного лише структурно (наприклад, кнопці налаштовують відступ відносно її контейнера, коли ставлять поруч з іншими блоками). Це дозволяє легко замінити один елемент на інший та повторно використовувати блоки незалежно від розташування.

Завдяки вищенаведеним перевагам, слідування правилам OOCSS зменшує кількість помилок та покращується масштабованість коду. Його легше читати, що підвищує ймовірність того, що розробники зрозуміють код однаково. Окрім того, це полегшує інтеграцію нових розробників в проєкт.

А що далі?

OOCSS допомагає розділяти відповідальність, але він не диктує чітких правил щодо структури коду. Тому дуже раджу звернути увагу на методологію BEM (block, element, modifier), яка продовжує ідею OOCSS та дає чіткі поради з неймінгу та структури. Вона є однією із найуживаніших та є невіддільною частиною в багатьох компаніях.

Використання об’єктноорієнтованого підходу до CSS буде корисним усім розробникам, оскільки це є базою до структурування коду. Модульна система ізолює блоки, робить код більш масштабованим і дозволяє уникати типових CSS-проблем.

👍ПодобаєтьсяСподобалось5
До обраногоВ обраному1
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

— CSS: чому елементарні правки призводять до несподіваних наслідків
— Тому що це і є природа CSS. Ви мені не вірите? Тоді я скажу магічне слово !important

Я завжди стараюсь делегувати написання CSS 😁

В CSS C значить Cascading, тобто окремі блоки за дизайном не ізолюються, і неможливо створити об’єктно орієнтовану модель. І каскадуються не всі проперті. Особливо цікаво з font, font-family, line-height. Тому найкраща методологія працювати з CSS це файрфрейми і тули по типу Figma, Adobe XD, Photoshop, і подібні. В термінах OOP в CSS легко робити конкретну імплементацію, але важко розробляти базові компоненти. Можете посміятись над намаганнями щось там ізолювати і ООП-шити змінивши line-height в body і порахуйте скільки ’ізольваних’ елементів це розілюзувало.

наче ж шедоу дом цю проблему вирішує, чи я помиляюся?

не про це шедоу дом. Це про те як створити компонент а не про недоцільність / обмеженість підходу з компонентами в web через дизайн самого CSS.

Ось чому мені, описаний автором підхід, нагадує джуна, котрий дізнався про якусь технологію, і пхає її усюди.

#header
#footer

Даже не уверен есть ли смысл читать автора, который не знает про существование тегов
header footer
(а они поддерживаются еще с хрома 5, на дворе хром 102)

ніщо не заважає на тег footer додати id footer, а селектор по айді юзати для більшої специфічності. бо футерів на сторінці може бути більше, ніж один. і взагалі, неізольовані селектори по тегам — така собі практика.

на тег footer додати id footer

И потом вы все ноете что в фронт-енде какое-то месиво, все скатилось и «раньше было лучше».
А можно не добавлять и держать код чистым, чтобы погоня за специфичностью не локнулась в !important.

бо футерів на сторінці може бути більше, ніж один

Та можно и десяток body тегов насовать, как-то и зачем-то браузер это пережует — но зачем?

я особисто просто юзаю інкапсуляцію стилів та не нию.

Та можно и десяток body тегов насовать, как-то и зачем-то браузер это пережует — но зачем?

дещо невалідно порівнювати body, який по специфікації повинен бути лише один і футер, який по специфікації, наприклад, може бути свій у кожного article. developer.mozilla.org/...​t/footer?retiredLocale=uk

тож юзати селектори по тегам мінімально — це типу один із показників чистого кода.

У нас вообще styled components и проблем с оверрайдом стилей ровно 0 (хоть как там внутри стили не пиши).

Что касается футера артиклов —
body > footer 
article footer
но кейс редкий достаточно.

Селекторы по тегам были моветоном в HTML4. HTML5 по большей части был создан для того, чтобы люди начали юзать кто-то кроме div

styles-components — то круто, але не на кожному проєкті вони є.

семантика — вона про пошуковики та кращу читабельність HTML. якщо в тебе купа різних тегів на сторінці — це не для того, щоб таргетити їх цссом.

головна проблема селекторів по тегам — це різниця в вазі пріорітетності відносно класу. куди зручніше, коли стиль прописан класу та лише одному класу (за виключенням деяких сценаріїв). тоді не треба плясати з бубном, щоб перебити щось типу div.container ul > li + .active

і з отаким, до речі, той самий БЕМ доволі непогано допомогає.

То если совсем ко всему документу вешать файл аля styles.css

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

.news-widget {
  header {...}
  footer {...}
  .buttonsHolder {...}
  .breadCrumbs {
    ul {
      li {...}
    }
  }
}

Легче всего понимать и сравнивать на живых примерах.

Вот хороший проект, где сверстали один тот же примитивный сайт на всех популярных css-методологиях: github.com/...​shaOleg/holy-grail-markup включая и OOCSS.

Там наглядно можно увидеть разницу.

говнокодити можна і з сцсс, якщо що.

Сьогодні день якийсь всратий з самого ранку. То на лінкедіні пости про досягнення Савєцкага Саюза та вкусний пломбір, то на доу пропонують використовувати лайно від яндекса.

Не подобається BEM можна спробувати CUBE (Composition Utility Block Exception) підхід.
Можливо буде дивно, але навіть у 2022 ~95% сайтів Інтернету не викроистовують жоден з компонентних JS-фреймворків (якщо покладатись на w3techs.com/...​erview/javascript_library), але усі юзають CSS. Структурувати код подтрібно. BEM/CUBE/SMACSS досить добре з цим справляються. Якщо є можливість то звичайно краще CSS Modules + Vue/React/Angular/Solid/Svelte/Elm/etc... але часто буває потрібно в чистий CSS до HTML шаблонів і тільки так.

ну а що принципово не так з БЕМ, окрім руснявого коріння (хоча наче воно є продовженням SMACSS) та громіздких класів (що вирішується препроцесорами)?

якщо, звісно немає можливості тупо увімкнути CSS Modules. або заюзати один з 100500 CSS-фреймворків, то, ІМХО, все краще атомарного підхода і, тим паче, безконтрольного пекла каскадів та пріоритетності класів.

особисто я своїм студентикам обовʼязково даю проект на БЕМ + SCSS + ejs, до Реакту, щоб розібралися з компонентним підходом та потім менше говнокодили на фреймворку.

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

Ну от я згоден, що зараз актуальних юз-кейсів під БЕМ не те, щоб багато.

Але, певен, є ситуації, коли спа — оверкіл (або не зручний з інших причин), сучасні білд-тули не доступні, але проєкт достатньо великий, щоб заплутатись у стилях. Наприклад, при роботі з якимись не дуже вдалими з точки зору фронта цмс (якось так з кентіко працював, боже збав).

Ну і якщо фронт обмежується майже самою лише версткою з мінімумом джса, то щось типу БЕМа може бути варіантом. Особливо якщо обирати між БЕМом та нічим.

Карочі, імхо, для свого часу то була непогана спроба організувати хаос. Хоч зараз в більшості випадків можна просто заюзати цсс модулі або шедоу дом.

изоляцию стилей можно сделать кучей способов, даже уже и на числом css.

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

Внутри ангуляр-компонента все равно надо писать стили и желательно хоть как-то разумно.

Нє, я згоден. Саме тому мені подобається розкривати компонентний підхід для новачків саме на прикладі БЕМ.

Тут скоріше про те, що коли компонент правильно декомпозован, а стилі ізольовані — проблем, які вирішує БЕМ, навіть і не виникне. І точно нема необхідності писати в кожному тезі класи на 1к+ символів, коли в тебе є цсс-модули, або шедоу дом, або ти юзаєш рішення цсс-ін-джс.

Имена классов можно писать любые. Смысл БЭМ это декомпозиция, разделение на компоненты. А как именно это будет реализовано технически — уже второстепенно.

вообще-то его придумали украинские разработчики в украинском Крыму, это такое.

SMACSS и БЭМ были придуманы одновременно и независимо друг от друга.
и да, это по сути одно и тоже.

Підписатись на коментарі