Эта статья — перевод оригинальной статьи 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)


  1. Focushift
    06.11.2021 17:18
    +1

    CSS модули

    особенно приятно работать, так как вам не придётся использовать строки в своих классах

    что? вы одну строку заменили другой, с жестким указанием имени класса

    Vue также позволяет вам переименовывать объект, так что вам не нужно обращаться к ним с помощью $style в вашем шаблоне, если вы этого не хотите

    вообще ничего не понятно, как можно переименовать объект, если ты напрямую ссылаешься на название поля объекта?


    1. qmzik Автор
      06.11.2021 17:28
      +1

      1. Здесь подразумевается что снижается риск опечатки обычной строки, а поле объекта вам подскажет IDE

      2. Объект $style можно переименовать, я думаю это автор имел ввиду


  1. ninJo
    06.11.2021 17:40

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


  1. Max_JK
    06.11.2021 18:55

    На самом деле плюс css модулей больше не в предотвращении опечаток, а в том, что:

    • можно импортировать один файл стилей в разные компоненты как обычный js объект и удобно работать с ним из кода

    • следуя из п1: переиспользование css кода без влезания в глобальную область.

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

    • нет проблем с вложенными компонентами, проще следить за деревом стилей т.к на входе и выходе одинаковый css код


    1. zorn-v
      07.11.2021 03:19

      Ну и еще не надо заморачиваться с именованием и т.п.

      Не проползет какой нибудь `color: red` в другой элемент.

      PS. Мы про однофайловые компоненты vue


  1. zorn-v
    07.11.2021 03:11

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

    Подумал уже будто я дурак, но нет - "доступные в Vue 3.2" :D


  1. hazg
    07.11.2021 10:06

    svelte из коробки инкапсулирует стили компонентов. Впрочем, оба автора вполне себе сошлись.


    1. DDroll
      08.11.2021 13:09
      +2

      Веганы мира фронтенда тут, как тут) Как бы, OK, и что? Тут целая статья о том, что Вью может инкапсулировать CSS, может не инкапсулировать, и еще может инкапсулировать с подвывертами, если того требует ситуация. В чем смысл вашего комментария? Напомнить о Свелти? Хорош фреймворк, если о нем приходится напоминать, чтобы не забыли.


      1. hazg
        08.11.2021 17:01

        Ладно. Вот такая загадка (не про шашлык)
        vue -> svelte -> vite

        в чем фишка и как связанно?



  1. korsetlr473
    07.11.2021 18:46

    Так и не смог понять какой самый красивый способ для моего варианта.

    Есть компонент "Покупка" где формочки поля и т.д.

    хотим в них менять только стили например

    <buy theme="red"></style>

    Внутри используем tailwind , и каждая формочка превращается в длиннющий ад:

    <input class="{ "aaa": theme=="red", "bb": theme="green", "shadow": theme == "red" }"

    и так у каждого контрола


    1. hardtop
      08.11.2021 09:51

      А чем tailwind сильно отличается от inline-style внутри html? Как по мне - это дорога в ад, просто под другим названием и благими намерениями (красивым дизайном).


    1. Devoter
      10.11.2021 10:55

      В свойство класс можно передать объект с полями - именами классов и булевыми значениями (добавить/не добавлять) (в том числе computed). Генерируйте объект и передавайте лишь его в класс вместо вот этой вот портянки в шаблоне. (Частное, не претендующее на звание истины мнение).


      1. korsetlr473
        10.11.2021 15:45

        да интересный вариант , есть какойто пример?


        1. 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>