Эта статья — перевод оригинальной статьи Lindsay Wardell "Styling Vue Single-File Components"
Также я веду телеграм канал “Frontend по-флотски”, где рассказываю про интересные вещи из мира разработки интерфейсов.
Вступление
Если у вас есть опыт написания однофайловых Vue компонентов, вы, вероятно, сталкивались с написанием CSS в своем компоненте. Они позволяют разработчикам группировать код более логическими способами, а не разбивать компоненты по используемому языку (HTML, CSS или JavaScript). Возможность группировать стили компонентов непосредственно рядом с HTML-кодом, к которому он применяется, является одним из основных преимуществ Vue, включая возможность применять CSS к компоненту, чтобы он не влиял на другие части пользовательского интерфейса.
Однако есть ряд функций взаимодействия Vue с CSS, с которыми вы, возможно, не знакомы, например, применение стилей непосредственно к элементам со слотами или новейшие функции, доступные в Vue 3.2. Давайте рассмотрим некоторые из этих других способов стилизации однофайловых Vue компонентов и их преимущества для ваших приложений.
Scoped стили
Начнем с наиболее частого использования CSS в Vue: стили с ограниченными областями видимости. Одна из трудностей при написании современных приложений заключается в том, что наши CSS файлы начинают расти все больше и больше, пока никто не знает, где используются определенные стили или на что может повлиять данное изменение. Это может привести к копированию определенных CSS селекторов и простому дублированию их для каждого компонента. Для этого есть и другие решения (например, БЭМ или служебные классы), но при работе с компонентной структурой, такой как Vue, имеет смысл сгруппировать классы CSS внутри компонента.
Стили с ограниченной областью видимости позволяют нам писать CSS, который применяется только к компоненту, с которым мы работаем. Вот пример из документации Vue:
<style scoped>
.example {
color: red;
}
</style>
<template>
<div class="example">hi</div>
</template>
Класс из примера будет применяться только в этом компоненте. Это достигается путем добавления уникального атрибута data ко всем элементам в компоненте, поэтому по-прежнему применяется обычный CSS каскад. Внешние стили по-прежнему могут влиять на дизайн этого компонента, но его стили с областью видимости не могут проникать в другие компоненты.
Глубокие стили
Это приводит к интересной проблеме. Если стили нашего компонента имеют ограниченную область видимости, как насчет дочерних компонентов? По умолчанию они не получат стили родительского компонента. Однако Vue предоставляет способ сделать это. Давайте посмотрим на пример ниже.
<!-- Card.vue -->
<template>
<div>
<header>
<Title>
<slot name="title">Card Title</slot>
</Title>
</header>
<section>
<slot>Lorum ipsum dolor sit amet</slot>
</section>
</div>
</template>
<style scoped>
header :deep(.card-title) {
font-weight: bold;
}
section {
padding 2rem;
}
</style>
<!-- Title.vue -->
<template>
<div class="card-title"><slot>Title</slot></div>
</template>
Используя псевдокласс :deep(), мы можем сообщить Vue, что этот конкретный класс (.card-title) не должен иметь области видимости. Поскольку специальный идентификатор по-прежнему применяется к корневому элементу (заголовку), стиль по-прежнему ограничен, но доступен для любого дочернего компонента ниже него.
Slotted стили
Проблема, с которой я сталкивался во многих ситуациях, заключается в том, что у меня есть компонент, в который вставляются слоты, но я не могу контролировать его стиль, как хочу. Vue предлагает решение и для этого с помощью slotted стилей. Давайте рассмотрим приведенный выше пример, но на этот раз мы добавим slotted стиль к нашему компоненту Title.vue.
<!-- Card.vue -->
<template>
<div>
<header>
<Title>
<slot name="title">Card Title</slot>
</Title>
</header>
<section>
<slot>Lorum ipsum dolor sit amet</slot>
</section>
</div>
</template>
<style scoped>
header :deep(.card-title) {
font-weight: bold;
}
section {
padding 2rem;
}
</style>
<!-- Title.vue -->
<template>
<div class="card-title">
<slot>Title</slot>
</div>
</template>
<style scoped>
:slotted(h1) {
font-size: 3rem;
}
</style>
Здесь мы добавили псевдокласс :slotted
, чтобы к любым тегам h1
со слотами применялся правильный стиль. Это может быть надуманный пример, но подумайте о необходимости иметь разные стили заголовка для каждого тега заголовка (или эквивалентного класса CSS). Компонент Title.vue может управлять всеми этими стилями, а не полагаться на то, что тот, кто будет использовать этот компонент передаст правильный класс или стиль.
Глобальные стили
Конечно, иногда вам нужно применить стили глобально, даже внутри компонента с ограниченной областью видимости. Vue предоставляет нам два разных способа справиться с этим: псевдоселектор :global
и несколько блоков стилей.
:global
В блоке стиля с ограниченной областью видимости, если вам нужно предоставить только один класс в качестве глобального значения, вы можете использовать псевдоселектор :global, чтобы отметить, что стиль не должен иметь области видимости. Из документации Vue:
<style scoped>
:global(.red) {
color: red;
}
</style>
Несколько блоков стилей
Также ничто не мешает вам иметь несколько блоков стилей в вашем компоненте. Просто создайте еще один тег <style> и поместите туда свои глобальные стили.
<style>
/* global styles */
</style>
<style scoped>
/* local styles */
</style>
CSS модули
Если вы работали с React, вероятно, вы более знакомы с CSS модулями, в которых вы импортируете CSS файл и получаете доступ к его классам как к JavaScript объекту. То же самое можно сделать в Vue, используя <style module> вместо <style scoped>. Вот пример из документации Vue:
<template>
<p :class="$style.red">
This should be red
</p>
</template>
<style module>
.red {
color: red;
}
</style>
С этим может быть особенно приятно работать, так как вам не придётся использовать строки в своих классах (которые подвержены ошибкам и опечаткам). Vue также позволяет вам переименовывать объект, так что вам не нужно обращаться к ним с помощью $style в вашем шаблоне, если вы этого не хотите.
Динамические CSS значения
Последняя функция Vue - это динамические CSS значения, управляемые состоянием. В современном CSS есть тенденция использовать кастомные свойства как способ динамического обновления значения некоторых CSS свойств. Это может сделать наш CSS более гибким и хорошо взаимодействовать с другим кодом нашего приложения. Давайте посмотрим на пример компонента, который отображает индикатор выполнения:
<template>
<div>
<strong>
Progress
</strong>
<div>{{ progress }}%</div>
<div class="progress-bar">
<div></div>
</div>
</div>
</template>
<script setup>
import { watch } from 'vue'
const props = defineProps({
progress: {
type: Number,
required: true
}
})
watch(props.progress,
(value) =>
document
.documentElement
.style
.setProperty('--complete-percentage', value + '%'),
{
immediate: true
})
</script>
<style scoped>
.progress-bar {
background-color: #ccc;
border-radius: 13px;
padding: 3px;
}
.progress-bar > div {
background-color: #000;
width: var(--complete-percentage);
height: 8px;
border-radius: 10px;
transition-property: width;
transition-duration: 150ms;
}
</style>
Этот компонент принимает число (progress
), затем отображает это число и обновляет кастомное CSS свойство. По мере изменения хода выполнения CSS свойство постоянно обновляется, чтобы оставаться в синхронизации с JavaScript значением.
Однако в Vue 3.2 нам предоставляется специальная CSS функция, которая делает все это за нас! Взгляните на обновленный код:
<template>
<div>
<strong>
Progress
</strong>
<div>{{ progress }}%</div>
<div class="progress-bar">
<div></div>
</div>
</div>
</template>
<script setup>
const props = defineProps({
progress: {
type: Number,
required: true
}
})
</script>
<style scoped>
.progress-bar {
background-color: #ccc;
border-radius: 13px;
padding: 3px;
}
.progress-bar > div {
background-color: #000;
width: v-bind(props.progress);
height: 8px;
border-radius: 10px;
transition-property: width;
transition-duration: 150ms;
}
</style>
Используя v-bind (props.progress
), мы устранили необходимость в нашем наблюдателе, и теперь стал ясно, что наш CSS синхронизируется со значением props.progress
. Под капотом Vue делает для нас то же самое с кастомным свойством, но это намного приятнее, чем писать его самостоятельно.
Заключение
На практике CSS - сложный язык, и его смешивание с JavaScript еще более усложняет задачу. Vue предоставляет разработчикам инструменты для надежной и предсказуемой обработки CSS, что способствует построению компонентной архитектуры. В следующий раз, когда у вас возникнут проблемы с CSS во Vue, посмотрите, может ли вам пригодиться один из этих методов!
Комментарии (14)
ninJo
06.11.2021 17:40сегодня как раз столкнулся с тем что нужно было поменять цсс слота внутри scoped компонента, вконце сделал prop в которое передавал цсс свойство, после прочтения статья перепишу этот код немного.
Max_JK
06.11.2021 18:55На самом деле плюс css модулей больше не в предотвращении опечаток, а в том, что:
можно импортировать один файл стилей в разные компоненты как обычный js объект и удобно работать с ним из кода
следуя из п1: переиспользование css кода без влезания в глобальную область.
каждый класс уникален для всего приложения, на выходе получается более компактный код
нет проблем с вложенными компонентами, проще следить за деревом стилей т.к на входе и выходе одинаковый css код
zorn-v
07.11.2021 03:19Ну и еще не надо заморачиваться с именованием и т.п.
Не проползет какой нибудь `color: red` в другой элемент.
PS. Мы про однофайловые компоненты vue
zorn-v
07.11.2021 03:11Классные штуки которых не хватало. Как минимум прокидывание стиля для слотов и/или чилдренов, не делая его глобальным.
Подумал уже будто я дурак, но нет - "доступные в Vue 3.2" :D
hazg
07.11.2021 10:06svelte из коробки инкапсулирует стили компонентов. Впрочем, оба автора вполне себе сошлись.
DDroll
08.11.2021 13:09+2Веганы мира фронтенда тут, как тут) Как бы, OK, и что? Тут целая статья о том, что Вью может инкапсулировать CSS, может не инкапсулировать, и еще может инкапсулировать с подвывертами, если того требует ситуация. В чем смысл вашего комментария? Напомнить о Свелти? Хорош фреймворк, если о нем приходится напоминать, чтобы не забыли.
hazg
08.11.2021 17:01Ладно. Вот такая загадка (не про шашлык)
vue -> svelte -> vite
в чем фишка и как связанно?
korsetlr473
07.11.2021 18:46Так и не смог понять какой самый красивый способ для моего варианта.
Есть компонент "Покупка" где формочки поля и т.д.
хотим в них менять только стили например
<buy theme="red"></style>
Внутри используем tailwind , и каждая формочка превращается в длиннющий ад:
<input class="{ "aaa": theme=="red", "bb": theme="green", "shadow": theme == "red" }"
и так у каждого контрола
hardtop
08.11.2021 09:51А чем tailwind сильно отличается от inline-style внутри html? Как по мне - это дорога в ад, просто под другим названием и благими намерениями (красивым дизайном).
Devoter
10.11.2021 10:55В свойство класс можно передать объект с полями - именами классов и булевыми значениями (добавить/не добавлять) (в том числе computed). Генерируйте объект и передавайте лишь его в класс вместо вот этой вот портянки в шаблоне. (Частное, не претендующее на звание истины мнение).
korsetlr473
10.11.2021 15:45да интересный вариант , есть какойто пример?
Devoter
11.11.2021 06:28Подобный пример приведен на странице документации. Я уже давно использую Composition API, поэтому мой пример будет выглядеть примерно вот так:
<template> <div :class="componentClass"></div> </template> <script lang="ts"> // Vue 2 + Vue Composition API plugin + TypeScript import { defineComponent, computed } from '@vue/composition-api'; interface Props { disabled?: boolean; highlight?: boolean; strong?: boolean; } function setup(props: Props) { const componentClass = computed(() => { let color: string; if (props.highlight) color = 'highlight'; else if (props.strong) color = 'strong'; else color = 'normal'; return { disabled: Boolean(props.disabled), [color]: true }; }); return { componentClass }; } export default defineComponent({ props: { disabled: Boolean, highlight: Boolean, strong: Boolean }, setup }); </script> <style scoped> .normal { color: #535353; } .highlight { color: #30de30; } .strong { color: #303030; } .disabled { color: #939393; user-select: none; } </style>
Focushift
CSS модули
что? вы одну строку заменили другой, с жестким указанием имени класса
вообще ничего не понятно, как можно переименовать объект, если ты напрямую ссылаешься на название поля объекта?
qmzik Автор
Здесь подразумевается что снижается риск опечатки обычной строки, а поле объекта вам подскажет IDE
Объект $style можно переименовать, я думаю это автор имел ввиду