CODEGURU

Vue.js - Изолированные стили или CSS модули?

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

Чаще всего для борьбы с глобальной природой CSS используется методология БЭМ (Блок Элемент Модификатор). Но это решает только небольшую часть проблемы.

К счастью для нас коммьюнити уже разработало решение, которое поможет нам справиться с проблемами CSS. Вы, возможно, уже слышали о CSS Модулях, Стилизованных компонентах, Glamorous или JSS — это наиболее популярные инструменты, которые вы можете использовать в своих проектах уже сегодня. Если вам интересна эта тема, почитайте этот пост. В нём Индрек Ласн подробно описывает идею CSS-in-JS.

Каждое Vue.js приложение, созданное с помощью vue-cli, уже имеет на вооружении два мощных инструмента: Изолированный CSS (Scoped CSS) и CSS Модули. У каждого из них есть свои преимущества и недостатки. Давайте рассмотрим подробнее и определимся с тем, какое решение подойдёт вам.

Изолированные стили

Для подключения изолированных стилей необходимо добавить атрибут scoped в тэг style:

<template>
    <button class=”button” />
</template>

<style scoped>
.button {
    color: red;
}
</style>

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

<style>
    .button[data-v-f61kqi1] {
        color: red;
    }
</style>

<button class=”button” data-v-f61kqi1></button>

Как видите это довольно просто. Всего одно слово — и у вас есть изолированные стили для компонента.

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

<template>
    <BasePanel class=”pricing-panel”>
        content
    </BasePanel>
</template>

<style scoped>
    .pricing-panel {
        width: 300px;
        margin-bottom: 30px;
    }
</style>

Это превратится в:

<style>
    .base-panel[data-v-d17eko1] {
        ...
    }

    .pricing-panel[data-v-b52c41] {
        width: 300px;
        margin-bottom: 30px;
    }
</style>

<div class=”base-panel pricing-panel” data-v-d17eko1 data-v-b52c41>
    content
</div>

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

Однако, у этого метода есть один серьёзный недостаток — если у корневого элемента дочернего компонента есть такой класс, как у родителя, то стили родителя применятся к дочернему компоненту. Посмотрите этот пример, чтобы увидеть это на практике.

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

<style scoped>
    .pricing-panel >>> .title {
        font-size: 24px;
    }
</style>

Это превратится в:

.pricing-panel[data-v-b52c41] .title {
    font-size: 24px;
}

Легко и просто, да? Вот только при этом мы полностью потеряли инкапсуляцию. Любой элемент с классом .title внутри этого родительского компонента унаследует эти стили.

CSS Модули

CSS Модули стали очень популярны благодаря коммьюнити React, которое активно использует и развивает этот инструмент. Vue.js, в свою очередь, выводит его на новый уровень, объединяя мощь с простотой использования и поддержкой «из коробки» при использовании vue-cli.

Давайте посмотрим, как использовать CSS модули:

<style module>
    .button {
        color: red
    }
</style>

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

.ComponentName__button__2Kxy {
    color: red;
}

Чем же модули в итоге отличаются от изолированных стилей? Тем, что при использовании модулей классы, объявленные в CSS, доступны через объект $style. Поэтому шаблон будет выглядеть вот так:

<template>
    <button :class="$style.button" />
</template>

<style module>
    .button {
        color: red
 }
</style>

Это сгенерирует следующий HTML и стили:

<style>
.ComponentName__button__2Kxy {
    color: red;
}
</style>

<button class=”ComponentName__button__2Kxy”></button> 

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

Теперь давайте посмотрим как стилизовать компонент в зависимости от контекста:

<template>
    <BasePanel :class="$style['pricing-panel']">
        content
    </BasePanel>
</template>

<style module>
    .pricing-panel {
        width: 300px;
        margin-bottom: 30px;
 }
</style>

Это превратится в:

<style>
    .BasePanel__d17eko1 {
        /* some styles */
    }

    .ComponentName__pricing-panel__a81Kj {
        width: 300px;
        margin-bottom: 30px;
    }
</style>

<div class="BasePanel__d17eko1 ComponentName__pricing-panel__a81Kj">
    content
</div>

Всё работает как должно, без сюрпризов. Более того, так как все классы доступны через объект $style, мы можем передавать их как угодно глубоко с помощью props:

<template>
    <BasePanel
        title="Lorem ipsum"
        :titleClass="$style.title"
 >
        Content
    </BasePanel>
</template>

Представьте, что вы делает график и храните переменные с цветами в CSS. Вы можете экспортировать их для использования в компоненте:

<template>
    <div>{{ $style.primaryColor }}</div> <!-- #B4DC47 -->
</template>

<style module lang="scss">
    $primary-color: #B4DC47;
    :export {
        primaryColor: $primary-color
}
</style>

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

Заключение

Оба решения очень просты в использовании и отлично выполняют свою задачу. Какое же выбрать?

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

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