Как писать CSS так, чтобы PM, User и Team Lead были довольны?

​React
​React

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

Вариантов на сегодняший день очень много. Чистый CSS, препроцессоры, постпроцессоры, CSS Modules, BEM, CSS переменные, CSS-in-JS подход. Как правильно сделать выбор и что использовать, чтобы все были довольны?

В этой статье я постараюсь разобрать самые популярные наборы библиотек / технологий для написани CSS и рассмотреть их со стороны разработчика, заказчика и пользователя. У каждой из этих сторон будут свои критерии (пожелания, требования) для написания стилей.

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

Будет очень много информации, ссылок и субъективных суждений :) Все, как мы любим) Если вам что-то не понятно или вы не согласны, пожалуйста, пишите коментарии, будем обсуждать.

Итак, поехали!

Стеки, встречающиеся в 80-90% проектов:

  • Чистый CSS
  • Sass + BEM
  • Sass + CSS Modules
  • Sass + CSS Modules + PostCSS
  • Styled Components

P.S. Sass будет представителем препроцессоров, Styled Components будет представлять CSS-in-JS подход, т.к. времени рассмотреть все препроцессоры и библиотеки CSS-in-JS подхода нам не хватит.

А вот и критерии:

Критерии со стороны разработчика:

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

Критерии со стороны заказчика. Заказчик / менеджер не выдвигает определенных критериев для написания стилей в проекте, но у него есть стандартные требования к любой задаче / проекту: ему важно, чтобы это было быстро и качественно (если такое вообще возможно):

В конце обсуждения каждой группы критериев я сделаю сводную таблицу с оценками для каждого из набора технологий. Например, я возьму проблему коллизии имен и буду смотреть, как она решается в каждом из наборов технологий. Оценки будут от 0 до 5. Где 0 — проблема никак не решается, 5 — решается очень хорошо.

Если вы понимаете, что для вас какой-то критерий не важен, например, переиспользование стилей в React Native, то можете посчитать свои баллы для каждого стека и вывести своего победителя.

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

Варианты подключения стилей

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

Сегодня есть два популярных подхода:

  • Классический подход (стили в CSS bundle, сюда же можно отнести inline-стили или стили в теге style)
  • CSS‑in‑JS подход (стили в JS bundle)

Классический подход нам всем знаком. Когда после сборки мы получаем CSS файл со стилями. Подключаем его в теге style в head и все счастливы.

Давайте в кратце пройдемся по этапам создания этого самого файла со стилями:

  1. Sass‑loader
  2. PostCSS
  3. CSS‑loader
  4. Style‑loader

Пример webpack конфига:

{ test: /\.(scss|sass)$/, use: [ 'style-loader', 'css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]', 'postcss-loader', 'sass-loader', ], },

Sass‑loader компилирует Sass в CSS. У каждого препроцессора есть свой API для создания определенных удобств при написании стилей. Для того чтобы Sass компилировался в CSS, у каждого препроцессора есть свой webpack-loader.

PostCSS очень крутая технология, которая принимает на вход CSS, делает из него AST — абстрактное синтаксическое дерево, добавляет какие-то вещи (например вендоные префиксы) и в итоге возвращает снова CSS с уже новым функционалом.

CSS‑loader — загрузчик CSS, интерпретирует @import и url () как import / require () и резолвит их.

Style‑loader добавляет CSS в DOM.

Это пример, который вероятнее всего, вы можете увидеть у себя в проекте. Он может меняться. Например, может не быть Sass‑loader-а или PostCSS.

CSS‑in‑JS подход отличается тем, что все стили после сборки попадают в JS‑bundle и вставляются в DOM на клиенте в рантайме.

То есть файла с CSS больше нет. Все стили лежат внутри JS файла в template string. Вебпак настраивать для этого также не нужно (по крайней мере со Styled Components).

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

Критерии разработчика

Отсутствие проблемы коллизий имен

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

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

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

css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]

На выходе из `some-classname` можно получить `some-classname__d3a9d` или что-то вроде этого.

Styled Components решает проблему почти также, как и CSS Modules, только делает это внутри самой библиотеки, а не на этапе сборки проекта.

Итоги: CSS Modules и Styled Components решают проблему коллизии имен лучше всего. В 90% проектов сегодня используется этот прием. Это очень удобно и позволяет избежать проблемы коллизии имен на 99.99%. На самом деле, подойдет любое решение, которое делает что-то подобное. У Vue.js используется атрибут scoped для тега style в .vue файле, но, по сути, делается тоже самое.

Удобное использование констант

Существует несколько способов использования констант при написании стилей. Разберем самые популярные из них:

CSS-переменные самый лучший способ, если вам не нужно поддерживать IE11:

CSS Variables
CSS Variables

Они дают возможность создавать переменные в области видимости DOM-элементов (shadowing). Также можно создавать глабальные переменные под :root или body / html. Также очень удобно, что их можно переопределять. Самое важное — это нативная технология, для которой ничего не нужно устанавливать, просто создаете переменную --color-white: white, потом используете ее var(--color-white). Все хорошо работает с calc и другими CSS-функциями.

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

P.S: Отличия Sass-переменных от CSS-переменных:

  • Sass-переменные компилируются в css-код с помощью sass-loader. CSS-переменные - нативная технология, которую понимает ваш браузер (если это не IE);
  • Если вы используете sass-переменные и вдруг меняете значение переменной, то значения, которые были использованы ранее, останутся прежними. А при использовании CSS-переменных изменятся все места, которые использвали эту переменную.

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

CSS‑in‑JS переменные — это обычные JS-константы, которые нужно импортировать везде, где хочется их использовать. Из плюсов: можно использовать одни и те же константы в CSS и JS коде, удобно для анимаций (например anumation duration). То есть, если раньше мы создавали одну константу для Sass $animation-duration: 0.2s и const ANIMATION_DURATION = '0.2' и для того, чтобы ее изменить, приходилось делать правки в двух местах, то теперь такая надобность отпала, так как переменная одна.

Итоги: Если вам не нужно поддерживать IE11, то это определенно CSS-переменные, а если все-таки нужно поддерживать, все остальные решения, по моему субъективному мнению +/- одинаковые. Ну и конечно, если вы пишите стили на Styled Components, например, то вам не нужно использовать препроцессорные или постпроцессорные переменные и наоборот.

Переиспользование кода (mixins)

Mixins позволяют переиспользовать ваш CSS-код и избавиться от boilerplate кода. Проще говоря, вы можете создать функцию, которая будет возвращать вам какой-то кусок стилей.

Существуют несколько популярных способов создания mixins:

Mixins в препроцессорах. В целом, все похоже на переменные в препроцессорах, за исключением того, что в mixin можно передать переменную и в зависимости от нее вернуть определенный кусок css. Это очень удобно. Что в @apply rule, например, сделать нельзя (даже если бы они и работали).

Mixins в PostCSS тоже самое, что и в препроцессорах, за исключением того, что компиляция происходит на другом этапе (как с css переменными) и немного отличается синтаксис. В плане возможностей и удобства + / - тоже самое.

JS mixins - мой вафорит. И вот почему:

  • Не нужно учить новый синтаксис написания mixins (JS-функции знает каждый разработчик);
  • Есть доступ к любой переменной JS в браузере (DOM, BOM);
  • Можно использовать в проекте на React и React Native;
  • Не нужно создавать лоадеры для webpack, т.к. это JS-файлы.

Создать mixin через @apply без использования дополнительных библиотек сегодня навряд ли получится. Но даже из-за отсутствия возможности передавать агрументы в mixin, не думаю, что это могло бы стать популярным решением.

CSS @apply rule
CSS @apply rule

Если вы хотите проверить как это работает, то активируйте этот раздел в Google Chrome.

Разделение логики работы компонента и логики определения его стилей

Кнопка, написанная на Styled Components:

import * as S from './style'; export default S.Button;

Кнопка, написанная на Sass + PostCSS + classnames:

import React from 'react'; import cn from 'classnames'; import styles from './Button.sass'; const Button = React.memo(({children, theme, size, onClick, fullWidth, rounded = false, ...rest}) => { const classNames = cn( styles.button, {'button_fill-width': fullWidth === true}, {'button_rounded': rounded === true}, {[styles[[`button_theme_${theme}`]]]: theme !== undefined}, {[styles[[`button_theme_${size}`]]]: size !== undefined}, ); return ( <button onClick={onClick} className={classNames} {...rest}>{children}</button> ); }); export default Button;

Открывая компонент, вам хочется понять, как он работает, а портянка с const classnames = ... мешает сделать это быстро и создает дополнительный визуальный мусор. В случае с Styled Compoents мы сразу понимаем, что кнопка — stateless-компонент и все props, что она принимает, либо раскладываются в атрибуты DOM-элемента (если такие есть), либо ловятся в функциях и участвуют в образовании стилей Styled Component-а.

Удобная постобработка (RTL, autoprefixer, postcss-preset-env, ...)

Удобная постобработка — это автоматическая постобработка. Мы же все ленивые, и делать что-то лишнее нам явно не хочется.

PostCSS прекрасно с этим справляется. Яркие тому примеры — это autoprefixer, postcss-preset-env, RLT и многие другие.

Разработчики Styled Components в последней версии, которая пока в стадии pre-release, добавили поддержку RTL. Если хочется сделать что-то еще, придется ждать, пока это добавят в бибилотеку или писать свои mixin-ы.

PostCSS удобен еще тем, что вы можете написать свой plugin и использовать его.

Переиспользование в React Native

Здесь без вариантов побеждает CSS-in-JS подход. В качестве примера можно рассмотреть Styled Components. Т.к. в мобильной разработке нет DOM и CSS, то за счет того что Styled Components написаны на JavaScript, они получают дополнительную гибкость. Для того чтобы использовать стили одновременно для web-проекта на React и проекта для мобильных платформ на React Native, вам нужно импортировать ваши функции из папки styled-components/native. И все.

Итак, мы обсудили критерии разработчика. Время подведения итогов и выставления оценок :)

Как писать CSS так, чтобы PM, User и Team Lead были довольны?

Критерии со стороны пользователя

Быстрая загрузка страницы

Как показывает практика, лучше всего работает (пока что) классический подход. На эту тему есть много статей. Я проводил даже собственные эксперименты (v 4.4.0). И примерно на 20% показатели загрузки страницы были ниже, если использовать styled components. Показатели, на которые я смотрел:

  • First meaningful paint;
  • First Contentful Paint;
  • Time to Interactive.

Давайте попробуем разобраться, почему Styled Components все-таки медленнее, чем отдельный CSS-бандл. Условимся, что скорость интернета, дизайн и все остальное в проектах абсолютно идентично. Отличается только написание стилей.

Рассмотрим классический подход:

1. Начинается загрузка HTML;

2. Браузер видит в head стили и начинает скачивать их;

3. Строится DOM-дерево;

4, Строится CSS OM. (CSS Object Model);

5. После того как DOM и CSS OM готовы, происходит этап attachment дерева стилей и дерева документа;

6. Строится Rendering Tree (дерево отображения);

7. Layout -> Paing -> Display.

Расмотрим CSS-in-JS подход:

1. Начинается загрузка HTML;

2. Тега style нет, т.к. все стили лежат в JS бандле;

3. Строится DOM-дерево;

4. Браузер встречает тег script (как правило в конце body) и начинает его скачивать (если скрипт подключен через defer / async). Здесь важно понимать, что Styled Components добавляет к весу бандла порядка 16.2кб min+gzip. Это, конечно, совсем незаметно для пользователя, но все же;

5. DOM-дерево построено, но CSS OM, скорее всего, еще не сформирован, так как скрипт не так давно начал скачиваться;

6. Скрипт скачался, выполнился, сгенерил CSS и вставил их в head;

7. После того как DOM и CSS OM готовы, происходит этап attachment дерева стилей и дерева документа;

8. Строится Rendering Engine (дерево отображения);

9. Layout -> Paing -> Display.

Есть еще много разных интересных событий при загрузке страницы, но я хотел лишь выделить то, что при классическом подходе CSS OM и DOM формируются практически параллельно, и CSS начинает скачиваться почти сразу после HTML. А в CSS-in-JS подходе это происходит только после того, как JS-бандл скачается и начнет выполняться. Только после этого из кода в head вставится тег style, и начнется процесс построения CSS OM. Это создает определенную задержку и делает Styled Components немного медленнее, чем чистый CSS в отдельном файле.

Работает во всех популярных браузерах

Часть проблем кроссбраузерного CSS можно решить, используя вендорные префиксы. Тут оба подхода решают проблемы достаточно хорошо. У PostCSS есть плагин autoprefixer, а Styled Components делает это из коробки.

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

Этот плагин позволяет писать код, используя современные фичи CSS и не думать о том, что это может не заработать в IE. Styled Components пока не может похвастаться чем-то подобным. Жирный лайк и 5 балов PostCSS.

Время оценок:

Как писать CSS так, чтобы PM, User и Team Lead были довольны?

Критерии со стороны менеджера

Быстрая разработка

Ускорить разработку могут:

  • миксины
  • вендорные префиксы
  • postcss-preset-env

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

Миксины можно писать в препроцессорах, постпроцессорах и в CSS-in-JS подходе. Вендорные префиксы можно удобно автоматизировать с PostCSS и в Styled Components, они также отлично работают. Но postcss-preset-env решает этот спор в пользу классического подхода и стека Sass + Css Modules + PostCSS.

Отсутствие багов

Намного удобнее тестировать JS-код, нежели CSS. Плюс ко всему, код на Styled Components можно типизировать используя TypeScript и тем самым добавить ему надежности. Это очень помогает при разработке отлавливать баги на раннем этапе. Также, если не достаточно строгой типизации в стилях, можно добавить unit-тесты на jest, например, и протестировать логику определения стилей.

Настроить stylelint одинаково удобно и в Styled Components, и в css / sass / less / scss файлах.

Посмотрим, что получилось:

Как писать CSS так, чтобы PM, User и Team Lead были довольны?

Подведение итогов:

Итак, для разработчика больше всего баллов набрал Styled Components, но пользователи с этим не совсем согласны. Из этого можно сделать вывод: если вы пишете внутренние проекты с очень ограниченным количеством пользователей, то вам определенно стоит попробовать Styled Components (надеюсь, вы еще помните, что Styled Components представитель CSS-in-JS подхода, и тут может быть другая библиотека). А если у вас проект с миллионной аудиторией, то Styled Components пока того не стоит, так как увеличивает время ключевых показателей загрузки страницы и, как следствие, портит конверсию. Но версия 5.0.0 обещает быть еще быстрее и легче.

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

Надеюсь, вам понравилась статья :)

Спасибо, что дочитали до конца. Ставьте лайки, пишите комментарии и до новых встреч!

6
Начать дискуссию