Логотип блога Владислава Иванова

Введение

Привет! Если Вы веб разработчик или когда либо имели удовольствие писать фронтенд Вы должно быть знаете что такое CSS. Если Вы когда нибудь писали фронтенд для достаточно большого сервиса Вы наверное сталкивались с тем, что открывая файл со стилями годовалой давности непонятно ничего от слова совсем!

Разбираться в большом CSS файле достаточно трудно. Особенно, если этот файл использует препроцессоры, например, SCSS, SASS и другие. К и так не всегда понятным с первого взгляда селекторам добавляются переменные, миксины и самое страшное - вложенности!

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

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

Если следовать БЭМ методологии и некоторому соглашению о разбиении файлов исходного кода, можно прийти в утопический мир будущего, где почти всегда в ОДНОМ файле стили для ОДНОГО элемента, а читая код не возникает примерно такой реакции:

Мем с Шелдоном Купером "Почему?"

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

Идея

Вы наверное знаете что такое css-loader и как он позволяет работать с CSS модулями. Если же это не так, то позвольте показать что это такое на примере React компонента:

javascript
import styles from './Button.css';

export const Button = ({ children }) => {
  return <button type="button" className={styles.Button}>{children}</button>;
}

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

Если разработка ведется с использованием БЭМ методологии, то это может выглядеть примерно так:

javascript
import cls from 'classnames';
import styles from './Button.css';

// type: primary | secondary

export const Button = ({ children, type }) => {
  return (
    <button 
      type="button" 
      className={cls(styles.Button, {
        [styles.Button_type_primary]: type === 'primary',
        [styles.Button_type_secondary]: type === 'secondary'
      })}
    >
      {children}
    </button>
  );
}

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

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

Подождите! Но ведь если у нас есть конкретные правила именования не можем ли мы сами написать функцию по аналогии с cls, которая могла бы включать определенные модификаторы в зависимости от передаваемых опций. Давайте попробуем:

javascript
import styles from './Button.css';

const cls = (base, mods) => {
  let names = Object.entries(mods).map(([modName, modVal]) => {
    return `${base}_${modName}_${modVal}`;
  });

  return names.concat([base]).join(' ');
}

// type: primary | secondary

export const Button = ({ children, type }) => {
  return (
    <button 
      type="button" 
      className={cls(styles.Button, { type })}
    >
      {children}
    </button>
  );
}

Выглядит круто! Но неудобно, что нужно повсюду таскать за собой cls функцию. К тому же, очень трудно предусмотреть все сценарии применения такой функции. Например, Если БЭМ модификатор булевый, то значение модификатора true можно опустить и использовать просто имя, например Button_disabled.

Ну что ж, встречайте!

Реализация

Для начала просто посмотрите небольшое демо:

Functional BEM демо

По сути все то, о чем я писал выше объединено воедино, а сверху еще и добавлена поддержка TypeScript.

Все это называется functional BEM и вы можете узнать о проекте подробнее здесь https://github.com/yungvldai/fbem. Все пакеты @fbem являются open-source проектами и распространяются под лицензией MIT, а это значит, что вы можете использовать их в своих проектах прямо сейчас!

В основе проекта лежит специальный лоадер @fbem/css-loader для webpack, который позволяет импортировать из CSS файлов специальные БЭМ-функции. У БЭМ функции всегда два аргумента: объект модификаторов, где ключ - имя модификатора, а значение - его значение; второй - массив миксов (термин из методологии). Проще говоря, это просто массив строк, которые будут добавлены в конец результирующего класса.

@fbem/css-loader очень схож с css-loader (и на самом деле его форк) и имеет почти тот же набор опций и функционал. Вы также можете использовать его с различными препроцессорами с помощью чейнинга лоадеров в конфиге webpack (но подумайте действительно ли они вам нужны 😏).

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

Для этих целей в @fbem предусмотрена специальная функция compose в пакете utils. Эта функция позволяет комбинировать несколько БЭМ-функций в одну:

javascript
import { compose } from '@fbem/utils';

import { cnButton as modDisabled } from './_disabled/button_disabled.css';
import { cnButton as modStyle } from './_style/button_style.css';
import { cnButton as base } from './button.css';

const cnButton = compose(base, modDisabled, modStyle);

cnButton({ style: 'flat', disabled: true }, ['mix']); // 'button button_style_flat button_disabled mix'

Заключение

В этой статье я постарался показать метод (и инструменты для реализации) организации стилей в проекте. Больше информации о @fbem вы сможете найти на странице проекта на Github. Я надеюсь Вам было интересно узнать о моих наработках, а также надеюсь Вы поделитесь этой статьей с кем-нибудь 😉.

Спасибо!

P.S. Этот блок написан с использованием @fbem.

Поддерживается markdown

Пока нет комментариев