Эволюция CSS: от CSS, SASS, BEM и CSS–модулей до styled-components

в 9:24, , рубрики: css, IT-стандарты, ReactJS, sass, Блог компании Mail.Ru Group, БЭМ, верстка, никто не читает теги, Разработка веб-сайтов

Эволюция CSS: от CSS, SASS, BEM и CSS–модулей до styled-components - 1

С самого начала истории интернета мы нуждались в стилях для наших сайтов. Многие годы нам для этого служил CSS, развивавшийся в своём темпе. И здесь мы рассмотрим историю его развития.

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

Дикий CSS

В 1990-е мы увлекались созданием «обалденных» интерфейсов, wow-фактор был самым важным. В те времена ценились inline-стили, и нас не заботило, если какие-то элементы страницы выглядели по-разному. Веб-страницы были милыми игрушками, которые мы насыщали прикольными гифками, бегущими строками и прочими кошмарными (но впечатляющими) элементами, стараясь привлечь внимание посетителей.

Затем мы начали создавать динамические сайты, но CSS оставался оплотом беспредела: каждый разработчик имел собственное представление, как делать CSS. Кто-то боролся со специфичностью (specificity), приводившей к визуальной регрессии при появлении нового кода. Мы полагались на !important, тем самым желая высечь в камне символ нашей воли к тому, чтобы элементы интерфейса выглядели определённым образом. Но вскоре мы поняли:

Эволюция CSS: от CSS, SASS, BEM и CSS–модулей до styled-components - 2

С увеличением размеров и сложности проектов, а также разрастанием команд разработчиков все эти методики превращались во всё более очевидные и крупные проблемы. Поэтому отсутствие закономерностей в применении стилей стало одним из главных препятствий для опытных и неопытных разработчиков, старавшихся найти правильный способ использования CSS. В конце концов мы поняли, что не существует правильных и неправильных способов. Мы лишь старались сделать так, чтобы всё выглядело прилично.

Эволюция CSS: от CSS, SASS, BEM и CSS–модулей до styled-components - 3

SASS спешит на помощь

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

Принцип такой: берётся CSS-код, предварительно обрабатывается, и в общий CSS-пакет помещается скомпилированный файл. Круто? На самом деле не слишком. Через некоторое время стало понятно, что без стратегий и применения лучших методик SASS приносит больше проблем, чем решает.

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

Пока не появился BEM…

BEM и концепция компонентов

BEM стал глотком свежего воздуха. Он позволил нам больше думать о возможности многократного использования и компонентах. По сути, эта технология вывела семантику на новый уровень. Теперь мы могли быть уверены, что className — уникален и что за счёт использования простого соглашения Block, Element, Modifier снижается риск специфического отображения.

Взгляните на пример:

<body class="scenery">
  <section class="scenery__sky">
    <div class="sky [sky--dusk / sky--daytime] [sky--foggy]">
      <div class="sky__clouds"></div>
      <div class="sky__sun"></div>
    </div>
  </section>
  <section class="scenery__ground"></section>
  <section class="scenery__people"></section>
</body>

Если вы проанализируете разметку, то сразу увидите работу соглашения BEM. В коде есть два явных блока: .scenery и .sky. Каждый из них имеет собственные блоки. Лишь у sky есть модификаторы, потому что, к примеру, туман, день или закат — всё это разные характеристики, которые могут применяться к одному и тому же элементу.

Для лучшего анализа взглянем на сопровождающий CSS, содержащий некий псевдокод:

// Block
.scenery {
   //Elements
  &__sky {
    fill: screen;
  }
  &__ground {
    float: bottom; 
  }
  &__people {
    float: center;
  }
}

//Block
.sky {
  background: dusk;
  
  // Elements
  
  &__clouds {
    type: distant;
  }
  
  &__sun {
    strength: .025;
  }
  
  // Modifiers
  &--dusk {
    background: dusk;
    .sky__clouds {
      type: distant;
    }
    .sky__sun {
      strength: .025;
    }
  }
  
  &--daytime {
    background: daylight;
    .sky__clouds {
      type: fluffy;
      float: center;
    }
    .sky__sun {
      strength: .7;
      align: center;
      float: top;
    }
  }
}

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

BEM хорош тем, что делает компоненты уникальными #reusabilityFtw. При таком подходе некоторые паттерны становились очевиднее по мере внедрения нового соглашения в наши старые таблицы стилей.

Но при этом возникли и новые проблемы:

  • Процедура выбора className превратилась в кропотливую задачу.
  • Со всеми этими длинными именами классов разметка стала раздутой.
  • Необходимо явно расширять каждый компонент интерфейса при каждом повторном использовании.
  • Разметка стала излишне семантической.

CSS-модули и локальное пространство видимости

Некоторые проблемы не смогли решить ни SASS, ни BEM. Например, в логике языка отсутствует концепция истинной инкапсуляции. Следовательно, задача выбора имён классов возлагается на разработчика. Чувствовалось, что проблему можно было решить с помощью инструментов, а не соглашений.

Именно это и сделали CSS-модули: в их основе лежит создание динамических имён классов для каждого локально заданного стиля. Это позволило избавиться от визуальных регрессий, возникавших из-за внедрения новых CSS-свойств, теперь все стили инкапсулировались корректно.

CSS-модули быстро стали популярны в экосистеме React, и сегодня они используются во многих проектах. У них есть свои преимущества и недостатки, но в целом это хорошая, полезная парадигма.

Однако… Сами по себе модули не решают ключевых проблем CSS, они лишь показывают нам способ локализации определений стилей: умный способ автоматизации BEM, чтобы нам больше не пришлось заниматься выбором имён классов (ну или хотя бы заниматься этим реже).

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

Вот как выглядит локальный CSS:

@import '~tools/theme';

:local(.root) {
  border: 1px solid;
  font-family: inherit;
  font-size: 12px;
  color: inherit;
  background: none;
  cursor: pointer;
  display: inline-block;
  text-transform: uppercase;
  letter-spacing: 0;
  font-weight: 700;
  outline: none;
  position: relative;
  transition: all 0.3s;
  text-transform: uppercase;
  padding: 10px 20px;
  margin: 0;
  border-radius: 3px;
  text-align: center;
}


@mixin button($bg-color, $font-color) {
  background: $bg-color;
  color: $font-color;
  border-color: $font-color;

  &:focus {
    border-color: $font-color;
    background: $bg-color;
    color: $font-color;
  }

  &:hover {
    color: $font-color;
    background: lighten($bg-color, 20%);
  }

  &:active {
    background: lighten($bg-color, 30%);
    top: 2px;
  }
}

:local(.primary) {
  @include button($color-primary, $color-white)
}

:local(.secondary) {
  @include button($color-white, $color-primary)
}

Это просто CSS, а его главное отличие в том, что все className с добавлением :local будут генерировать уникальные имена классов наподобие:

.app–components–button–__root — 3vvFf {}

Можно сконфигурировать генерируемый идентификатор с помощью параметра запроса localIdentName. Пример: css–loader?localIdentName=[path][name]–––[local]–––[hash:base64:5] для облегчения отладки.

В основе локальных CSS-модулей лежит простая идея. Они являются способом автоматизации BEM-нотации за счёт генерирования уникального className, которое не станет конфликтовать ни с одним другим, даже если будет использоваться одно и то же имя. Весьма удобно.

Полное вливание CSS в JavaScript с помощью styled-components

Styled-components — это визуальные примитивы, работающие как обёртки. Они могут быть привязаны к конкретным HTML-тегам, которые всего лишь обёртывают дочерние компоненты с помощью styled-components.

Этот код поможет понять идею:

import React from "react"
import styled from "styled-components"
// Simple form component

const Input = styled.input`
  background: green
`

const FormWrapper = () => <Input placeholder="hola" />

// What this compiles to:
<input placeholder="hola" class="dxLjPX">Send</input>

Всё очень просто: styled-components использует для описания CSS-свойств шаблонное буквенное обозначение (template literal notation). Похоже, что команда разработчиков попала в точку, объединив возможности ES6 и CSS.

Styled-components предоставляет очень простой паттерн для многократного использования и полностью отделяет интерфейс от компонентов функциональности и структуры. Создаётся API, имеющий доступ к нативным тегам — либо в браузере как HTML, либо нативно используется React Native.

Вот как передаются в styled-components кастомные свойства (или модификаторы):

import styled from "styled-components"

const Sky = styled.section`
  ${props => props.dusk && 'background-color: dusk' }
  ${props => props.day && 'background-color: white' }
  ${props => props.night && 'background-color: black' }
`;

// You can use it like so:
<Sky dusk />
<Sky day />
<Sky night />
  

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

Основной интерфейс может многократно использоваться кем угодно

Стало быстро понятно, что ни CSS-модули, ни styled-components сами по себе не были идеальным решением. Необходим некий паттерн, чтобы всё это эффективно работало и масштабировалось. Такой паттерн возник из определения, чем является компонент, и его полного отделения от логики. Это позволило создать основные компоненты (core components), единственное предназначение которых — стили.

Пример реализации таких компонентов с помощью CSS-модулей:

import React from "react";

import classNames from "classnames";
import styles from "./styles";

const Button = (props) => {
  const { className, children, theme, tag, ...rest } = props;
  const CustomTag = `${tag}`;
  return (
    <CustomTag { ...rest } className={ classNames(styles.root, theme, className) }>
      { children }
    </CustomTag>
  );
};

Button.theme = {
  secondary: styles.secondary,
  primary: styles.primary
};

Button.defaultProps = {
  theme: Button.theme.primary,
  tag: "button"
};

Button.displayName = Button.name;

Button.propTypes = {
  theme: React.PropTypes.string,
  tag: React.PropTypes.string,
  className: React.PropTypes.string,
  children: React.PropTypes.oneOfType([
    React.PropTypes.string,
    React.PropTypes.element,
    React.PropTypes.arrayOf(React.PropTypes.element)
  ])
};


export default Button;

Здесь компонент получает свойства, которые привязаны к дочернему компоненту. Иными словами, компонент-обёртка передаёт все свойства дочернему компоненту.

Теперь ваш компонент можно применить так:

import React from "react"
import Button from "components/core/button"

const = Component = () => <Button theme={ Button.theme.secondary }>Some Button</Button>

export default Component

Продемонстрирую аналогичный пример полной реализации кнопки с помощью styled-components:

import styled from "styled-components";

import {
  theme
} from "ui";

const { color, font, radius, transition } = theme;

export const Button = styled.button`
  background-color: ${color.ghost};
  border: none;
  appearance: none;
  user-select: none;
  border-radius: ${radius};
  color: ${color.base}
  cursor: pointer;
  display: inline-block;
  font-family: inherit;
  font-size: ${font.base};
  font-weight: bold;
  outline: none;
  position: relative;
  text-align: center;
  text-transform: uppercase;
  transition:
    transorm ${transition},
    opacity ${transition};
  white-space: nowrap;
  width: ${props => props.width ? props.width : "auto"};

  &:hover,
  &:focus {
    outline: none;
  }

  &:hover {
    color: ${color.silver};
    opacity: 0.8;
    border-bottom: 3px solid rgba(0,0,0,0.2);
  }

  &:active {
    border-bottom: 1px solid rgba(0,0,0,0.2);
    transform: translateY(2px);
    opacity: 0.95;
  }

  ${props => props.disabled && `
    background-color: ${color.ghost};
    opacity: ${0.4};
    pointer-events: none;
    cursor: not-allowed;
  `}

  ${props => props.primary && `
    background-color: ${color.primary};
    color: ${color.white};
    border-color: ${color.primary};

    &:hover,
    &:active {
      background-color: ${color.primary}; 
      color: ${color.white};
    }
  `}

  ${props => props.secondary && `
    background-color: ${color.secondary};
    color: ${color.white};
    border-color: ${color.secondary};

    &:hover,
    &:active {
      background-color: ${color.secondary}; 
      color: ${color.white};
    }
  `}
`;

Любопытный момент: компонент получается совершенно тупым и служит только обёрткой CSS-свойств, привязанных к родительскому компоненту. У такого подхода есть преимущество:

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

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

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

Получатели свойств

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

// Prop passing Shorthands for Styled-components
export const borderProps = props => css`
  ${props.borderBottom && `border-bottom: ${props.borderWidth || "1px"} solid ${color.border}`};
  ${props.borderTop && `border-top: ${props.borderWidth || "1px"} solid ${color.border}`};
  ${props.borderLeft && `border-left: ${props.borderWidth || "1px"} solid ${color.border}`};
  ${props.borderRight && `border-right: ${props.borderWidth || "1px"} solid ${color.border}`};
`;

export const marginProps = props => css`
  ${props.marginBottom && `margin-bottom: ${typeof (props.marginBottom) === "string" ? props.marginBottom : "1em"}`};
  ${props.marginTop && `margin-top: ${typeof (props.marginTop) === "string" ? props.marginTop : "1em"}`};
  ${props.marginLeft && `margin-left: ${typeof (props.marginLeft) === "string" ? props.marginLeft : "1em"}`};
  ${props.marginRight && `margin-right: ${typeof (props.marginRight) === "string" ? props.marginRight : "1em"}`};
  ${props.margin && `margin: ${typeof (props.margin) === "string" ? props.margin : "1em"}`};
  ${props.marginVertical && `
    margin-top: ${typeof (props.marginVertical) === "string" ? props.marginVertical : "1em"}
    margin-bottom: ${typeof (props.marginVertical) === "string" ? props.marginVertical : "1em"}
  `};
  ${props.marginHorizontal && `
    margin-left: ${typeof (props.marginHorizontal) === "string" ? props.marginHorizontal : "1em"}
    margin-right: ${typeof (props.marginHorizontal) === "string" ? props.marginHorizontal : "1em"}
  `};
`;
// An example of how you can use it with your components

const SomeDiv = styled.div`
  ${borderProps}
  ${marginProps}
`

// This lets you pass all borderProps to the component like so:

<SomeDiv borderTop borderBottom borderLeft borderRight marginVertical>

Пример использования получателей свойств

Это позволяет не хардкодить границы для каждого конкретного компонента, что экономит нам кучу времени.

Placeholder / Функциональность наподобие миксина

В styled-components можно использовать весь потенциал JS, чтобы функции были не просто получателями свойств и чтобы разные компоненты могли совместно использовать код:

// Mixin like functionality

const textInput = props => `
  color: ${props.error ? color.white : color.base};
  background-color: ${props.error ? color.alert : color.white};
`;

export const Input = styled.input`
  ${textInput}
`;

export const Textarea = styled.textarea`
  ${textInput};
  height: ${props => props.height ? props.height : '130px'}
  resize: none;
  overflow: auto;
`;

Компоненты макета

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

import styled from "styled-components";
import {
  theme,
  borderProps,
  sizeProps,
  backgroundColorProps,
  marginProps
} from "ui";

const { color, font, topbar, gutter } = theme;

export const Panel = styled.article`
  ${marginProps}
  padding: 1em;
  background: white;
  color: ${color.black};
  font-size: ${font.base};
  font-weight: 300;
  ${props => !props.noborder && `border: 1px solid ${color.border}`};
  width: ${props => props.width ? props.width : "100%"};
  ${props => borderProps(props)}
  transition: 
    transform 300ms ease-in-out,
    box-shadow 300ms ease-in-out,
    margin 300ms ease-in-out;
  box-shadow: 0 3px 3px rgba(0,0,0,0.1);

  ${props => props.dark && `
    color: ${color.white};
    background-color: ${color.black};
  `}

  &:hover {
    transform: translateY(-5px);
    box-shadow: 0 6px 3px rgba(0,0,0,0.1);
  }
`;

export const ScrollView = styled.section`
  overflow: hidden;
  font-family: ${font.family};
  -webkit-overflow-scrolling: touch;
  overflow-y: auto;
  ${props => props.horizontal && `
    white-space: nowrap;
    overflow-x: auto;
    overflow-y: hidden;
    `
  }
  ${props => sizeProps(props)}
`;

export const MainContent = styled(ScrollView)`
  position: absolute;
  top: ${props => props.topbar ? topbar.height : 0};
  right: 0;
  left: 0;
  bottom: 0;
  font-size: ${font.base};
  padding: ${gutter} 3em;

  ${props => props.bg && `
    background-color: ${props.bg};
  `}
`;

export const Slide = styled.section`
  ${backgroundColorProps}
  font-weight: 400;
  flex: 1;
  height: ${props => props.height ? props.height : "100%"};
  width: ${props => props.width ? props.width : "100%"};
  justify-content: center;
  flex-direction: column;
  align-items: center;
  text-align: center;
  display: flex;
  font-size: 3em;
  color: ${color.white};
`;

export const App = styled.div`
  *, & {
    box-sizing: border-box;
  }
`;

Компонент <ScrollView /> получает в виде свойств ширину и высоту, а также свойство горизонтали для появляющейся внизу полосы прокрутки.

Вспомогательные компоненты

Они облегчают нам жизнь и позволяют активно заниматься многократным использованием. Здесь мы храним все часто используемые паттерны. Вот некоторые из полезных для меня вспомогательных компонентов:

import styled, { css } from "styled-components";

import {
  borderProps,
  marginProps,
  backgroundColorProps,
  paddingProps,
  alignmentProps,
  positioningProps,
  sizeProps,
  spacingProps,
  theme
} from "ui";

const { screenSizes } = theme;

export const overlay = `
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0,0,0,0.5);  
`;

// You can use this like ${media.phone`width: 100%`}

export const media = Object.keys(screenSizes).reduce((accumulator, label) => {
  const acc = accumulator;
  acc[label] = (...args) => css`
    @media (max-width: ${screenSizes[label]}em) {
      ${css(...args)}
    }
  `;
  return acc;
}, {});

// Spacing

export const Padder = styled.section`
  padding: ${props => props.amount ? props.amount : "2em"};
`;

export const Spacer = styled.div`
  ${spacingProps}
`;

// Alignment

export const Center = styled.div`
  ${borderProps}
  ${marginProps}
  ${backgroundColorProps}
  ${paddingProps}
  ${alignmentProps}
  ${positioningProps}
  ${sizeProps}
  text-align: center;
  margin: 0 auto;
`;

// Positioning

export const Relative = styled.div`
  ${props => borderProps(props)};
  position: relative;
`;

export const Absolute = styled.div`
  ${props => marginProps(props)};
  ${props => alignmentProps(props)};
  ${props => borderProps(props)};
  position: absolute;
  ${props => props.right && `right: ${props.padded ? "1em" : "0"}; `}
  ${props => props.left && `left: ${props.padded ? "1em" : "0"}`};
  ${props => props.top && `top: ${props.padded ? "1em" : "0"}`};
  ${props => props.bottom && `bottom: ${props.padded ? "1em" : "0"}`};
`;

// Patterns
export const Collapsable = styled.section`
  opacity: 1;
  display: flex;
  flex-direction: column;
  ${props => props.animate && `
    transition: 
      transform 300ms linear,
      opacity 300ms ease-in,
      width 200ms ease-in,
      max-height 200ms ease-in 200ms;
    max-height: 9999px;
    transform: scale(1);
    transform-origin: 100% 100%;

    ${props.collapsed && `
      transform: scale(0);
      transition: 
        transform 300ms ease-out,
        opacity 300ms ease-out,
        width 300ms ease-out 600ms;
    `}
  `}

  ${props => props.collapsed && `
    opacity: 0;
    max-height: 0;
  `}
`;

export const Ellipsis = styled.div`
  max-width: ${props => props.maxWidth ? props.maxWidth : "100%"};
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
`;

export const Circle = styled.span`
  ${backgroundColorProps}
  display: inline-block;
  border-radius: 50%;
  padding: ${props => props.padding || '10px'};
`;

export const Hidden = styled.div`
  display: none;
`;

Тема

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

export const theme = {
  color: {
    primary: "#47C51D",
    secondary: '#53C1DE',
    white: "#FFF",
    black: "#222",
    border: "rgba(0,0,0,0.1)",
    base: "rgba(0,0,0,0.4)",
    alert: '#FF4258',
    success: 'mediumseagreen',
    info: '#4C98E6',
    link: '#41bbe1'
  },
  icon: {
    color: "gray",
    size: "15px"
  },
  font: {
    family: `
    -apple-system,
    BlinkMacSystemFont,
    'Segoe UI',
    Roboto,
    Helvetica,
    Arial,
    sans-serif,
    'Apple Color Emoji',
    'Segoe UI Emoji',
    'Segoe UI Symbol'`,
    base: '13px',
    small: '11px',
    xsmall: '9px',
    large: '20px',
    xlarge: '30px',
    xxlarge: '50px',
  },
  headings: {
    family: 'Helvetica Neue',
  },
  gutter: '2em',
  transition: '300ms ease-in-out'
};

export default theme;

Преимущества

  • Вся мощь JS у нас в руках, полное взаимодействие с интерфейсом компонента.
  • Не нужно с помощью className связывать компоненты и стили (это делается без вашего участия).
  • Огромное удобство разработки, не приходится забивать себе голову именами классов и их привязкой к компонентам.

Недостатки

  • Ещё нужно тестировать на реальных проектах.
  • Создано для React.
  • Проект очень молодой.
  • Тестирование надо проводить через aria-label или с помощью className.

Заключение

Какую бы технологию вы ни использовали — SASS, BEM, CSS-модули или styled-components, — не существует заменителя для хорошо продуманной архитектуры стилей, позволяющей разработчикам интуитивно развивать кодовую базу, без долгих и мучительных обдумываний, без ломания или внедрения новых подвижных частей системы.

Такой подход необходим для корректного масштабирования, и его можно достичь даже при условии использования чистого CSS и BEM. Всё дело лишь в объёме работы и LOC, необходимых для каждой реализации. В целом styled-components можно назвать подходящим решением для большинства React-проектов. Его ещё нужно активно тестировать, но проект выглядит многообещающе.

Автор: Mail.Ru Group

Источник

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


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