Еволюція CSS: від CSS, SASS, BEM і CSS–модулів до styled-components



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

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

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

microsoft's fist сайті, back in 1994. :) Looking pretty cool #microsoft pic.twitter.com/7wy1HQUObB  Ivan Todorov (@ivantodorov) April 17, 2013


Потім ми почали створювати динамічні сайти, але CSS залишався оплотом свавілля: кожен розробник мав власне уявлення, як робити CSS. Хтось боровся з специфічністю (specificity), приводила до візуальної регресії при появі нового коду. Ми покладалися на !important, тим самим бажаючи висікти на камені символ нашої волі до того, щоб елементи інтерфейсу виглядали певним чином. Але незабаром ми зрозуміли:


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


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
&--сутінки {
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.3 s;
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.сутінки && '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 />
<night Sky />

Як бачите, властивості несподівано стали модифікаторами, одержуваними кожним компонентом, і вони можуть бути оброблені, отримуючи на виході різні рядки 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 : "авто"};

&: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: 6px 0 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-проектів. Його ще потрібно активно тестувати, але проект виглядає багатообіцяюче.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.