8. Компоненты
Vue Компоненты — строительные блоки UI 🧱
Заголовок раздела «Vue Компоненты — строительные блоки UI 🧱»Компонент — это как LEGO кубик: самостоятельный, переиспользуемый, с чётким интерфейсом. Маленькие компоненты собираются в большие, большие — в страницы, страницы — в приложение. Научись делать правильные компоненты — и твой код будет радовать тебя годами! 🎉
Single File Component — анатомия SFC 🧬
Заголовок раздела «Single File Component — анатомия SFC 🧬»<template> <!-- Шаблон — может иметь НЕСКОЛЬКО корневых элементов (Fragments!) --> <article class="user-card"> <img :src="user.avatar" :alt="user.name" class="avatar" /> <div class="info"> <h3>{{ user.name }}</h3> <p>{{ user.bio }}</p> </div> </article></template>
<script setup lang="ts">// TypeScript + Composition APIinterface User { id: number name: string bio: string avatar: string}
const props = defineProps<{ user: User }>()const emit = defineEmits<{ click: [user: User] }>()</script>
<style scoped>.user-card { display: flex; gap: 12px; padding: 16px; border-radius: 12px; border: 1px solid #e2e8f0;}</style>Регистрация компонентов 📝
Заголовок раздела «Регистрация компонентов 📝»Глобальная регистрация
Заголовок раздела «Глобальная регистрация»// main.ts — доступны везде в приложенииimport { createApp } from 'vue'import App from './App.vue'import BaseButton from '@/components/common/BaseButton.vue'import BaseInput from '@/components/common/BaseInput.vue'import BaseCard from '@/components/common/BaseCard.vue'
const app = createApp(App)
// Регистрируем глобальные компонентыapp.component('BaseButton', BaseButton)app.component('BaseInput', BaseInput)app.component('BaseCard', BaseCard)
app.mount('#app')// Автоматическая регистрация всех Base* компонентов// (работает в Vite)const components = import.meta.glob('./components/Base*.vue', { eager: true })Object.entries(components).forEach(([path, component]) => { const name = path.split('/').pop()?.replace('.vue', '') || '' app.component(name, (component as any).default)})Локальная регистрация (рекомендуется!)
Заголовок раздела «Локальная регистрация (рекомендуется!)»<script setup lang="ts">// В <script setup> достаточно просто импортировать — авто-регистрация!import UserCard from '@/components/UserCard.vue'import ProductList from '@/components/ProductList.vue'import { ref } from 'vue'
const users = ref([...])</script>
<template> <UserCard v-for="user in users" :key="user.id" :user="user" /> <ProductList /></template>Именование компонентов 📛
Заголовок раздела «Именование компонентов 📛»✅ Правила именования компонентов:
SFC файлы: PascalCase → UserCard.vue → ProductList.vue → BaseButton.vue
В шаблоне: PascalCase ИЛИ kebab-case → <UserCard /> или <user-card /> → PascalCase — рекомендуется, легче найти компонент
Соглашения: Base* / App* / The* — базовые/единственные компоненты → BaseButton.vue, BaseInput.vue → TheHeader.vue, TheSidebar.vue (только один в приложении!)
Префикс фичи для связанных компонентов: → TodoList.vue, TodoItem.vue, TodoAddForm.vue → UserList.vue, UserCard.vue, UserAvatar.vueДинамические компоненты — <component :is> 🔄
Заголовок раздела «Динамические компоненты — <component :is> 🔄»<template> <!-- Динамически меняем компонент --> <component :is="currentComponent" v-bind="componentProps" /></template>
<script setup lang="ts">import { ref, computed, shallowRef } from 'vue'import TabHome from '@/components/TabHome.vue'import TabProfile from '@/components/TabProfile.vue'import TabSettings from '@/components/TabSettings.vue'
const tabs = [ { name: 'home', label: 'Главная', component: TabHome }, { name: 'profile', label: 'Профиль', component: TabProfile }, { name: 'settings', label: 'Настройки', component: TabSettings }]
// shallowRef для компонентов — не делаем reactive из компонента!const currentTab = ref('home')const currentComponent = computed( () => tabs.find(t => t.name === currentTab.value)?.component)</script><KeepAlive> — сохранение состояния 💾
Заголовок раздела «<KeepAlive> — сохранение состояния 💾»<template> <!-- Без KeepAlive — компонент пересоздаётся при каждом показе --> <TabHome v-if="activeTab === 'home'" />
<!-- С KeepAlive — компонент сохраняет состояние (кешируется) --> <KeepAlive> <component :is="activeComponent" /> </KeepAlive>
<!-- include/exclude — кешировать только определённые компоненты --> <KeepAlive include="TabHome,TabProfile"> <component :is="activeComponent" /> </KeepAlive>
<!-- max — максимум кешированных компонентов --> <KeepAlive :max="10"> <component :is="activeComponent" /> </KeepAlive></template>
<script setup lang="ts">import { onActivated, onDeactivated } from 'vue'
// Lifecycle hooks для KeepAlive компонентовonActivated(() => { // Вызывается при активации (вместо onMounted при повторном показе) console.log('Компонент активирован из кеша') refreshData()})
onDeactivated(() => { // Вызывается при деактивации (вместо onUnmounted при скрытии) console.log('Компонент деактивирован и закеширован')})</script><Transition> — анимации переходов ✨
Заголовок раздела «<Transition> — анимации переходов ✨»<template> <!-- Одиночный элемент/компонент --> <Transition name="fade"> <div v-if="isVisible" class="box">Исчезающий блок</div> </Transition>
<!-- Transition между компонентами --> <Transition name="slide" mode="out-in"> <component :is="activeComponent" :key="activeTab" /> </Transition></template>
<style scoped>/* fade переход */.fade-enter-active,.fade-leave-active { transition: opacity 0.3s ease;}.fade-enter-from,.fade-leave-to { opacity: 0;}
/* slide переход */.slide-enter-active,.slide-leave-active { transition: all 0.3s ease;}.slide-enter-from { transform: translateX(20px); opacity: 0;}.slide-leave-to { transform: translateX(-20px); opacity: 0;}</style><TransitionGroup> — анимации списков
Заголовок раздела «<TransitionGroup> — анимации списков»<template> <TransitionGroup name="list" tag="ul" class="todo-list"> <li v-for="item in items" :key="item.id" class="todo-item"> {{ item.text }} </li> </TransitionGroup></template>
<style scoped>.list-enter-active,.list-leave-active { transition: all 0.3s ease;}.list-enter-from { opacity: 0; transform: translateY(-10px);}.list-leave-to { opacity: 0; transform: translateX(20px);}/* Плавное движение соседних элементов */.list-move { transition: transform 0.3s ease;}</style>Асинхронные компоненты 🕐
Заголовок раздела «Асинхронные компоненты 🕐»import { defineAsyncComponent } from 'vue'
// Простая загрузка — lazy loading!const HeavyChart = defineAsyncComponent( () => import('@/components/HeavyChart.vue'))
// С настройками загрузки и ошибокconst AsyncUserProfile = defineAsyncComponent({ loader: () => import('@/components/UserProfile.vue'),
// Компонент-заглушка пока грузится loadingComponent: LoadingSpinner,
// Задержка перед показом LoadingSpinner (мс) delay: 200,
// Компонент при ошибке errorComponent: ErrorDisplay,
// Таймаут загрузки timeout: 3000})<!-- Использование с Suspense --><template> <Suspense> <template #default> <AsyncUserProfile :userId="userId" /> </template> <template #fallback> <LoadingSpinner /> </template> </Suspense></template>defineComponent — явный компонент с типами 🔷
Заголовок раздела «defineComponent — явный компонент с типами 🔷»// Когда нужны полные TypeScript типы в Options APIimport { defineComponent, PropType } from 'vue'
interface Product { id: number name: string price: number category: string}
export const ProductCard = defineComponent({ name: 'ProductCard',
props: { product: { type: Object as PropType<Product>, required: true }, showActions: { type: Boolean, default: true } },
emits: { addToCart(product: Product) { return product.id > 0 } },
setup(props, { emit }) { const handleAddToCart = () => { emit('addToCart', props.product) }
return { handleAddToCart } }})Паттерны компонентов 💡
Заголовок раздела «Паттерны компонентов 💡»Базовый компонент (Base Component)
Заголовок раздела «Базовый компонент (Base Component)»<template> <button :class="['btn', `btn-${variant}`, { 'btn-loading': loading }]" :disabled="disabled || loading" v-bind="$attrs" @click="$emit('click', $event)" > <span v-if="loading" class="spinner">⏳</span> <slot /> </button></template>
<script setup lang="ts">defineProps<{ variant?: 'primary' | 'secondary' | 'danger' loading?: boolean disabled?: boolean}>()
defineEmits<{ click: [event: MouseEvent] }>()
// Не наследуем attrs автоматически (передаём вручную)defineOptions({ inheritAttrs: false })</script>Container/Presentational паттерн
Заголовок раздела «Container/Presentational паттерн»<!-- UserListContainer.vue — Smart компонент (логика) --><script setup lang="ts">import { ref, onMounted } from 'vue'import UserList from './UserList.vue'
const users = ref([])const loading = ref(true)
onMounted(async () => { users.value = await fetchUsers() loading.value = false})</script>
<template> <UserList :users="users" :loading="loading" @delete="deleteUser" /></template>
<!-- UserList.vue — Dumb компонент (только отображение) --><script setup lang="ts">defineProps<{ users: User[]; loading: boolean }>()defineEmits<{ delete: [userId: number] }>()</script>$attrs и наследование атрибутов 🎁
Заголовок раздела «$attrs и наследование атрибутов 🎁»<template> <!-- Атрибуты передаются в корневой элемент автоматически (fallthrough) --> <!-- class="extra" id="main" данные из родителя — прилетят сюда --> <div class="my-component">...</div></template>
<script setup lang="ts">import { useAttrs } from 'vue'
// Получаем все переданные attrs (включая class, style, event listeners)const attrs = useAttrs()
// Отключаем автоматическое наследование — управляем вручнуюdefineOptions({ inheritAttrs: false })</script>
<template> <!-- Теперь сами решаем куда передать attrs --> <div class="wrapper"> <input v-bind="attrs" /> <!-- attrs прилетят именно на input, не на wrapper --> </div></template>Резюме 🧱
Заголовок раздела «Резюме 🧱»Vue компоненты:
- SFC (
.vue) — шаблон + скрипт + стили в одном файле - Глобальная регистрация —
app.component(), доступны везде - Локальная регистрация — импортируй в
<script setup>, авто-регистрация <component :is>— динамически выбираемый компонент<KeepAlive>— кеширование состояния между скрытиями<Transition>— CSS анимации появления/исчезновенияdefineAsyncComponent()— lazy loading компонентов- Паттерн Container/Presentational — разделение логики и отображения
Следующий урок — Props и Emits! 📡