3. Composition API
Composition API — сердце Vue 3 🧩
Заголовок раздела «Composition API — сердце Vue 3 🧩»Представь, что ты переехал в новую квартиру и расставляешь вещи. Options API говорит: «Все книги — в одну комнату, все инструменты — в другую, всю одежду — в третью». Удобно? Наверное. Но когда тебе нужно готовить завтрак, ты прыгаешь между кухней, кладовкой и ванной. Composition API говорит: «Всё для завтрака — на кухне, всё для работы — в кабинете». Вот разница! 🏠
Почему Composition API? 🤔
Заголовок раздела «Почему Composition API? 🤔»Проблема Options API с большими компонентами:
Options API — код одной фичи разбросан по файлу:
data() { ← 🟦 user данные здесь user: null, userLoading: false, cart: [], ← 🟥 cart данные тут}
computed: { userFullName() {...}, ← 🟦 user логика cartTotal() {...}, ← 🟥 cart логика}
methods: { fetchUser() {...}, ← 🟦 user логика (далеко от data!) addToCart() {...}, ← 🟥 cart логика removeFromCart() {...} ← 🟥 cart логика}
mounted() { this.fetchUser() ← 🟦 user логика (ещё дальше!)}Composition API — каждая фича вместе:
// useUser.ts — ВСЯ user логика в одном месте 🟦export function useUser() { const user = ref(null) const loading = ref(false) const fullName = computed(() => user.value?.name) const fetchUser = async () => { loading.value = true; ... } onMounted(fetchUser) return { user, loading, fullName, fetchUser }}
// useCart.ts — ВСЯ cart логика в одном месте 🟥export function useCart() { const cart = ref([]) const total = computed(() => cart.value.reduce(...)) const addToCart = (item) => cart.value.push(item) return { cart, total, addToCart }}<script setup> — магия без шаблонного кода ✨
Заголовок раздела «<script setup> — магия без шаблонного кода ✨»<script setup> — это синтаксический сахар Vue 3.2. Компилятор Vue превращает его в setup() автоматически:
<!-- С <script setup> — чисто и кратко --><script setup lang="ts">import { ref, computed } from 'vue'
const count = ref(0)const doubled = computed(() => count.value * 2)const increment = () => count.value++
// Всё автоматически доступно в шаблоне! ✅</script><!-- Без <script setup> — verbose эквивалент --><script lang="ts">import { ref, computed, defineComponent } from 'vue'
export default defineComponent({ setup() { const count = ref(0) const doubled = computed(() => count.value * 2) const increment = () => count.value++
// Нужно явно возвращать всё! ❌ return { count, doubled, increment } }})</script>setup() функция — подробно 🔍
Заголовок раздела «setup() функция — подробно 🔍»import { ref, computed, onMounted, defineComponent } from 'vue'
export default defineComponent({ name: 'UserProfile',
props: { userId: { type: Number, required: true } },
emits: ['updated'],
// setup() получает props и context setup(props, context) { // context содержит: // context.attrs — нереактивные атрибуты (fallthrough attrs) // context.slots — слоты // context.emit — функция emit // context.expose — контроль публичного API компонента
const { emit, expose } = context
const user = ref(null)
// props реактивны! props.userId — Proxy const userId = computed(() => props.userId)
const loadUser = async () => { // Используем props.userId напрямую user.value = await fetchUser(props.userId) }
onMounted(loadUser)
// expose — что видно через template refs expose({ reload: loadUser })
return { user, loadUser } }})defineProps — типизированные props 📥
Заголовок раздела «defineProps — типизированные props 📥»<script setup lang="ts">// Способ 1: Generic тип — только TypeScript (рекомендуется!)const props = defineProps<{ title: string count?: number // опциональный items: string[] user: { name: string; age: number }}>()
// Способ 2: Runtime объект — работает и без TypeScriptconst props = defineProps({ title: { type: String, required: true }, count: { type: Number, default: 0 }, items: { type: Array as PropType<string[]>, default: () => [] }})</script>withDefaults — значения по умолчанию для generic props
Заголовок раздела «withDefaults — значения по умолчанию для generic props»<script setup lang="ts">import { withDefaults } from 'vue'
interface Props { title: string count?: number items?: string[] theme?: 'light' | 'dark'}
// withDefaults — дефолтные значения для TypeScript propsconst props = withDefaults(defineProps<Props>(), { count: 0, items: () => [], // массивы и объекты — всегда фабрика! theme: 'light'})
// Теперь props.count — всегда number (не number | undefined)console.log(props.count) // 0console.log(props.theme) // 'light'</script>defineEmits — типизированные события 📤
Заголовок раздела «defineEmits — типизированные события 📤»<script setup lang="ts">// Способ 1: TypeScript callable syntax (Vue 3.3+) — рекомендуетсяconst emit = defineEmits<{ click: [] // без аргументов change: [value: string] // один аргумент update: [id: number, data: User] // несколько аргументов 'item-selected': [item: Item] // kebab-case событие}>()
// Способ 2: Array syntaxconst emit = defineEmits(['click', 'change', 'update'])
// Способ 3: Object syntax с валидациейconst emit = defineEmits({ click: null, // без валидации change: (value: string) => { // с валидацией return value.length > 0 }})
// Использованиеconst handleClick = () => { emit('click') emit('change', 'новое значение') emit('update', 42, { name: 'Яша' })}</script>defineExpose — публичный API компонента 🔓
Заголовок раздела «defineExpose — публичный API компонента 🔓»По умолчанию компоненты с <script setup> закрыты — родитель не может получить доступ к их данным через ref. defineExpose открывает нужное:
<script setup lang="ts">import { ref } from 'vue'
const inputRef = ref<HTMLInputElement | null>(null)const value = ref('')const count = ref(0)
// Открываем только публичный APIdefineExpose({ focus() { inputRef.value?.focus() }, reset() { value.value = ''; count.value = 0 }, getValue() { return value.value }})// count и inputRef остаются приватными!</script><script setup lang="ts">import { ref, onMounted } from 'vue'import Child from './Child.vue'
// Тип InstanceType позволяет получить тип exposeconst childRef = ref<InstanceType<typeof Child> | null>(null)
onMounted(() => { childRef.value?.focus() // ✅ доступно childRef.value?.reset() // ✅ доступно // childRef.value?.count // ❌ TypeScript ошибка!})</script>
<template> <Child ref="childRef" /></template>defineComponent — для Options API + TypeScript 🔧
Заголовок раздела «defineComponent — для Options API + TypeScript 🔧»// Когда нужны явные типы с Options APIimport { defineComponent, PropType } from 'vue'
interface User { id: number name: string}
export default defineComponent({ name: 'UserList',
props: { users: { type: Array as PropType<User[]>, required: true } },
emits: { select(user: User) { return user.id > 0 } },
setup(props, { emit }) { const selectUser = (user: User) => emit('select', user) return { selectUser } }})Логика по фиче — composables 🪝
Заголовок раздела «Логика по фиче — composables 🪝»Composition API раскрывается полностью, когда ты выносишь логику в composables:
import { ref, computed } from 'vue'
export function useCounter(initialValue = 0) { const count = ref(initialValue) const doubled = computed(() => count.value * 2) const isNegative = computed(() => count.value < 0)
const increment = () => count.value++ const decrement = () => count.value-- const reset = () => count.value = initialValue
return { count, doubled, isNegative, increment, decrement, reset }}
// Использование в компонентеimport { useCounter } from '@/composables/useCounter'
const { count, doubled, increment } = useCounter(10)// count.value = 10, doubled.value = 20// composables/useFetch.ts — универсальный хук для HTTP запросовimport { ref, computed } from 'vue'
export function useFetch<T>(url: string) { const data = ref<T | null>(null) const error = ref<Error | null>(null) const loading = ref(false)
const fetch = async () => { loading.value = true error.value = null try { const response = await globalThis.fetch(url) if (!response.ok) throw new Error(response.statusText) data.value = await response.json() } catch (e) { error.value = e as Error } finally { loading.value = false } }
const isEmpty = computed(() => !data.value)
// Автоматически загружаем при монтировании fetch()
return { data, error, loading, isEmpty, refresh: fetch }}
// Использованиеconst { data: users, loading, error } = useFetch<User[]>('/api/users')Реактивность в setup — важные правила ⚠️
Заголовок раздела «Реактивность в setup — важные правила ⚠️»// ✅ Правильно — ref и reactive реактивныconst count = ref(0)const user = reactive({ name: 'Яша', age: 25 })
// ❌ Деструктуризация ломает реактивность reactive!const { name, age } = user // name и age — обычные примитивы, не реактивны!
// ✅ toRefs сохраняет реактивностьimport { toRefs } from 'vue'const { name, age } = toRefs(user) // name.value и age.value — реактивны
// ✅ props тоже нельзя деструктурировать напрямуюconst props = defineProps<{ title: string }>()const { title } = props // ❌ не реактивно!const { title } = toRefs(props) // ✅ реактивноПорядок выполнения setup() 📋
Заголовок раздела «Порядок выполнения setup() 📋»setup() { // 1️⃣ Реактивные данные const count = ref(0)
// 2️⃣ Вычисляемые свойства (зависят от данных) const doubled = computed(() => count.value * 2)
// 3️⃣ Функции и методы const increment = () => count.value++
// 4️⃣ Watchers watch(count, (newVal) => console.log(newVal))
// 5️⃣ Lifecycle hooks onMounted(() => console.log('Компонент монтирован')) onUnmounted(() => console.log('Компонент удалён'))
// 6️⃣ Return (если не <script setup>) return { count, doubled, increment }}Composition API vs Options API — когда что? 📊
Заголовок раздела «Composition API vs Options API — когда что? 📊»| Критерий | Options API | Composition API |
|---|---|---|
| Обучение | Легче для новичков | Требует понимания реактивности |
| Переиспользование | Mixins (конфликты, неявность) | Composables (чисто, явно) |
| TypeScript | Нужны хелперы | Из коробки |
| Большие компоненты | Логика разбросана | Логика по фичам |
| Маленькие компоненты | Отлично | Тоже отлично |
| Рекомендация Vue | Legacy, но поддерживается | ✅ Рекомендуется |
Резюме 🎯
Заголовок раздела «Резюме 🎯»Composition API — это:
<script setup>— синтаксис без boilerplatedefineProps<T>()— типизированные propsdefineEmits<T>()— типизированные событияdefineExpose()— контроль публичного API- Composables — переиспользуемые куски логики
Следующий урок — Options API (для полноты картины)! 📋