Vue.js: Composables المخصصة
آخر تحديث: 2026-08-26
تُعد الدوال القابلة للتركيب (المعروفة أيضًا باسم «الخطافات») نمط إعادة الاستخدام الأساسي في واجهة برمجة تطبيقات التركيب (Composition API) في Vue 3 — فهي تُغلف «البيانات التفاعلية + منطق الأعمال» في دوال قابلة لإعادة الاستخدام. وبشكل أساسي، فهي «حزم منطقية» تتألف من ref وcomputed وwatch، وهكذا دواليك.
يُعد إتقان استخدام «Composables» أمرًا أساسيًّا لكتابة تطبيقات Vue قابلة للصيانة — فهي تجعل كود المكونات أكثر إيجازًا وتسمح بمشاركة المنطق التشغيلي بين المكونات. سيساعدك هذا الدرس على إنشاء مكتبة «Composables» الخاصة بك من الصفر.
1. ما ستتعلمه
- ما المقصود بـ«Composable» (hook)، ولماذا نحتاج إليه؟
- قواعد تسمية قابلة للتركيب
useXxx - 5 مكونات قابلة للتركيب عملية (useMouse / useLocalStorage / useFetch / useDebounce / useToggle)
- أفضل الممارسات لاستلام المعلمات وإرجاع القيم
- تغليف نقاط ربط دورة الحياة
- مكونات قابلة للتركيب مشتركة بين المكونات (مجلد composables/)
- مكتبة VueUse (أكثر من 30 عنصرًا قابلًا للتركيب جاهزًا للاستخدام)
2. تم تكرار منطق «Get + debounce» الخاص بعربة التسوق 5 مرات
(1) المشكلة: 5 حقول بحث، و5 حالات تكرار في الكود
كان لدى نظام إدارة أليس 5 حقول بحث (الطلبات/المنتجات/المستخدمون/إلخ)، كل منها مزود بمنطق «الجلب» و«إزالة التداخل»:
// ❌ The "Broken" Version:5 component,5 Duplicate code
// OrdersSearch.vue
let timer = null
const searchQuery = ref('')
const results = ref([])
watch(searchQuery, (val) => {
if (timer) clearTimeout(timer)
timer = setTimeout(async () => {
const res = await fetch(`/api/orders?q=${val}`)
results.value = await res.json()
}, 300)
})
// ProductsSearch.vue - Exactly the same logic(Change only URL)
// UsersSearch.vue - Exactly the same logic(Change only URL)
// ... And also 2 more ...
المجموع: 50 سطراً × 5 = 250 سطراً من الكود المكرر. يتطلب إصلاح الخلل إجراء تغييرات في 5 مواضع.
يضيف مدير المنتج تشارلي متطلبًا جديدًا:
"أليس، غيّري مدة الانتظار بين الضغطات من 300 مللي ثانية إلى 500 مللي ثانية. وأضيفي فحصًا للحد الأدنى للطول (لا تجري البحث إلا إذا كان عدد الأحرف 3 أو أكثر)."
يتعين على أليس تعديل 5 ملفات. عملية عرضة للأخطاء.
(2) حل Vue Composable: 1 useSearch، أعيد استخدامه في 5 أماكن
// composables/useSearch.js - 1 Composable
import { ref, watch } from 'vue'
export function useSearch(apiUrl, options = {}) {
const { debounceMs = 300, minLength = 0 } = options
const searchQuery = ref('')
const results = ref([])
const loading = ref(false)
let timer = nulo
watch(searchQuery, (val) => {
if (timer) clearTimeout(timer)
if (val.length < minLength) {
results.value = []
return
}
timer = setTimeout(async () => {
loading.value = true
const res = await fetch(`${apiUrl}?q=${val}`)
results.value = await res.json()
loading.value = false
}, debounceMs)
})
return { searchQuery, results, loading }
}
<!-- OrdersSearch.vue - 1 Line call -->
<script setup>
import { useSearch } from '@/composables/useSearch'
const { searchQuery, results, loading } = useSearch('/api/orders', { debounceMs: 500, minLength: 3 })
</script>
<!-- ProductsSearch.vue - Likewise 1 line (Different URL) -->
<script setup>
import { useSearch } from '@/composables/useSearch'
const { searchQuery, results, loading } = useSearch('/api/products', { debounceMs: 500, minLength: 3 })
</script>
استخدام دالة useSearch مرة واحدة → أعيد استخدامها في 5 أماكن. يؤدي تغيير إعدادات «debounce» في مكان واحد إلى تطبيق التغيير في كل الأماكن.
(3) الإيرادات
بعد «Composable»:
- حجم الكود: 250 → 60 سطراً (-76%)
- تغيير واحد مطلوب للتنفيذ: ملف Composable واحد
- مربع بحث جديد: استدعاء من سطر واحد
- قابل للاختبار: يمكن إجراء اختبار وحدة لـ useSearch بشكل مستقل
3. أساسيات «Composable»
(1) قواعد تسمية الملفات
// ✅ Correct:useXxx Naming
useMouse()
useLocalStorage('key')
useFetch('/api/users')
useDebounce(searchQuery, 500)
useToggle(false)
// ❌ Error:Other Names
fetchUser() // Not based on use Introduction
mouseTracker() // Not based on use Introduction
getStorage() // Verb get/set Not included hook
(2) 5 خصائص رئيسية
| الميزة | الوصف |
|---|---|
| يبدأ بـ "use" | معيار صناعي (يستخدم React أيضًا "useXxx") |
| إرجاع البيانات التفاعلية | إرجاع ref / computed / reactive |
| المعلمات المقبولة | تقبل عادةً الأنواع الأولية (سلسلة/رقم/كائن) |
| يمكن استخدامه بشكل مستقل | ما عليك سوى إدخال useXxx() داخل المكون |
| عنصر قابل للتركيب | يمكن تداخل عدة عناصر قابلة للتركيب |
(3) تنظيم الملفات
src/
├-- components/ # Components
├-- views/ # Page
├-- composables/ # Composable Function(Key Points)
│ ├-- useMouse.js
│ ├-- useLocalStorage.js
│ ├-- useFetch.js
│ ├-- useDebounce.js
│ ├-- useToggle.js
│ └-- index.js # Batch Export
├-- stores/ # Pinia stores
└-- App.vue
4. 5 أمثلة عملية على «composable»
(1) useMouse: تتبع موضع الماوس
// composables/useMouse.js
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
function update(evento) {
x.value = evento.clientX
y.value = evento.clientY
}
onMounted(() => {
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
window.removeEventListener('mousemove', update)
})
return { x, y }
}
<!-- MouseTracker.vue -->
<script setup>
import { useMouse } from '@/composables/useMouse'
const { x, y } = useMouse()
</script>
<template>
<p>Mouse: {{ x }}, {{ y }}</p>
</template>
(2) useLocalStorage: localStorage التفاعلي
// composables/useLocalStorage.js
import { ref, watch } from 'vue'
export function useLocalStorage(key, defaultValue) {
const stored = localStorage.getItem(key)
const data = ref(stored !== null ? JSON.parse(stored) : defaultValue)
watch(data, (val) => {
localStorage.setItem(key, JSON.stringify(val))
}, { deep: true })
return data
}
<!-- ThemeToggle.vue -->
<script setup>
import { useLocalStorage } from '@/composables/useLocalStorage'
const theme = useLocalStorage('theme', 'light')
</script>
<template>
<button @click="theme = theme === 'light' ? 'dark' : 'light'">
Current: {{ theme }}
</button>
</template>
(3) useFetch: استرجاع البيانات للأغراض العامة
// composables/useFetch.js
import { ref } from 'vue'
export function useFetch(url) {
const data = ref(nulo)
const loading = ref(false)
const erro = ref(nulo)
async function fetchData() {
loading.value = true
erro.value = nulo
try {
const res = await fetch(url.value || url)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
data.value = await res.json()
} catch (err) {
erro.value = err.message
} finally {
loading.value = false
}
}
// Get It Now
fetchData()
return { data, loading, erro, refetch: fetchData }
}
<!-- UserList.vue -->
<script setup>
import { useFetch } from '@/composables/useFetch'
const { data: users, loading, error, refetch } = useFetch('/api/users')
</script>
<template>
<div v-if="loading">Loading...</div>
<div v-else-if="error">Error: {{ error }}</div>
<ul v-else>
<li v-for="user in users" :key="user.id">{{ user.name }}</li>
</ul>
<button @click="refetch">Refresh</button>
</template>
(4) useDebounce: قيمة إزالة الارتداد
// composables/useDebounce.js
import { ref, customRef } from 'vue'
export function useDebounce(value, delay = 300) {
let timer = null
return customRef((track, trigger) => ({
get() {
track()
return value.value
},
set(newValue) {
clearTimeout(timer)
timer = setTimeout(() => {
value.value = newValue
trigger()
}, delay)
}
}))
}
// Usage
const searchInput = ref('')
const debouncedSearch = useDebounce(searchInput, 500)
watch(debouncedSearch, (val) => {
console.log('Search:', val)
// 500ms Execute later
})
(5) useToggle: مفتاح التبديل
// composables/useToggle.js
import { ref } from 'vue'
export function useToggle(initialValue = false) {
const value = ref(initialValue)
function toggle() {
value.value = !value.value
}
function setTrue() {
value.value = true
}
function setFalse() {
value.value = false
}
return { value, toggle, setTrue, setFalse }
}
<!-- ModalToggle.vue -->
<script setup>
import { useToggle } from '@/composables/useToggle'
const { value: showModal, open: setTrue, close: setFalse } = useToggle(false)
</script>
<template>
<button @click="setTrue">Open Modal</button>
<Modal v-if="showModal" @close="setFalse" />
</template>
5. أفضل الممارسات في مجال «Composable»
(1) تصميم المعلمات الستة الرئيسية
// 1. Basic Parameters
useDebounce(value, 300)
// 2. Configuration Object
useFetch(url, { method: 'POST', body: data })
// 3. Citation Types(Responsive)
useFetch(ref('/api/users'))
// 4. Function Arguments(callback)
useEventListener('click', (e) => console.log(e))
// 5. Multi-parameter
useLocalStorage('key', defaultValue, { mergeDefaults: true })
// 6. Generics(TypeScript)
useLocalStorage<User>('user', { name: '', age: 0 })
(2) 5 اعتبارات أساسية لتصميم قيمة العائد
// 1. Return directly ref
export function useCounter() {
const count = ref(0)
return { count }
}
// 2. Back ref + Methods
export function useCounter() {
const count = ref(0)
const increment = () => count.value++
return { count, increment }
}
// 3. Return to Namespace(Recommendations)
export function useCounter() {
const count = ref(0)
return {
state: { count },
actions: { increment: () => count.value++ }
}
}
// 4. Back ref(Deconstructing Friendship)
export function useCounter() {
return useRef(0) // Return directly ref,For external use .value
}
// 5. Back readonly(Prevent External Modifications)
import { readonly } from 'vue'
export function useCounter() {
const count = ref(0)
return { count: readonly(count) }
}
(3) ستة أنواع رئيسية من تغليف دورة الحياة
// 1. onMounted + onUnmounted(Most common)
export function useEventListener(event, handler) {
onMounted(() => window.addEventListener(event, handler))
onUnmounted(() => window.removeEventListener(event, handler))
}
// 2. watch Automatic Cleanup
export function useWatch(source, callback) {
const stop = watch(source, callback)
onUnmounted(() => stop())
}
// 3. setInterval Automatic Cleanup
export function useInterval(fn, delay) {
let timer = null
onMounted(() => { timer = setInterval(fn, delay) })
onUnmounted(() => clearInterval(timer))
}
// 4. setTimeout Automatic Cleanup
export function useTimeout(fn, delay) {
let timer = null
onMounted(() => { timer = setTimeout(fn, delay) })
onUnmounted(() => clearTimeout(timer))
}
// 5. Canceling Asynchronous Tasks
export function useAsyncTask(task) {
let cancelled = false
onUnmounted(() => { cancelled = true })
return async () => {
if (cancelled) return
await task()
}
}
// 6. Route Redirect Cleanup
export function useRouteLeave(callback) {
onBeforeRouteLeave((to, from) => {
if (callback()) return false // Prevent Departure
})
}
6. مكتبة VueUse (أكثر من 30 عنصرًا قابلًا للتركيب)
(1) ما هو VueUse؟
VueUse هي مكتبة «Composable» من مجتمع Vue توفر أكثر من 200 عنصر «Composable» جاهز للاستخدام (مثل useMouse وuseLocalStorage وuseDebounce وuseEventListener)، مما يوفر عليك عناء كتابتها بنفسك.
# Installation
npm install @vueuse/core
// main.js
import { createApp } from 'vue'
import VueUse from '@vueuse/core'
import App from './App.vue'
createApp(App).use(VueUse).mount('#app')
(2) أكثر 5 مكونات VueUse شيوعًا
import { useMouse, useLocalStorage, useDebounceFn, useEventListener, useToggle } from '@vueuse/core'
// 1. Mouse Position
const { x, y } = useMouse()
// 2. localStorage Responsive
const theme = useLocalStorage('theme', 'light')
// 3. Function Debouncing
const debouncedFn = useDebounceFn(() => {
console.log('Debounced!')
}, 500)
// 4. Global Event Listeners
useEventListener('resize', () => {
console.log('Window resized')
})
// 5. Switch Toggle
const [value, toggle] = useToggle()
(3) 5 حالات استخدام رئيسية
| السيناريو | استخدام Vue Composable |
|---|---|
| موضع الماوس | useMouse / useMouseInElement |
| موضع التمرير | useScroll / useInfiniteScroll |
| localStorage | useLocalStorage / useStorage |
| status الشبكة | useOnline / useNetwork |
| استعلامات الوسائط | useMediaQuery / useBreakpoints |
| وضع ملء الشاشة | useFullscreen |
| الحافظة | useClipboard |
| سحب الماوس | useDraggable |
| حجم العنصر | useElementSize / useResizeObserver |
| إزالة الارتداد / التحكم في السرعة | useDebounceFn / useThrottleFn |
7. مثال كامل: تجميع مكون من 5 عناصر قابلة للتركيب
▶ مثال: 1. التنفيذ الكامل لـ useMouse
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
function update(event) {
x.value = event.clientX
y.value = event.clientY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
return { x, y }
}
▶ مثال: 2. التنفيذ الكامل لـ useFetch
import { ref, watch, isRef } from 'vue'
export function useFetch(url, options = {}) {
const data = ref(null)
const loading = ref(false)
const error = ref(null)
async function fetchData() {
const requestUrl = isRef(url) ? url.value : url
if (!requestUrl) return
loading.value = true
error.value = null
try {
const res = await fetch(requestUrl, options)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
data.value = await res.json()
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
watch(url, fetchData, { immediate: true })
return { data, loading, error, refetch: fetchData }
}
▶ مثال: 3. التنفيذ الكامل لـ useLocalStorage
import { ref, watch } from 'vue'
export function useLocalStorage(key, defaultValue) {
const stored = localStorage.getItem(key)
const data = ref(stored !== null ? JSON.parse(stored) : defaultValue)
watch(data, (val) => {
localStorage.setItem(key, JSON.stringify(val))
}, { deep: true })
// Cross-Tab Synchronization
window.addEventListener('storage', (e) => {
if (e.key === key && e.newValue) {
data.value = JSON.parse(e.newValue)
}
})
return data
}
مثال: 4. useDebounce + useThrottle
import { ref, customRef } from 'vue'
// Image Stabilization:After the last operation delay Trigger
export function useDebounce(value, delay = 300) {
let timer = null
return customRef((track, trigger) => ({
get() {
track()
return value.value
},
set(newValue) {
clearTimeout(timer)
timer = setTimeout(() => {
value.value = newValue
trigger()
}, delay)
}
}))
}
// Cost-cutting: Max 1 trigger during delay period
export function useThrottle(value, delay = 300) {
let last = 0
return customRef((track, trigger) => ({
get() {
track()
return value.value
},
set(newValue) {
const now = Date.now()
if (now - last >= delay) {
last = now
value.value = newValue
trigger()
}
}
}))
}
▶ مثال: 5. مرجع سريع لـ 5 أخطاء شائعة
| الخطأ | الأعراض | الحل |
|---|---|---|
| لا تبدأ بكلمة "use" | غير معترف بها من قبل الفريق | اسمها "useXxx" |
| إرجاع كائن عادي | فقدان التفاعلية | إرجاع مرجع |
| الآثار الجانبية لعدم إجراء عملية التنظيف | تسربات الذاكرة | إجراء عملية التنظيف في onUnmounted |
| الإفراط في التجريد | استخدم «Composables» في السيناريوهات البسيطة | ضع المنطق البسيط مباشرةً في المكونات |
| مكون قابل للتركيب متداخل من 5 مستويات | يصعب تصحيحه | قسّمه إلى مكونات فرعية أو استخدم Pinia |
▶ مثال: 6. مقارنة أداء أفضل 5 مكونات قابلة للتركيب
| النمط | قابلية إعادة الاستخدام | الأداء | سهولة الصيانة | قابلية التطبيق |
|---|---|---|---|---|
| نسخ ولصق | ❌ | ⭐⭐⭐ | ❌ | لمرة واحدة |
| Mixin (Vue 2) | ⭐⭐ | ⭐⭐ | ⭐⭐ | مشروع قديم |
| قابل للتركيب | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | موصى به |
| Pinia | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | تطبيق واسع النطاق |
| Event Bus | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ | التواصل بين المكونات |
❓ أسئلة شائعة
setup؟<script setup>.useSearch على useFetch + useDebounce.const { data } = useFetch('/api')، وقم بإجراء فحص صحة على data.value. استخدم Vitest لإجراء اختبار الوحدة.📖 ملخص
- «Composable» (الـ«hook») هي دالة قابلة لإعادة الاستخدام تتألف من «ref» و«computed» و«watch».
- 5 ميزات رئيسية: تبدأ بكلمة "use" / تُرجع استجابة تفاعلية / تقبل المعلمات / يمكن استخدامها بشكل مستقل / قابلة للتركيب
- 5 أمثلة عملية: useMouse / useLocalStorage / useFetch / useDebounce / useToggle
- 6 تصميمات للمعلمات الرئيسية + 5 تصميمات لقيم الإرجاع الرئيسية
- 6 نقاط ربط رئيسية لدورة الحياة (onMounted/onUnmounted)
- توفر مكتبة VueUse أكثر من 200 عنصر قابل للتركيب
- Composable مقابل Pinia: استخدم Composable للمنطق البسيط، وPinia لإدارة الحالات الكبيرة
📝 تمارين
-
أسئلة أساسية (مستوى الصعوبة: ⭐)
تنفيذ الدالة
useToggle:- قبول القيم الأولية (الافتراضي: false)
- تُرجع { القيمة، التبديل، setTrue، setFalse }
- اختبار سيناريوهات الاستخدام الأربعة في المكون
-
مسائل متقدمة (مستوى الصعوبة: ⭐⭐)
تنفيذ النسخة الكاملة من
useFetch:- يدعم المعلمة
ref(عناوين URL التفاعلية) - يدعم طريقة
refetch - يدعم إعادة التحميل على الساعة
- معالجة الأخطاء + status التحميل
- الاختبار في مكونين
- يدعم المعلمة
-
مسألة التحدي (مستوى الصعوبة: ⭐⭐⭐)
تنفيذ «مكتبة Composable شاملة للخدمات الخلفية للتجارة الإلكترونية»:
- 5 مكونات قابلة للتركيب: useCart / useAuth / useSearch / usePagination / useTable
- يجب أن يحتوي كل «Composable» على ما لا يقل عن 50 سطراً من كود التنفيذ بالإضافة إلى اختبارات كاملة
- تصدير كل شيء إلى
composables/index.js - اكتب 5 حالات اختبار (باستخدام Vitest)
- قارن طريقة التنفيذ التي اتبعتها مع تلك المستخدمة في مكتبة VueUse
- توثيق واجهة برمجة التطبيقات (API) لكل مكون قابل للتركيب