5. Компоненты и автоимпорт
🧩 Компоненты и авто-импорт в Nuxt 3
Заголовок раздела «🧩 Компоненты и авто-импорт в Nuxt 3»Nuxt 3 полностью избавляет от необходимости вручную импортировать компоненты. Создал .vue файл в нужной папке — компонент автоматически доступен во всём приложении. Это не магия, это умная система конвенций.
Как работает авто-импорт компонентов 🔮
Заголовок раздела «Как работает авто-импорт компонентов 🔮»components/├── AppHeader.vue → <AppHeader>├── AppFooter.vue → <AppFooter>├── Button.vue → <Button>└── ui/ ├── Card.vue → <UiCard> ├── Badge.vue → <UiBadge> └── form/ ├── Input.vue → <UiFormInput> └── Select.vue → <UiFormSelect>Правило именования: путь папки + имя файла = имя компонента:
components/Modal/Header.vue → <ModalHeader>components/Blog/PostCard.vue → <BlogPostCard>components/Admin/User/Table.vue → <AdminUserTable>Базовый компонент 📦
Заголовок раздела «Базовый компонент 📦»<script setup lang="ts">interface Props { variant?: 'primary' | 'secondary' | 'danger' size?: 'sm' | 'md' | 'lg' loading?: boolean disabled?: boolean}
const props = withDefaults(defineProps<Props>(), { variant: 'primary', size: 'md', loading: false, disabled: false,})
const emit = defineEmits<{ click: [event: MouseEvent]}>()</script>
<template> <button :class="['btn', \`btn-\${variant}\`, \`btn-\${size}\`]" :disabled="disabled || loading" @click="emit('click', $event)" > <span v-if="loading" class="spinner" /> <slot /> </button></template>Использование без импорта:
<template> <!-- UiButton доступен автоматически! --> <UiButton variant="primary" @click="doSomething"> Нажми меня </UiButton></template>Глобальные компоненты 🌍
Заголовок раздела «Глобальные компоненты 🌍»Компоненты в components/global/ доступны везде, включая layouts и плагины:
components/└── global/ ├── Icon.vue → <Icon> — везде доступен ├── Modal.vue → <Modal> └── Toast.vue → <Toast>Или в nuxt.config.ts:
export default defineNuxtConfig({ components: { dirs: [ { path: '~/components/global', global: true, }, '~/components', ] }})Lazy компоненты 💤
Заголовок раздела «Lazy компоненты 💤»Для компонентов, которые не нужны немедленно:
<template> <!-- LazyMyModal — загружается только когда нужен --> <LazyMyModal v-if="showModal" @close="showModal = false" />
<!-- LazyHeavyChart — загружается при появлении --> <LazyHeavyChart v-if="chartVisible" :data="chartData" />
<!-- LazyBlogComments — ленивая загрузка по клику --> <LazyBlogComments v-if="commentsLoaded" /></template>
<script setup>const showModal = ref(false)const chartVisible = ref(false)const commentsLoaded = ref(false)</script>Nuxt не скачивает чанк с LazyMyModal пока showModal не стало true. Отлично для:
- 📊 Тяжёлых чартов и виджетов
- 💬 Комментариев и сложных форм
- 🗺️ Карт (Leaflet, Google Maps)
- 📝 Rich text редакторов
Динамические компоненты 🔄
Заголовок раздела «Динамические компоненты 🔄»<script setup>import { resolveComponent } from 'vue'
const currentTab = ref('overview')
// Динамический выбор компонентаconst currentComponent = computed(() => { const components = { overview: resolveComponent('TabOverview'), details: resolveComponent('TabDetails'), settings: resolveComponent('TabSettings'), } return components[currentTab.value]})</script>
<template> <div> <nav> <button @click="currentTab = 'overview'">Обзор</button> <button @click="currentTab = 'details'">Детали</button> <button @click="currentTab = 'settings'">Настройки</button> </nav>
<!-- Динамический компонент --> <component :is="currentComponent" /> </div></template>Компоненты из других директорий 📁
Заголовок раздела «Компоненты из других директорий 📁»export default defineNuxtConfig({ components: [ // Дефолтная директория '~/components',
// Дополнительные директории { path: '~/modules/blog/components', prefix: 'Blog', }, { path: '~/modules/shop/components', prefix: 'Shop', }, ]})modules/blog/components/├── Card.vue → <BlogCard>└── List.vue → <BlogList>
modules/shop/components/├── Cart.vue → <ShopCart>└── Product.vue → <ShopProduct>Передача атрибутов (inheritAttrs) 🎯
Заголовок раздела «Передача атрибутов (inheritAttrs) 🎯»<script setup lang="ts">// Отключаем наследование атрибутов на корневой элементdefineOptions({ inheritAttrs: false })
defineProps<{ label?: string error?: string}>()</script>
<template> <div class="input-wrapper"> <label v-if="label">{{ label }}</label>
<!-- Явно передаём $attrs на input, а не на div --> <input v-bind="$attrs" class="input" />
<span v-if="error" class="error">{{ error }}</span> </div></template>Использование — все нативные атрибуты попадут на <input>:
<UiInput label="Email" type="email" required v-model="email" :error="errors.email"/>TypeScript и defineExpose 🔷
Заголовок раздела «TypeScript и defineExpose 🔷»<script setup lang="ts">const inputRef = ref<HTMLInputElement>()const query = ref('')
const focus = () => inputRef.value?.focus()const clear = () => { query.value = '' }const getValue = () => query.value
// Экспортируем методы для родительского компонентаdefineExpose({ focus, clear, getValue })</script><script setup lang="ts">const searchInput = ref<InstanceType<typeof SearchInput>>()
onMounted(() => { // Вызываем метод дочернего компонента searchInput.value?.focus()})</script>
<template> <SearchInput ref="searchInput" /></template>Компоненты с Slots и Scoped Slots 🎰
Заголовок раздела «Компоненты с Slots и Scoped Slots 🎰»<script setup lang="ts">defineProps<{ items: any[] columns: { key: string; label: string }[]}>()</script>
<template> <table> <thead> <tr> <th v-for="col in columns" :key="col.key"> <!-- Именованный слот для кастомного заголовка --> <slot :name="\`header-\${col.key}\`" :column="col"> {{ col.label }} </slot> </th> </tr> </thead> <tbody> <tr v-for="item in items" :key="item.id"> <td v-for="col in columns" :key="col.key"> <!-- Scoped слот с данными строки --> <slot :name="\`cell-\${col.key}\`" :item="item" :value="item[col.key]"> {{ item[col.key] }} </slot> </td> </tr> </tbody> </table></template><!-- Использование с кастомными ячейками --><DataTable :items="users" :columns="columns"> <template #cell-avatar="{ item }"> <img :src="item.avatar" :alt="item.name" /> </template>
<template #cell-status="{ value }"> <UiBadge :variant="value === 'active' ? 'success' : 'warning'"> {{ value }} </UiBadge> </template></DataTable>Provide/Inject с компонентами 💉
Заголовок раздела «Provide/Inject с компонентами 💉»<!-- components/Dropdown.vue — Provider --><script setup lang="ts">const isOpen = ref(false)const toggle = () => { isOpen.value = !isOpen.value }
// Предоставляем контекст дочерним компонентамprovide('dropdown', { isOpen, toggle })</script>
<template> <div class="dropdown"> <slot /> </div></template><script setup>const { toggle } = inject('dropdown')</script>
<template> <button @click="toggle"> <slot /> </button></template>Конвенции и лучшие практики 💡
Заголовок раздела «Конвенции и лучшие практики 💡»✅ Структура компонентов:components/├── app/ ← Компоненты приложения (AppHeader, AppNav)├── ui/ ← Переиспользуемые UI элементы│ ├── Button.vue│ ├── Card.vue│ └── form/│ ├── Input.vue│ └── Select.vue├── sections/ ← Секции страниц (HeroSection, FeaturesSection)├── widgets/ ← Виджеты (WeatherWidget, NewsWidget)└── global/ ← Глобальные компоненты
✅ Именование:- PascalCase для имён файлов- Описательные имена: UserProfileCard, not Card2- Префикс для группировки: UiButton, BlogPostCard
❌ Избегай:- Компоненты без TypeScript- Слишком крупные компоненты (>300 строк)- Бизнес-логику в UI компонентах