Vue: концепция компонентов Печать
Добавил(а) microsin   

Концепция компонентов фреймворка Vue [1] — основополагающая идея о том, что пользовательский интерфейс (UI) можно представить в виде дерева небольших, переиспользуемых, самостоятельных блоков, каждый из которых отвечает за свою часть логики и отображения.

Вместо одного огромного файла вы создаёте приложение из компонентов, как собираете сложную структуру из LEGO. Основная модель компонента — это однофайловый компонент (`.vue` файл), который инкапсулирует три части: шаблон (HTML), логику (JavaScript/TypeScript) и стили (CSS). Некую аналогию компонента можно произвести с классом языка C++ или модулем/структурой языка C, в которую встроены переменные и указатели на функции.

[Ключевые принципы компонентов Vue]

Принцип Суть и пример
Инкапсуляция Компонент объединяет связанные HTML, JS и CSS в одном файле (`.vue`). Внешнему миру доступен только его публичный интерфейс (props и события), а внутренняя реализация скрыта.
Переиспользование Один раз созданный компонент (например, кнопка < BaseButton>, карточка < ProductCard>) можно использовать многократно в разных частях приложения с разными данными.
Иерархия и композиция Приложение — это дерево компонентов. Сложные компоненты создаются путём вложений более простых (композиция). Например, < App> содержит < Header>, который содержит < NavMenu>.
Односторонний поток данных Данные передаются сверху вниз от родительских компонентов к дочерним через props (входные параметры). Это делает поток данных предсказуемым.
Коммуникация через события Дочерние компоненты общаются с родителями, отправляя пользовательские события ($emit). Родитель может на них реагировать.
Реактивность Когда данные компонента меняются, Vue автоматически и эффективно обновляет связанные с ними части DOM. Разработчик управляет состоянием, а фреймворк заботится о его синхронизации с представлением.
Жизненный цикл Каждый компонент проходит этапы: создание (инициализация), монтирование в DOM, обновление и удаление. На каждом этапе можно выполнять свой код с помощью хуков жизненного цикла (onMounted, onUnmounted и др.).

[Анатомия однофайлового компонента (SFC - Single File Component)]

Например, файл MyComponent.vue выглядит так:

< !-- 1. Шаблон (HTML + директивы Vue) -->
< template>
  < div class="my-component">
    < h1>{{ title }}< /h1>
    < button @click="handleClick">Нажми меня< /button>
  < /div>
< /template>

< !-- 2. Логика (JavaScript/TypeScript) --> < script setup>import { ref } from 'vue'

// Реактивное состояние
const title = ref('Привет, компонент!')

// Метод
const handleClick = () => {
title.value = 'Кнопка нажата!' } < /script>

< !-- 3. Стили (CSS/SCSS) --> < style scoped> .my-component {
border: 1px solid #ccc; } < /style>

[Как компоненты общаются: Props и Events]

Связь "Родитель ↔ Ребёнок" - основа архитектуры Vue.

< !-- РОДИТЕЛЬСКИЙ КОМПОНЕНТ -->
< template>
  < !-- 1. Передаём данные "сверху вниз" через props -->
  < ChildComponent :message="parentMessage" />
  < !-- 2. Слушаем события "снизу вверх" -->
  < ChildComponent @child-clicked="handleChildClick" />
< /template>

< !-- ДОЧЕРНИЙ КОМПОНЕНТ --> < template> < div @click="$emit('child-clicked', someData)"> {{ message }} < !-- Отображаем полученный prop --> < /div> < /template>
< script setup> defineProps(['message']) // Объявляем входные параметры defineEmits(['child-clicked']) // Объявляем события < /script>

[Дополнительные возможности компонентов]

Слоты (Slots) — позволяют родителю передавать дочернему компоненту произвольное содержимое (разметку, другие компоненты), делая его более гибким.

Асинхронные компоненты — можно загружать компоненты "лениво", по мере необходимости, что повышает производительность больших приложений.

Динамические компоненты — можно переключаться между разными компонентами с помощью `< component :is="currentComponent">`.

Функциональные компоненты (в Options API) — компоненты без состояния, работающие как чистые функции от своих props.

[Сравнение с другими фреймворками]

Фреймворк Основная модель компонента Ключевое отличие Vue
React Компоненты как функции (JSX). Vue использует синтаксис, близкий к стандартному HTML и JS, отделяет шаблон от логики.
Angular Классы с декораторами. Vue легче, использует более простой и декларативный шаблонный синтаксис.

[Преимущества подхода Vue к компонентам]

1. Ясность и структурированность — код организован логично, его легко читать и поддерживать.

2. Лёгкость переиспользования — компоненты становятся строительными блоками для всей кодовой базы.

3. Эффективность командной работы — разные разработчики могут работать над разными компонентами, не мешая друг другу.

4. Тестируемость — изолированный компонент проще покрыть модульными тестами.

Концепция компонентов — это сердце Vue. Она превращает разработку интерфейса из написания скриптов в проектирование системы взаимодействующих, переиспользуемых блоков. Именно эта концепция позволяет Vue-приложениям оставаться поддерживаемыми и масштабируемыми.

[Создание компонентов приложения Vue]

Общепринятая концепция - держать помещать каждый компонент в отдельную директорию components, которая находится в папке src. Далее показан пошаговый пример создания компонента для кнопки.

1. Создайте в папке src папку components.

2. В папке components создайте файл Button.vue. В этом файле создайте 3 пустые секции:

< script setup>< /script>
< template>< /template>
< style scoped>< /style>

Такой единый шаблон применяется для всех компонентов, в том числе и для компонента самого приложения App. Однако файл приложения компонента App.vue находится в непосредственно в папке src.

3. В теге template создайте вложенный тег button:

< template>
    < button>Кнопка< /button>
< /template>

Это необходимый минимум, который нужен для создания компонента.

Чтобы использовать этот компонент в другом (родительском) компоненте, его необходимо импортировать.

4. Откройте файл App.vue, и добавьте в его тег < script setup> директиву импорта:

import MyButton from "./components/Button.vue"

Здесь вместо MyButton может быть любое осмысленное имя.

5. Добавьте MyButton в шаблон App.vue:

< template>
    < MyButton />
< /template>

[Добавление стиля к компоненту]

1. Добавьте в файле компонента Button.vue, в теге button атрибут class:

< template>
    < button class="button">Кнопка< /button>
< /template>

Здесь class="button" создает ссылку на класс CSS. Имя класса может быть произвольным, главное чтобы оно совпадало с именем в описании стиля.

2. В файле компонента Button.vue, в теге style scoped создайте описание стиля класса button:

< style scoped>
.button {
border: none;
border-radius: 10px; }
< /style>

scoped в теге < style scoped> компонентов Vue используется для ограничения области действия CSS-стилей только текущим компонентом. Если убрать scoped, то стиль для компонента станет глобальным, и применится ко всему проекту.

Как это работает:

1. Изоляция стилей - с использованием scoped стили применяются только к элементам текущего компонента.

2. Автоматическая генерация атрибутов - Vue добавляет уникальные data-атрибуты к элементам компонента.

3. Преобразование CSS-селекторов - селекторы модифицируются так, чтобы соответствовать только элементам с этими атрибутами.

Пример преобразования:

< template>
  < div class="button">Нажми меня< /div>
< /template>
< style scoped> .button {
background-color: blue; color: white; } < /style>

Vue преобразует это в примерно такой HTML-код:

< div data-v-f3f3eg9 class="button">Нажми меня< /div>
< style> .button[data-v-f3f3eg9] {
background-color: blue; color: white; } < /style>

Преимущества:

- Предотвращение конфликтов стилей между компонентами
- Автоматическая изоляция без именования классов вручную
- Более чистая разработка - можно использовать простые имена классов

Ограничения:

Глубокие селекторы - для стилизации дочерних компонентов нужно использовать `::v-deep`, `:deep()` или `>>>`:

:deep(.child-class) {
color: red; }

Глобальные стили - если нужны глобальные стили, используйте < style> без scoped, или отдельный CSS-файл.

Когда использовать scoped:

- Компоненты многократного использования - для предотвращения стилевых конфликтов.
- Библиотеки компонентов - для обеспечения изоляции.
- Большие приложения - для поддержания порядка в стилях.

Когда не использовать:

- Глобальные стили (reset.css, переменные CSS).
- Стилизация корневого элемента приложения.
- Когда нужен доступ к стилям извне компонента.

scoped - один из ключевых инструментов Vue для создания поддерживаемых и изолированных компонентов.

3. В определении стиля можно использовать переменные из корневого файла style.css с помощью var. Например, в файле style.css есть определение градиента --gradient:

:root {
--gradient: linear-gradient(90.8deg, rgba(144, 217, 224, 0.9) 0.2%, rgba(84, 96, 230, 0.9) 100%); }

body{ }

Тогда это определение можно применить к компоненту следующим образом. В файле Button.vue добавьте background: var(--gradient);:

< style scoped>
.button {
border: none;
border-radius: 10px;
background: var(--gradient); } < /style>

Пример полного настроенного стиля для кнопки:

< style scoped>
.button {
border: none;
border-radius: 10px;
background: var(--gradient);
padding: 14px;
font-family: var(--font);
font-size: 20px;
font-weight: 600;
color: var(--primary);
cursor: pointer; }
.button:hover {
background: var(--gradient-inverted); } < /style>

Этот scoped стиль определяет стилизацию для кнопки в Vue-компоненте с изоляцией. Давайте расшифруем каждое свойство:

border: none; - убирает стандартную рамку кнопки

border-radius: 10px; - скругляет углы на 10 пикселей

background: var(--gradient); - задаёт градиентный фон через CSS-переменную --gradient

padding: 14px; - внутренние отступы со всех сторон

font-family: var(--font); - шрифт через CSS-переменную --font

font-size: 20px; - размер шрифта 20px (крупный, для акцента)

font-weight: 600; - полужирное начертание (полужирное)

color: var(--primary); - цвет текста через CSS-переменную --primary

cursor: pointer; - меняет курсор на "руку" при наведении

Состояние при наведении .button:hover:

background: var(--gradient-inverted); - меняет градиент при наведении (инвертированный вариант, описанный в CSS-переменной --gradient-inverted)

Ключевые особенности:

Использование CSS-переменных (Custom Properties):

--gradient, --gradient-inverted - градиенты для разных состояний
--font - шрифтовая гарнитура
--primary - основной цвет текста

Это говорит о том, что тема компонента настраивается через глобальные CSS-переменные, что позволяет:

- Легко менять тему всего приложения
- Создать согласованный дизайн-систему
- Настраивать кнопку извне компонента (несмотря на scoped)

Визуальные характеристики кнопки:

Интерактивная - меняет фон при наведении
Безрамная с закруглениями - современный плоский дизайн
Крупная (padding 14px + font-size 20px)
Выделенная (полужирный шрифт 600)
Доступная (курсор pointer указывает на кликабельность)

Что означает scoped в этом контексте: стили применятся ТОЛЬКО к элементам с классом .button внутри этого конкретного компонента, даже если в других компонентах есть элементы с таким же классом.

Как будет выглядеть кнопка в разметке:

< button class="button" data-v-abc123>Текст кнопки< /button>
< !-- или --> < div class="button" data-v-abc123>Кнопка-див< /div>

[Тег slot]

С помощью тега < slot>< /slot> в компоненте можно передавать содержимое из компонента-родителя (обычно это компонент приложения App.vue) в дочерний компонент. Пример для дочернего компонента нашей кнопки показан ниже. Файл App.vue:

< script setup>
import MyButton from "./components/Button.vue" < /script>
< template> < MyButton>Сохранить< /MyButton> < /template>
< style scoped> < /style>

В файле Button.vue:

< template>
    < button class="button">
        < slot>< /slot>
    < /button>
< /template>

Тег < slot> в этом примере выполняет критически важную функцию — это система content projection Vue, аналогичная `children` в React или transclusion в Angular. В результате содержимое из < MyButton>< /MyButton> будет перенесено на компонент кнопки (в этом примере текст "Сохранить").

Замечание: тег < slot>< /slot> можно заменить на самозакрывающийся < slot />, что дает то же самое.

В компоненте Button.vue тег slot определит место для вставки контента из родителя:

< button class="button">
    < slot>< /slot>
< /button>

< slot> — это placeholder, который говорит: "Здесь будет отображаться контент, который передаст родительский компонент".

В компоненте App.vue:

< MyButton>Сохранить< /MyButton>

Текст "Сохранить" попадает внутрь < slot>< /slot> компонента Button.vue.

< !-- Результат рендеринга в HTML -->
< button class="button" data-v-код-компонента>
    Сохранить
< /button>

Аналогия из реальной жизни: представьте шаблон кнопки как рамку для фото:

- Button.vue — сама рамка с определённым дизайном
- < slot> — пустое место внутри рамки
- App.vue — вы вставляете фотографию ("Сохранить") в эту рамку

Преимущества использования слотов:

1. Переиспользуемость компонентов. Один компонент кнопки можно использовать с разным текстом:

< MyButton>Отправить< /MyButton>
< MyButton>Удалить< /MyButton>
< MyButton>Отмена< /MyButton>

2. Гибкость содержимого. В слот можно передавать не только текст, но и другие элементы. Например в App.vue:

< template>
  < MyButton>< span class="icon">+< /span> Сохранить< /MyButton>
  < MyButton>< strong>ВАЖНО:< /strong>Подтвердить< /MyButton>
< /template>

3. Разделение ответственности:

- Button.vue отвечает за внешний вид, поведение, доступность
- App.vue отвечает за содержимое, контекст использования

4. Fallback контент. Можно задать контент по умолчанию:

< !-- Button.vue -->
< button class="button">
    < slot>Кнопка< /slot> < !-- "Кнопка" отобразится если слот пустой -->
< /button>

Без слота пришлось бы:

Вариант 1. Использовать props, что хуже, потому что нельзя передавать HTML/компоненты.

< !-- Button.vue -->
< button class="button">{{ text }}< /button>

< !-- App.vue --> < MyButton text="Сохранить" />

Вариант 2. Дублировать код, что намного хуже - нарушение принципа DRY (Don't Repeat Yourself), сложное обновление.

< !-- App.vue -->
< button class="button">Сохранить< /button>
< button class="button">Отправить< /button>
< button class="button">Удалить< /button>

[Несколько slot]

В компоненте можно применить несколько slot, используемых для разных целей. Для этого в slot вставляется атрибут name. Например для компонента Button.vue:

< template>
    < button class="button">
        < slot name="icon" />
        < slot />
    < /button>
< /template>

В этом примере определено 2 слота (оба самозакрывающиеся), один с именем icon, а второй безымянный. Можно определить произвольное количество именованных слотов с уникальными именами. Имя слота (в данном примере "icon") произвольное, и оно используется для ссылки на этот слот в родительском компоненте.

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

В родительском компоненте несколько слотов используются с помощью вложенных тегов template и директивы v-slot, например:

< template>
  < MyButton>
    < template v-slot:icon>+< /template>
    Сохранить
  < /MyButton>
< /template>

Также существует сокращенный вариант директивы v-slot, '#':

< template>
  < MyButton>
    < template #icon>+< /template>
    Сохранить
  < /MyButton>
< /template>

[Динамический слот]

< template>
  < MyButton>
    < template #[имя_переменной]>+< /template>
    Сохранить
  < /MyButton>
< /template>
< template #[имя_переменной]>+< /template>

Здесь имя_переменной это имя JavaScript переменной или константы. Подразумевается, что у нас определен слот, имя которого совпадает с имя_переменной.

[Ссылки]

1. Vue 3, быстрый старт.