Kodėl verta pereiti prie Composition API
Kai Vue 3 pasirodė su Composition API, daugelis kūrėjų iš pradžių žiūrėjo skeptiškai. Dar vienas būdas rašyti tą patį kodą? Bet pamažu pradedi suprasti, kad tai ne tik alternatyva Options API – tai visiškai kitoks mąstymo būdas apie komponentų logiką.
Dirbant su didesniu projektu, Options API pradeda parodyti savo silpnąsias vietas. Logika išsibarstyta po skirtingas sekcijas – data, methods, computed, watch. Norint suprasti vieną funkciją, reikia šokinėti po visą failą. O kai pridedi TypeScript, Options API tampa dar keblesnis – tipo išvedimas ne visada veikia taip, kaip tikėjaisi.
Composition API leidžia grupuoti susijusią logiką kartu. Visos funkcijos, susijusios su naudotojo autentifikacija, gali būti vienoje vietoje. Viskas, kas susiję su duomenų įkėlimu iš API – kitoje. Tai ne tik skaitomesnė, bet ir lengviau testuojama bei pakartotinai naudojama.
TypeScript integracija nuo pirmos eilutės
Vienas didžiausių Composition API privalumų – puikus TypeScript palaikymas. Nebereikia kovoti su this kontekstu ar rašyti sudėtingų tipo anotacijų, kaip Options API.
Štai paprastas pavyzdys, kaip atrodo komponentas su TypeScript:
import { ref, computed, defineComponent } from 'vue'
interface User {
id: number
name: string
email: string
}
export default defineComponent({
setup() {
const user = ref(null)
const isLoading = ref(false)
const error = ref('')
const userName = computed(() => {
return user.value?.name ?? 'Svečias'
})
const fetchUser = async (userId: number) => {
isLoading.value = true
try {
const response = await fetch(`/api/users/${userId}`)
user.value = await response.json()
} catch (e) {
error.value = 'Nepavyko įkelti duomenų'
} finally {
isLoading.value = false
}
}
return {
user,
isLoading,
error,
userName,
fetchUser
}
}
})
TypeScript iš karto supranta, kad user.value gali būti null, todėl priversčia naudoti optional chaining. Jokių netikėtų klaidų runtime metu. IDE rodo visas galimas savybes ir metodus – autocomplete veikia puikiai.
Reactivity sistema ir jos paslaptys
Vue 3 reactivity sistema paremta JavaScript Proxy, kas leidžia sekti pakeitimus daug efektyviau nei Vue 2. Bet reikia suprasti skirtumą tarp ref ir reactive, nes tai dažna klaidų priežastis.
ref naudojamas primityvams ir atskiroms reikšmėms. Jis sukuria objektą su value savybe. Template’uose Vue automatiškai „išpakuoja” ref, bet script’e visada reikia naudoti .value:
const count = ref(0)
count.value++ // script'e
// {{ count }} template'e
reactive skirtas objektams ir masyvams. Jis grąžina reaktyvų proxy, kuris stebi visus objekto pakeitimus:
const state = reactive({
user: null,
posts: [],
settings: {
theme: 'dark'
}
})
state.user = newUser // veikia
state.posts.push(newPost) // veikia
state.settings.theme = 'light' // veikia
Bet yra gudrybė – jei destructure’ini reactive objektą, prarandamas reaktyvumas:
const { user, posts } = state // BLOGAI! Prarandamas reaktyvumas
Tam reikia naudoti toRefs:
const { user, posts } = toRefs(state) // Gerai!
Asmeniškai dažniausiai naudoju ref visam – taip paprasčiau ir nuosekliau. Taip, reikia rašyti .value, bet bent jau niekada nepamiršti, ar kintamasis reaktyvus, ar ne.
Composables – logiką į atskirus modulius
Čia Composition API tikrai spindi. Galima iškelti bet kokią logiką į atskirą funkciją (composable) ir naudoti keliuose komponentuose. Tai kaip mixins, tik be visų jų problemų.
Pavyzdžiui, sukurkime useApi composable, kurį galėsime naudoti visur:
// composables/useApi.ts
import { ref } from 'vue'
export function useApi() {
const data = ref(null)
const loading = ref(false)
const error = ref(null)
const execute = async (url: string, options?: RequestInit) => {
loading.value = true
error.value = null
try {
const response = await fetch(url, options)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
data.value = await response.json()
} catch (e) {
error.value = e as Error
} finally {
loading.value = false
}
}
return {
data,
loading,
error,
execute
}
}
Dabar bet kuriame komponente:
import { useApi } from '@/composables/useApi'
interface Post {
id: number
title: string
body: string
}
export default defineComponent({
setup() {
const { data: posts, loading, error, execute } = useApi()
onMounted(() => {
execute('/api/posts')
})
return { posts, loading, error }
}
})
Kiekvienas useApi iškvietimas sukuria naują nepriklausomą būseną. Galite turėti kelis API kvietimus viename komponente, ir jie netrukdys vienas kitam.
Script setup sintaksė – mažiau boilerplate
Vue 3.2 pristatė <script setup> sintaksę, kuri dar labiau supaprastina kodą. Nebereikia defineComponent, setup() funkcijos ar return statement’o:
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
interface User {
id: number
name: string
}
const user = ref<User | null>(null)
const isLoading = ref(false)
const userName = computed(() => user.value?.name ?? 'Svečias')
const loadUser = async () => {
isLoading.value = true
// ... fetch logic
isLoading.value = false
}
onMounted(() => {
loadUser()
})
</script>
<template>
<div v-if="isLoading">Kraunama...</div>
<div v-else>Sveiki, {{ userName }}!</div>
</template>
Viskas, kas deklaruojama <script setup> bloke, automatiškai prieinama template’e. Props ir emits apibrėžiami su compiler macro:
<script setup lang="ts">
interface Props {
userId: number
showEmail?: boolean
}
interface Emits {
(e: 'update', userId: number): void
(e: 'delete'): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const handleUpdate = () => {
emit('update', props.userId)
}
</script>
TypeScript tipo patikrinimas veikia puikiai, ir nereikia rašyti runtime validacijos (nors galite, jei reikia).
Lifecycle hooks ir watch funkcionalumas
Composition API lifecycle hooks’ai turi kitokius pavadinimus ir veikia šiek tiek kitaip. Visi jie kviečiami setup() funkcijoje (arba <script setup> bloke):
import { onMounted, onUpdated, onUnmounted, onBeforeMount } from 'vue'
onBeforeMount(() => {
console.log('Komponentas netrukus bus prijungtas')
})
onMounted(() => {
console.log('Komponentas prijungtas prie DOM')
// Čia galima daryti API kvietimus, inicializuoti bibliotekos
})
onUpdated(() => {
console.log('Komponentas atnaujintas')
})
onUnmounted(() => {
console.log('Komponentas bus pašalintas')
// Išvalyti event listener'ius, intervalus ir pan.
})
watch ir watchEffect leidžia reaguoti į reaktyvių reikšmių pasikeitimus. watch reikia nurodyti, ką stebėti:
const searchQuery = ref('')
const results = ref([])
watch(searchQuery, async (newQuery, oldQuery) => {
if (newQuery.length > 2) {
results.value = await searchApi(newQuery)
}
})
watchEffect automatiškai stebi visas reaktyvias reikšmes, kurias naudoja:
watchEffect(() => {
// Automatiškai stebi searchQuery ir filters
console.log(`Ieškoma: ${searchQuery.value}, filtrai: ${filters.value}`)
})
Praktiškai watch naudoju dažniau, nes turiu daugiau kontrolės – galiu gauti senąją reikšmę, kontroliuoti, kada tiksliai vykdyti callback’ą, ir pan.
Realus projekto pavyzdys su geriausiomis praktikomis
Pažiūrėkime, kaip visa tai atrodo realiame projekte. Sukursime vartotojų sąrašo komponentą su paieška, filtravimui ir puslapiavimui:
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { useApi } from '@/composables/useApi'
import { usePagination } from '@/composables/usePagination'
import { useDebounce } from '@/composables/useDebounce'
interface User {
id: number
name: string
email: string
role: 'admin' | 'user'
}
interface UsersResponse {
users: User[]
total: number
}
const searchQuery = ref('')
const roleFilter = ref('all')
const { data, loading, error, execute } = useApi<UsersResponse>()
const { currentPage, pageSize, totalPages, setTotal } = usePagination(20)
// Debounce paieškos užklausai
const debouncedSearch = useDebounce(searchQuery, 500)
// Computed filtruotiems vartotojams
const filteredUsers = computed(() => {
if (!data.value) return []
let users = data.value.users
if (roleFilter.value !== 'all') {
users = users.filter(u => u.role === roleFilter.value)
}
return users
})
// Įkelti vartotojus
const loadUsers = async () => {
const params = new URLSearchParams({
page: currentPage.value.toString(),
limit: pageSize.value.toString(),
search: debouncedSearch.value
})
await execute(`/api/users?${params}`)
if (data.value) {
setTotal(data.value.total)
}
}
// Stebėti paieškos ir puslapio pasikeitimus
watch([debouncedSearch, currentPage], () => {
loadUsers()
})
// Pirminis įkėlimas
loadUsers()
</script>
<template>
<div class="users-list">
<div class="filters">
<input
v-model="searchQuery"
type="text"
placeholder="Ieškoti vartotojų..."
/>
<select v-model="roleFilter">
<option value="all">Visi</option>
<option value="admin">Administratoriai</option>
<option value="user">Vartotojai</option>
</select>
</div>
<div v-if="loading" class="loading">Kraunama...</div>
<div v-else-if="error" class="error">{{ error.message }}</div>
<div v-else class="users">
<div
v-for="user in filteredUsers"
:key="user.id"
class="user-card"
>
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
<span class="role">{{ user.role }}</span>
</div>
</div>
<div class="pagination">
<button
@click="currentPage--"
:disabled="currentPage === 1"
>
Atgal
</button>
<span>{{ currentPage }} / {{ totalPages }}</span>
<button
@click="currentPage++"
:disabled="currentPage === totalPages"
>
Pirmyn
</button>
</div>
</div>
</template>
Šis pavyzdys rodo kelias svarbias praktikas:
– Logika išskaidyta į composables (useApi, usePagination, useDebounce)
– TypeScript tipai apibrėžti visiems duomenims
– Debouncing paieškos užklausoms, kad nesiunčiame per daug request’ų
– Reaktyvus filtravimas su computed savybe
– Automatinis duomenų perkrovimas su watch
Ką daryti su senais Options API komponentais
Gera žinia – nereikia perrašyti viso projekto iš karto. Vue 3 palaiko abu API stilius tame pačiame projekte. Galite pamažu migruoti komponentus, pradedant nuo naujų ar dažniausiai keičiamų.
Jei turite didelį Options API komponentą, kurį norite perkelti, rekomenduoju tokią strategiją:
1. Pradėkite nuo paprasčiausių dalių – data ir methods
2. Iškelkite pakartotinai naudojamą logiką į composables
3. Perkelkite lifecycle hooks
4. Galiausiai – computed ir watch
Nebandykite viską padaryti vienu metu. Geriau turėti veikiantį hibridinį komponentą nei pusiau perrašytą ir neveikiantį.
Kai kurie dalykai Options API vis dar patogesni – pavyzdžiui, jei turite labai paprastą komponentą su keliomis eilutėmis kodo, Options API gali būti aiškesnis. Nėra vieno teisingo atsakymo visoms situacijoms.
Kas laukia ateityje ir kaip tobulėti
Vue 3 su Composition API ir TypeScript – tai ne tik naujas sintaksės būdas. Tai kitoks mąstymas apie komponentų architektūrą. Logikos pakartotinis naudojimas tampa natūralus, o ne priverstas per mixins ar higher-order components.
Jei tik pradedate, nesijaudinkite dėl visų detalių iš karto. Pradėkite nuo paprastų komponentų su <script setup>. Naudokite ref viskam. Pamažu pradėsite jausti, kada verta iškelti logiką į composable, kada naudoti reactive vietoj ref.
TypeScript integracija iš pradžių gali atrodyti kaip papildomas sudėtingumas, bet po kelių savaičių suprasite, kaip daug laiko sutaupote, kai IDE parodo klaidas prieš paleidžiant kodą. Autocomplete tampa tikrai protingas, o refactoring’as – saugus.
Svarbiausia – eksperimentuokite. Sukurkite testinį projektą, pabandykite skirtingus pattern’us. Paskaitykite kitų kūrėjų composables bibliotekų kodą (VueUse yra puikus pavyzdys). Matydami, kaip kiti sprendžia problemas, išmoksite daug daugiau nei iš dokumentacijos.
Composition API nėra tobulas, bet tai didžiulis žingsnis į priekį. O su TypeScript – tai viena geriausių kombinacijų šiuolaikiniam frontend kūrimui.
