Практическое руководство по использованию CSS Modules в React приложениях

Привет Хабр! Предлагаю вашему вниманию свободный перевод статьи «Practical Guide to React and CSS Modules» от Tatu Tamminen.

В прошлом веб-разработчики тратили много времени и сил на создание повторно используемых компонентов. Оcобую проблему представлял собой CSS и природа его каскадов. Например, если разработчик создаёт компонент для отображения древовидной структуры, то как он может гарантировать, что CSS класс (например, .leaf), используемый в этом компоненте, не приведёт к побочным эффектам в других частях приложения? Были созданы различные методологии и соглашения, чтобы справиться с проблемами селекторов. БЭМ и SMACSS — широко используемые методологии, которые хорошо выполняют свои задачи, но в то же время далеко не идеальны. В этой статье рассказывается о недостатках таких методологий, основанных на соглашении об именах, о том, что представляют собой CSS Modules, и о том, как эти модули можно использовать в React приложении.

Проблема с каскадами


Давайте создадим повторно используемый компонент select в качестве примера, иллюстрирующего проблемы глобальных стилей. Стилизация элемента <select> напрямую — это плохое решение, поскольку в других местах сайта нам может потребоваться или изначальный нестилизованный элемент, или совсем другая его стилизация. Вместо этого можно использовать синтаксис БЭМ для определения классов:

.select {}
.select__item {}
.select__item__icon {}
.select--loading {}

Если бы новый класс item был создан без префикса select__, то у всей команды могли бы возникнуть проблемы, если бы кто-нибудь захотел использовать такое же имя item. При этом неважно, разработчик ли пишет CSS или же его генерирует какая-то утилита. Использование БЭМ помогает решить эту проблему, вводя контекст для элемента select.

Синтаксис БЭМ является шагом вперёд по направлению к компонентам, так как «Б» в БЭМ расшифровывается как «Блок», а блоки можно интерпретировать как легковесные компоненты. Select — это компонент, у которого есть различные состояния (select--loading) и потомки (select__item).

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

CSS Modules спешат на помощь


«CSS модуль» определяется следующим образом:
CSS модуль — это CSS файл, в котором все имена классов и анимаций имеют локальную область видимости по умолчанию.

Ключевая идея здесь — локальная область видимости.

Чтобы проиллюстрировать эту концепцию, давайте создадим JavaScript и CSS файлы компонента.

/* select.css */
.select {}
.loading {}
.item {}
.icon {}

/* select.js */
import styles from "./select.css";

console.log(styles.select, styles.loading);

Это простой пример, который, однако, содержит много всего, происходящего за сценой.

Теперь CSS файл содержит намного меньше шума, чем в БЭМ версии, потому что в нём больше нет префиксов и специальных символов, задающих контекст. Почему же стало возможным удалить префикс .select--, не создавая при этом проблем?

Оператор import в JavaScript файле загружает CSS файл и конвертирует его в объект. В следующем разделе мы рассмотрим, как настроить рабочее окружение, позволяющее импортировать CSS файлы.

Каждое имя класса из CSS файла является свойством объекта. В примере выше это styles.select, styles.icon и т. д.

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

Вы можете подумать: «Какой ужас!» В чём смысл изменения осмысленного имени класса на непонятный хеш? Основная причина в том, что такой идентификатор гарантированно является уникальным в глобальном контексте. Позднее в этом руководстве мы изменим механизм создания идентификаторов, так что они будут иметь более осмысленный вид, но при этом останутся уникальными.

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

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

К недостаткам можно отнести следующее:

  • сложнее читать и понимать DOM,
  • требуется некоторая начальная настройка окружения, чтобы заставить всё это работать.

CSS Modules требуют сборки проекта, но это не проблема, так как различные сборщики поддерживают CSS Modules для JavaScript как на клиенте, так и на сервере. CSS Modules также можно использовать совместно с большинством UI библиотек.

Для простоты в этой статье мы остановимся на сборщике модулей Webpack и библиотеке React.

React, Webpack и CSS Modules


Для быстрого создания приложения можно использовать Create React App.

Следуя инструкциям в документации, мы создадим и запустим новый проект практически мгновенно.

npm install -g create-react-app

create-react-app css-modules  
cd css-modules/  
npm start  

Вуаля, и у нас работающее React приложение:



Начальная страница сообщает нам, что нужно редактировать файл App.js.

import React, { Component } from 'react';  
import logo from './logo.svg';  
import './App.css';

class App extends Component {  
  render() {
    return (
      <div className="App">
        <div className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h2>Welcome to React</h2>
        </div>
        <p className="App-intro">
          To get started, edit {gfm-js-extract-pre-1} and save to reload.
        </p>
      </div>
    );
  }
}

export default App;

Используются ли CSS Modules в Create React App? Это можно узнать, взглянув на файл App.js. CSS файл импортируется, но не присваивается никакой переменной, при этом во всех атрибутах className используются строки вместо динамических значений.

С этой точки зрения Create React App не поддерживает CSS Modules, так что нужно изменить конфигурацию, чтобы включить эту поддержку.

Настройка Create React App для поддержки CSS Modules


Чтобы получить доступ к скрытому конфигу сборки, нужно выполнить команду eject. Внимание: если вы сделали это, то вернуться обратно вы уже не сможете.

npm run eject  

Теперь можно открыть папку с конфигами для webpack:



Create React App использует webpack для сборки, поэтому webpack.config.dev.js — тот самый файл, который нужно изменить (а также webpack.config.prod.js для настроек продакшна — прим. переводчика).

Найдём раздел, задающий, что делать с CSS файлами (в оригинальной статье используется старый синтаксис конфигов webpack, здесь же я использовал новый — прим. переводчика):

{
    test: /\.css$/,
    use: [
        require.resolve('style-loader'),
        {
            loader: require.resolve('css-loader'),
            options: {
                importLoaders: 1,
            },
        },
        {
            loader: require.resolve('postcss-loader'),
            options: {
                // Necessary for external CSS imports to work
                // https://github.com/facebookincubator/create-react-app/issues/2677
                ident: 'postcss',
                plugins: () => [
                    require('postcss-flexbugs-fixes'),
                    autoprefixer({
                        browsers: [
                            '>1%',
                            'last 4 versions',
                            'Firefox ESR',
                            'not ie < 9', // React doesn't support IE8 anyway
                        ],
                        flexbox: 'no-2009',
                    }),
                ],
            },
        },
    ],
},

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

{
    test: /\.css$/, 
    use: [
        require.resolve('style-loader'),
        {
            loader: require.resolve('css-loader'),
            options: {
                importLoaders: 1,
                modules: true,
                localIdentName: "[name]__[local]___[hash:base64:5]"
            },
        },
        {
            loader: require.resolve('postcss-loader'),
            options: {
                // Necessary for external CSS imports to work
                // https://github.com/facebookincubator/create-react-app/issues/2677
                ident: 'postcss',
                plugins: () => [
                    require('postcss-flexbugs-fixes'),
                    autoprefixer({
                        browsers: [
                            '>1%',
                            'last 4 versions',
                            'Firefox ESR',
                            'not ie < 9', // React doesn't support IE8 anyway
                        ],
                        flexbox: 'no-2009',
                    }),
                    require('postcss-modules-values'),
                ],
            },
        },
    ],
},

Что делают эти загрузчики loaders? В файле webpack.config есть закомментированная секция, описывающая загрузчики стилей и CSS:
postcss-loader применяет autoprefixer к CSS.
style-loader преобразовывает CSS в JS модули, которые инжектят теги <style>.
css-loader разрешает пути в CSS и добавляет ресурсы как необходимые зависимости.

Опция modules: true в настройках css-loader включает поддержку CSS Modules. Параметр localIdentName изменяет шаблон имени класса таким образом, что оно включает в себя имя компонента React, имя класса и уникальный хеш-идентификатор. Это позволит производить отладку намного легче, потому что легко можно идентифицировать все компоненты.

Использование CSS Modules в React


Можно проверить, что конфигурация работает, добавив вызов console.log после оператора import.

Заменяя import './App.css'; на

import styles from './App.css';

console.log(styles); 

мы получаем следующий вывод в консоль браузера:



Сейчас классы уникальны, но они не используются в React компонентах. Нужно сделать ещё два шага, чтобы применить стили к React компонентам. Во-первых, нужно изменить имена классов согласно camelCase нотации. Во-вторых, нужно изменить атрибуты className так, чтобы они использовали импортированные классы.

Использовать camelCase нотацию необязательно, но при доступе к классам из JavaScript легче писать styles.componentName, чем styles[«component-name»].

Исходный файл стилей выглядит так:

.App {
  text-align: center;
}

.App-logo {
  animation: App-logo-spin infinite 20s linear;
  height: 80px;
}

.App-header {
  background-color: #222;
  height: 150px;
  padding: 20px;
  color: white;
}

.App-intro {
  font-size: large;
}

@keyframes App-logo-spin {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

Больше нет необходимости в префиксах App, поэтому сейчас хороший момент, чтобы удалить их тоже. Модифицированный CSS будет выглядеть так:

.app {
  text-align: center;
}

.logo {
  animation: logoSpin infinite 20s linear;
  height: 80px;
}

.header {
  background-color: #222;
  height: 150px;
  padding: 20px;
  color: white;
}

.intro {
  font-size: large;
}

@keyframes logoSpin {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

Следующий шаг — изменить использование классов в компоненте. Результат будет следующим:

import React, { Component } from 'react';  
import logo from './logo.svg';  
import styles from './App.css';

class App extends Component {  
  render() {
    return (
      <div className={ styles.app }>
        <div className={ styles.header }>
          <img src={logo} className={ styles.logo } alt="logo" />
          <h2>Welcome to React</h2>
        </div>
        <p className={ styles.intro }>
          To get started, edit {gfm-js-extract-pre-2} and save to reload.
        </p>
      </div>
    );
  }
}

export default App; 

Теперь наш компонент использует CSS Modules.

Как нарушить границы CSS модуля, когда это необходимо


Подход, описанный в предыдущем разделе, является основным для React проектов, но разработчики обычно быстро обнаруживают, что им нужен способ выделять и использовать общие стили. В этом контексте «общие» значит лишь то, что компонент должен наследовать что-то от базовых стилей.

Эту общую информацию могут представлять собой переменные (цвета, размеры шрифта, и т. д.), хелперы (миксины SASS) или utility-классы.

CSS Modules дают возможность композиции с помощью ключевого слова from. Композиция возможна и между классами из разных файлов.

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

/* base_button.css */
.baseButton {
  border: 2px solid darkgray;
  background-color: gray;
}

/* submit_button.css */
.submitButton {
  composes: baseButton from "./base_button.css";
  background-color: blue;
}

Если есть необходимость в использовании переменных, то можно использовать или препроцессор, например, SASS или Less, или настроить поддержку переменных в webpack.

Пример из документации webpack по переменным в CSS:

/* variables.css */
@value blue: #0c77f8;
@value red: #ff0000;
@value green: #aaf200;

/* demo.css */

/* import your colors... */
@value colors: "./variables.css";
@value blue, red, green from colors;

.button {
  color: blue;
  display: inline-block;
}

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

Изменённый пример:

/* variables.css */
@value customBlue: #0c77f8;
@value customRed: #ff0000;
@value customGreen: #aaf200;

/* demo.css */

/* import your colors... */
@value colors: "./variables.css";
@value customBlue, customRed, customGreen from colors;

.button {
  color: customBlue;
  display: inline-block;
}

Заключение


В этом руководстве мы начали с рассмотрения проблем глобального CSS, затем увидели, как CSS Modules улучшают ситуацию, вводя область видимости CSS и заставляя нас думать в терминах компонентов. Также мы рассмотрели, как легко можно начать экспериментировать с CSS Modules, используя React Starter Kit.

CSS Modules используются совместно со сборкой всего фронтэнда, то есть, с поддержкой в браузере нет никаких проблем. Браузеры получают обычный CSS от сервера, так что нет никакой возможности «сломать» сайт, используя CSS Modules. Наоборот, при этому мы только повышаем его надёжность, уменьшая количество потенциальных багов. Webpack с загрузчиками, сконфигурированными для поддержки CSS Modules, не создаёт никаких проблем, так что без сомнений можно рекомендовать этот инструмент к использованию.

Если вы использовали CSS Modules в своих проектах, я (то есть, автор оригинальной статьи — прим. переводчика) хотел бы услышать о вашем опыте!



→ Публикация — свободный перевод статьи «Practical Guide to React and CSS Modules». Автор статьи Tatu Tamminen
→ Исходный код можно найти в react-cssmodules-demo
→ Также заслуживает внимания CSS Modules Webpack Demo
Метки:
Поделиться публикацией
Комментарии 16
  • +1
    Я совсем недавно задался вопросов «Почему до сих пор популярна методология БЭМ, когда есть CSS Modules?» в slack #web-standards и написал свои мысли, но меня сообщество не поняло.
    Ваш пост многое разъяснил.
    • 0

      Очень рекомендую интересующихся посмотреть на этот плагин для бейбеля react-css-modules


      Использовать или нет — ваш выбор, но удобство использования css modules увеличивает сильно (недостатком является неявное использование переменных, что в общем случае зло).

      • 0
        Очень интересно, спасибо буду пробывать
        • 0
          Тоже смотрел на этот плагин. Поскольку опыта в фронтэнде мало, пока решил ограничиться обычными CSS Modules. Наверное, основная фича — возможность смешивать локальный и глобальный CSS. Мне на проекте такое пока не требуется, но так-то всё может быть.
        • 0
          А что делать, если нужно несколько классов использовать?
          • 0
            <div classNames={[styles.base, styles.clickable, styles.withIcon].join(' ')} />

            нашёл такой вариант

            • 0
              В общем, логично. Это же просто сгенерированные уникальные имена классов, чего же их не собрать в одну строку. С другой стороны мне нравится идея думать, что класс определяет состояние компонента. Ну, например, кнопка может быть .enabled или .disabled. Если у состояний есть что-то общее, то мы выделяем общий класс и через compose используем его в «классах-состояниях».
            • 0

              как то костыльно выгядит

              • 0
                О, спасибо! Позволит избежать сборки собственного велосипеда.
            • 0
              Не совсем ясно, можно ли как-то попробовать css модули без реакта с вебпаком, а например с помощью gulp-а? Для обычных нормальных простых сайтов, которые рендерятся на сервере…
              • 0
                Думаю, можно. Сам я не пробовал, но по сути CSS Modules — это всего лишь один из множества плагинов для PostCSS. Значит, надо смотреть в сторону настройки PostCSS для gulp. А про PostCSS есть отличный доклад Андрея Ситника.
                • 0
                  Кажется CSS Modules относится не к PostCSS, а скорее к css-loader
                  • 0
                    Насколько, я понимаю, CSS Modules — это концепция, которую можно организовать разными способами. css-loader вебпака поддерживает их. И через PostCSS можно реализовать. Вот есть плагин: https://github.com/css-modules/postcss-modules. Да и в самом докладе Андрей рассказывает, как включить CSS Modules при использовании gulp.

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