04 β Composition API & Composables¶
β¬ οΈ Oldingi: 03 β Komponentlar Β· π README Β· Keyingi: 05 β Vue Router β‘οΈ
Bu modul Vue'ni toza arxitektura bilan yozishning kalitidir. Backend tajribang shu yerda juda asqotadi: composable β bu frontend'dagi "service", logikani UI'dan ajratib, qayta ishlatiladigan qilib o'raydigan birlik.
Options API vs Composition API¶
Eski Vue (2) β Options API: logika data, methods, computed, watch "qutilariga" bo'linadi. Bitta feature (masalan, "qidiruv") kodi 4 ta joyga tarqaladi.
Yangi β Composition API (<script setup>): logikani feature bo'yicha birga ushlaysan va composable'larga ajratasan.
Options API (feature tarqalgan): Composition API (feature jamlangan):
data: { search, results } ββ useSearch() βββ
methods: { doSearch } search, results β hammasi
computed:{ filtered } doSearch β bir joyda,
watch: { search } filtered β qayta ishlatsa bo'ladi
β
Laravel analogiyasi: Options API β "fat controller" (hamma narsa bitta klassda, metodlarga bo'lingan). Composition API β logikani service va action klasslariga ajratish (DDD'dagi kabi). Composable = injektsiya qilinadigan reusable xizmat.
Quyidagi diagramma bitta "qidiruv" feature'i ikki uslubda qanday joylashishini taqqoslaydi (reaktivlik ikkalasida ham bir xil Proxy mexanizmida β farq faqat kodni tashkil qilishda):
4.1 <script setup> β chuqurroq¶
<script setup> ichida:
- Top-level e'lon qilingan har narsa avtomatik template'ga ochiladi.
- defineProps, defineEmits, defineModel, defineExpose β kompilyator makrolari (import shart emas).
- Kod komponent yaratilganda bir marta ishlaydi (setup() tanasi kabi).
<script setup>
import { ref, computed, onMounted } from 'vue'
const count = ref(0)
const double = computed(() => count.value * 2)
onMounted(() => console.log('DOM tayyor'))
// hammasi avtomatik template uchun ochiq
</script>
defineExpose β komponent ichidan tashqariga metod ochish¶
<script setup> default'da hamma narsani yopiq qiladi (ota template ref orqali bola ichiga kira olmaydi). Ataylab ochish kerak bo'lsa:
<!-- Child.vue -->
<script setup>
import { ref } from 'vue'
const isOpen = ref(false)
function open() { isOpen.value = true }
defineExpose({ open }) // ota faqat shularni ko'radi
</script>
<!-- Parent -->
<script setup>
import { ref } from 'vue'
const childRef = ref(null)
</script>
<template>
<Child ref="childRef" />
<button @click="childRef.open()">Bolani och</button>
</template>
4.2 Lifecycle hooks (hayot tsikli)¶
Komponent yaratilishidan yo'q qilinishigacha bo'lgan bosqichlarga "ulanish":
<script setup>
import { onMounted, onUpdated, onUnmounted, onBeforeMount,
onBeforeUnmount, onErrorCaptured } from 'vue'
onBeforeMount(() => {}) // DOM'ga joylashdan oldin
onMounted(() => {
// DOM tayyor β DOM o'lchash, 3rd-party kutubxona init, fetch, addEventListener
})
onUpdated(() => {}) // reaktiv o'zgarish DOM'ga tushgach
onBeforeUnmount(() => {}) // o'chishdan oldin β tozalashga eng yaxshi joy
onUnmounted(() => {
// listener'larni olib tashlash, interval clear, socket yopish
})
onErrorCaptured((err) => {/* bola xatosini tutib olish */ return false })
</script>
Eng ko'p ishlatiladigani β onMounted (DOM/fetch boshlash) va onUnmounted (tozalash).
MUHIM β leak'dan saqlanish:
onMounted(() => {
const id = setInterval(tick, 1000)
window.addEventListener('resize', onResize)
onUnmounted(() => { // har doim juftini tozala
clearInterval(id)
window.removeEventListener('resize', onResize)
})
})
Nuxt SSR'da
onMountedfaqat brauzerda ishlaydi (server'da DOM yo'q). Bu βwindow/documentga murojaatnionMountedichida qilish kerakligining sababi.
Quyidagi vaqt o'qi hooklarning chaqirilish tartibini ko'rsatadi (setup β onMounted β onUpdated β onUnmounted), jumladan SSR/hydration'dagi xulq:
4.3 Composable β qayta ishlatiladigan logika (ENG MUHIM)¶
Composable = reaktiv holat + logikani o'rab, qayta ishlatish uchun use...() funksiyasi. Vue'ning "custom hook"i.
Qoidalar¶
- Nomi
usebilan boshlanadi:useCounter,useFetch,useAuth. - Reaktiv qiymatlar (
ref,computed) va funksiyalarni qaytaradi. - Odatda
composables/papkada (Nuxt'da bu papka auto-import qilinadi).
Eng oddiy misol¶
// composables/useCounter.js
import { ref, computed } from 'vue'
export function useCounter(initial = 0, step = 1) {
const count = ref(initial)
const double = computed(() => count.value * 2)
const inc = () => count.value += step
const dec = () => count.value -= step
const reset = () => count.value = initial
return { count, double, inc, dec, reset } // ref'larni qaytaramiz
}
<script setup>
import { useCounter } from '@/composables/useCounter'
const { count, double, inc, dec, reset } = useCounter(10, 2)
// destructure qilsa ham reaktivlik saqlanadi β chunki ular ref!
</script>
<template>
<p>{{ count }} (x2 = {{ double }})</p>
<button @click="inc">+</button>
<button @click="dec">-</button>
<button @click="reset">Reset</button>
</template>
Eslatma: composable'dan ref qaytarganing uchun destructure reaktivlikni buzmaydi (02-modulda
reactivedestructure muammosini ko'rgansan). Shuning uchun composable'lar odatda ref qaytaradi.
Real composable β useFetch (soddalashtirilgan)¶
// composables/useFetch.js
import { ref } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const loading = ref(false)
async function execute() {
loading.value = true
error.value = null
try {
const res = await fetch(url)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
data.value = await res.json()
} catch (e) {
error.value = e
} finally {
loading.value = false
}
}
execute()
return { data, error, loading, refetch: execute }
}
<script setup>
import { useFetch } from '@/composables/useFetch'
const { data, loading, error, refetch } = useFetch('https://api.example.com/users')
</script>
<template>
<p v-if="loading">Yuklanmoqda...</p>
<p v-else-if="error">Xato: {{ error.message }}</p>
<ul v-else>
<li v-for="u in data" :key="u.id">{{ u.name }}</li>
</ul>
<button @click="refetch">Qayta</button>
</template>
Bu bitta composable'ni 100 ta komponentda ishlatasan. DRY, testlanadigan, toza. (Nuxt'da useFetch allaqachon mavjud β 08-modul.)
Composable ichida lifecycle va cleanup¶
// composables/useMouse.js
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
function update(e) { x.value = e.clientX; y.value = e.clientY }
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
return { x, y }
}
Composable ichida onMounted/onUnmounted ishlatish mumkin β ular composable'ni chaqirgan komponentga "ulanadi". Bu β logikani to'liq kapsulalash: listener qo'shish ham, tozalash ham composable ichida. Komponent faqat const { x, y } = useMouse() deydi.
Quyidagi diagramma bitta composable bir nechta komponentda qanday qayta ishlatilishini ko'rsatadi (har komponent o'z mustaqil reaktiv nusxasini oladi):
Composable'larni birga ishlatish (compose qilish)¶
export function useUserProfile(userId) {
const { data: user, loading } = useFetch(`/api/users/${userId}`)
const { data: posts } = useFetch(`/api/users/${userId}/posts`)
const isLoaded = computed(() => !!user.value && !!posts.value)
return { user, posts, loading, isLoaded }
}
Kichik composable'lardan kattalarini quryapsan β xuddi service'lardan biznes-logika qatlamini qurgandek.
4.4 Composable vs boshqa yondashuvlar¶
| Vosita | Qachon |
|---|---|
| Composable | Reaktiv holat + logika (stateful). State'ni baham ko'rish/qayta ishlatish |
| Util funksiya | Sof, holati yo'q yordamchi (formatDate, slugify) β utils/ |
| Komponent | Vizual UI bo'lagi |
| Pinia store | Butun ilova bo'ylab bitta global holat (auth, cart) |
| provide/inject | Daraxtning ma'lum shoxi uchun lokal kontekst |
Sezgi: Logika UI markup'siz va qayta ishlatilsa β composable. Faqat bitta komponentda ishlatilsa va kichik bo'lsa β komponent ichida qoldir.
4.5 Advanced reactivity (composable yozayotganda kerak bo'ladi)¶
import { shallowRef, triggerRef, customRef, toValue, readonly } from 'vue'
// shallowRef β faqat .value almashishini kuzatadi (ichini emas). Katta obyektlar uchun.
const big = shallowRef({ huge: 'data' })
big.value = { huge: 'new' } // kuzatiladi
big.value.huge = 'x' // kuzatilMAYDI (ichki) β triggerRef(big) kerak
// toValue β ref/getter/oddiy qiymatni "yechadi" (composable arg moslashuvchanligi uchun)
function useX(source) { // source: ref | getter | qiymat β barchasini qabul qiladi
const val = toValue(source)
}
// readonly β o'zgartirib bo'lmaydigan nusxa (store'dan tashqariga immutable berish)
const state = reactive({ count: 0 })
const ro = readonly(state) // ro.count = 1 β warning
toValue β kuchli composable yozishda muhim: foydalanuvchi useX(myRef), useX(() => x) yoki useX(5) bersa ham ishlaydi.
4.6 Composable papka strukturasi (EduCore uchun namuna)¶
composables/
βββ useAuth.ts # login, logout, current user
βββ useApi.ts # asosiy fetch wrapper (token, baseURL)
βββ usePagination.ts # sahifalash logikasi
βββ useDebounce.ts # debounce util-composable
βββ useToggle.ts # boolean toggle
βββ useTenant.ts # multi-tenant: joriy tenant konteksti
βββ useTable.ts # qidiruv+filter+sort birlashgan jadval logikasi
Bu β frontend'dagi "application layer". Komponentlar ingichka (thin) qoladi, logika composable'larda β xuddi controller'lar ingichka, logika service/action'larda bo'lgani kabi.
Xulosa¶
- Composition API logikani feature bo'yicha jamlaydi (Options API tarqatadi)
<script setup>β eng qisqa, zamonaviy uslub;defineExposebilan tanlab ochish- Lifecycle:
onMounted(boshlash),onUnmounted(tozalash) β leak'dan saqlan - Composable =
use...(), ref/computed/fn qaytaradi, qayta ishlatiladigan reaktiv logika (frontend "service") - Composable ichida lifecycle + cleanup β to'liq kapsulalash
toValue,shallowRef,readonlyβ kuchli composable vositalari- Toza arxitektura: ingichka komponent + boy composable
π― Masalalar (kamida 22 ta)¶
Lifecycle (1β5)¶
onMounteddaconsole.logqil,onUnmountedda yana βv-ifbilan komponentni o'chirib/yoqib ketma-ketlikni kuzat.- Soat (β
):
onMounteddasetIntervalbilan har soniya vaqtni yangila;onUnmounteddaclearInterval. Tozalashni unutsang nima bo'lishini izohla. - Window resize (β
): Oyna kengligini ekranda ko'rsat (
resizelistener + cleanup). - Scroll position (β ): Sahifa scroll qiymatini kuzatib ko'rsat; tozala.
onErrorCaptured(β β ): Ataylab xato tashlaydigan bola yarat, ota uni tutib "Xatolik yuz berdi" ko'rsatsin.
Asosiy composable'lar (6β13)¶
useToggle(initial=false)β{ value, toggle, setTrue, setFalse }. Modal/sidebar'da ishlat.useCounter(initial, step)β{ count, inc, dec, reset, double }(yuqoridagini o'zing qayta yoz).useLocalStorage(key, default)(β ):localStoragebilan sinxron reaktiv ref (watchichida saqla, init'da o'qi).useDebounce(value, delay)(β ): ref'ni debounce qilingan reaktiv qiymatga aylantir.useMouse()β{ x, y }(listener + cleanup composable ichida).useWindowSize()(β ) β{ width, height }, reaktiv.useClipboard()(β ):{ copy, copied }β matn nusxalash, 2s "copied" holati.useInterval(callback, ms)(β ): start/stop boshqaruvi bilan.
Data composable'lari (14β18)¶
useFetch(url)(β ): yuqoridagini qayta yoz;loading/error/data/refetchbilan.usePagination(items, perPage)(β β ):currentPage,totalPages,paginatedItems,next/prev/goTo.useSearch(items, keys)(β β ): qidiruv matni bo'yichakeyslar ichidan filtrlangan ro'yxat qaytarsin.useSort(items)(β β ): ustun bo'yichaasc/descsaralash, holat boshqaruvi bilan.useTable(β β β ): 15+16+17 ni birlashtir β bitta composable qidiruv+filter+sort+pagination beradigan. EduCore jadvallari uchun asos.
Arxitektura va kompozitsiya (19β24)¶
- Compose qilish (β
β
):
useUserProfile(id)niuseFetchustiga qur (user + posts birga). - toValue (β
β
):
useDoubled(source)yoz βsourceref, getter yoki oddiy son bo'lsa ham ishlasin (toValue). - defineExpose (β
β
):
VideoPlayerkomponentiplay()/pause()metodlarinidefineExposeqilsin; ota tugma orqali boshqarsin. - Composable + komponent (β
β
):
useFetch+DataList(03-modul scoped slot) ni birlashtirib, har qanday API ro'yxatini ko'rsatadigan reusable blok yarat. - Refactor (β
β
β
): Quyidagi "fat" komponentni composable(lar)ga ajrat:
> Bitta komponentda: qidiruv inputi + API'dan ro'yxat olish + filterlash + pagination + localStorage'ga oxirgi qidiruvni saqlash β hammasi aralashgan. Buni
useSearch,useFetch,usePagination,useLocalStoragega bo'lib, komponentni ingichka qil. - EduCore
useAuthskeleti (β β β ):user(ref),isAuthenticated(computed),login(creds),logout(),fetchUser()β token'niuseLocalStoragebilan sinxronla (hozircha API'ni soxta qil).
β Tanlangan yechimlar¶
6 β useToggle
// composables/useToggle.js
import { ref } from 'vue'
export function useToggle(initial = false) {
const value = ref(initial)
const toggle = () => value.value = !value.value
const setTrue = () => value.value = true
const setFalse = () => value.value = false
return { value, toggle, setTrue, setFalse }
}
8 β useLocalStorage
// composables/useLocalStorage.js
import { ref, watch } from 'vue'
export function useLocalStorage(key, defaultValue) {
const stored = localStorage.getItem(key)
const data = ref(stored ? JSON.parse(stored) : defaultValue)
watch(data, (val) => {
localStorage.setItem(key, JSON.stringify(val))
}, { deep: true })
return data
}
// Ishlatish: const theme = useLocalStorage('theme', 'dark')
15 β usePagination
// composables/usePagination.js
import { ref, computed, toValue } from 'vue'
export function usePagination(items, perPage = 10) {
const currentPage = ref(1)
const list = computed(() => toValue(items)) // ref yoki oddiy massiv bo'lsa ham
const totalPages = computed(() =>
Math.max(1, Math.ceil(list.value.length / perPage))
)
const paginatedItems = computed(() => {
const start = (currentPage.value - 1) * perPage
return list.value.slice(start, start + perPage)
})
const next = () => { if (currentPage.value < totalPages.value) currentPage.value++ }
const prev = () => { if (currentPage.value > 1) currentPage.value-- }
const goTo = (p) => { currentPage.value = Math.min(Math.max(1, p), totalPages.value) }
return { currentPage, totalPages, paginatedItems, next, prev, goTo }
}
18 β useTable (qisqartirilgan tuzilma)
// composables/useTable.js
import { ref, computed } from 'vue'
export function useTable(source, { searchKeys = [], perPage = 10 } = {}) {
const search = ref('')
const sortKey = ref(null)
const sortDir = ref('asc')
const page = ref(1)
const searched = computed(() => {
if (!search.value) return source.value
const q = search.value.toLowerCase()
return source.value.filter(row =>
searchKeys.some(k => String(row[k]).toLowerCase().includes(q))
)
})
const sorted = computed(() => {
if (!sortKey.value) return searched.value
return [...searched.value].sort((a, b) => {
const r = a[sortKey.value] > b[sortKey.value] ? 1 : -1
return sortDir.value === 'asc' ? r : -r
})
})
const totalPages = computed(() => Math.max(1, Math.ceil(sorted.value.length / perPage)))
const rows = computed(() => {
const start = (page.value - 1) * perPage
return sorted.value.slice(start, start + perPage)
})
function setSort(key) {
if (sortKey.value === key) sortDir.value = sortDir.value === 'asc' ? 'desc' : 'asc'
else { sortKey.value = key; sortDir.value = 'asc' }
page.value = 1
}
return { search, sortKey, sortDir, page, rows, totalPages, setSort }
}
β‘οΈ Keyingi: 05 β Vue Router