Vue.js: Composables المخصصة

آخر تحديث: 2026-08-26

تُعد الدوال القابلة للتركيب (المعروفة أيضًا باسم «الخطافات») نمط إعادة الاستخدام الأساسي في واجهة برمجة تطبيقات التركيب (Composition API) في Vue 3 — فهي تُغلف «البيانات التفاعلية + منطق الأعمال» في دوال قابلة لإعادة الاستخدام. وبشكل أساسي، فهي «حزم منطقية» تتألف من ref وcomputed وwatch، وهكذا دواليك.

يُعد إتقان استخدام «Composables» أمرًا أساسيًّا لكتابة تطبيقات Vue قابلة للصيانة — فهي تجعل كود المكونات أكثر إيجازًا وتسمح بمشاركة المنطق التشغيلي بين المكونات. سيساعدك هذا الدرس على إنشاء مكتبة «Composables» الخاصة بك من الصفر.

1. ما ستتعلمه



2. تم تكرار منطق «Get + debounce» الخاص بعربة التسوق 5 مرات

(1) المشكلة: 5 حقول بحث، و5 حالات تكرار في الكود

كان لدى نظام إدارة أليس 5 حقول بحث (الطلبات/المنتجات/المستخدمون/إلخ)، كل منها مزود بمنطق «الجلب» و«إزالة التداخل»:

JS
// ❌ 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 أماكن

JS
// 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 }
}
VUE
<!-- 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»:



3. أساسيات «Composable»

(1) قواعد تسمية الملفات

JS
// ✅ 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) تنظيم الملفات

TEXT 📖 للعرض فقط
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: تتبع موضع الماوس

JS
// 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 }
}
VUE
<!-- MouseTracker.vue -->
<script setup>
import { useMouse } from '@/composables/useMouse'
const { x, y } = useMouse()
</script>

<template>
  <p>Mouse: {{ x }}, {{ y }}</p>
</template>

(2) useLocalStorage: localStorage التفاعلي

JS
// 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
}
VUE
<!-- 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: استرجاع البيانات للأغراض العامة

JS
// 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 }
}
VUE
<!-- 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: قيمة إزالة الارتداد

JS
// 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)
    }
  }))
}
JS
// Usage
const searchInput = ref('')
const debouncedSearch = useDebounce(searchInput, 500)

watch(debouncedSearch, (val) => {
  console.log('Search:', val)
  // 500ms Execute later
})

(5) useToggle: مفتاح التبديل

JS
// 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 }
}
VUE
<!-- 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) تصميم المعلمات الستة الرئيسية

JS
// 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 اعتبارات أساسية لتصميم قيمة العائد

JS
// 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) ستة أنواع رئيسية من تغليف دورة الحياة

JS
// 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)، مما يوفر عليك عناء كتابتها بنفسك.

BASH
# Installation
npm install @vueuse/core
JS
// 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 شيوعًا

JS
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

JS
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

JS
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

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 })
  
  // Cross-Tab Synchronization
  window.addEventListener('storage', (e) => {
    if (e.key === key && e.newValue) {
      data.value = JSON.parse(e.newValue)
    }
  })
  
  return data
}
▶ جرّب الكود

مثال: 4. useDebounce + useThrottle

JS
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 ⭐⭐⭐ ⭐⭐⭐ ⭐⭐ التواصل بين المكونات


❓ أسئلة شائعة

س ما الفرق بين «Composable» و«mixin»؟
ج «mixin» هو نهج إعادة الاستخدام المستخدم في Vue 2 (دمج ضمني، وتعارضات في الأسماء). أما «Composable» فهو النهج المستخدم في Vue 3 (إرجاع صريح، وآمن من حيث الأنواع). توصي Vue 3 باستخدام «Composables».
س هل يجب استدعاء «Composable» في المستوى الأعلى من setup؟
ج نعم. نظرًا لأن «Composables» تستخدم «refs» و«watches» و«hooks» الخاصة بدورة الحياة، فيجب استدعاؤها في المستوى الأعلى من <script setup>.
س هل تقبل العناصر القابلة للتركيب (Composables) المعلمات (props)؟
ج لا تقبل هذه العناصر المعلمات بشكل مباشر. ومع ذلك، يمكنها قبول المعلمات المرجعية (refs) (التفاعلية) أو القيم العادية. وتستخدم مكتبة VueUse المعلمات المرجعية (refs) على نطاق واسع.
س هل يمكن لـ«Composables» استدعاء بعضها البعض؟
ج نعم. «Composables» هي في الأساس دوال ويمكن تداخلها. على سبيل المثال، يمكن أن تحتوي useSearch على useFetch + useDebounce.
س متى يجب عليّ كتابة الكود بنفسي ومتى يجب عليّ استخدام VueUse؟
ج اكتب الكود بنفسك للمنطق الخاص بالأعمال (مثل useSearch أو useCart). استخدم VueUse للمنطق العام (أحداث الماوس، التمرير، إزالة الارتداد، وضع الشاشة الكاملة) لتوفير 80% من وقتك.
س هل Composable نسخة من React Hooks؟
ج نعم، فقد استلهمت من React Hooks. وتستعير Vue 3 ميزاتها وتطورها: فهي تستخدم refs (التجميع التلقائي للتبعيات) مقترنةً بدعوة إعداد على المستوى الأعلى (دون الحاجة إلى تغليفها بـ useEffect).
س كيف يمكنني اختبار «Composable»؟
ج «Composable» هي دالة JavaScript عادية. قم باستدعائها مباشرةً: const { data } = useFetch('/api')، وقم بإجراء فحص صحة على data.value. استخدم Vitest لإجراء اختبار الوحدة.

📖 ملخص


📝 تمارين

  1. أسئلة أساسية (مستوى الصعوبة: ⭐)

    تنفيذ الدالة useToggle:

    • قبول القيم الأولية (الافتراضي: false)
    • تُرجع { القيمة، التبديل، setTrue، setFalse }
    • اختبار سيناريوهات الاستخدام الأربعة في المكون
  2. مسائل متقدمة (مستوى الصعوبة: ⭐⭐)

    تنفيذ النسخة الكاملة من useFetch:

    • يدعم المعلمة ref (عناوين URL التفاعلية)
    • يدعم طريقة refetch
    • يدعم إعادة التحميل على الساعة
    • معالجة الأخطاء + status التحميل
    • الاختبار في مكونين
  3. مسألة التحدي (مستوى الصعوبة: ⭐⭐⭐)

    تنفيذ «مكتبة Composable شاملة للخدمات الخلفية للتجارة الإلكترونية»:

    1. 5 مكونات قابلة للتركيب: useCart / useAuth / useSearch / usePagination / useTable
    2. يجب أن يحتوي كل «Composable» على ما لا يقل عن 50 سطراً من كود التنفيذ بالإضافة إلى اختبارات كاملة
    3. تصدير كل شيء إلى composables/index.js
    4. اكتب 5 حالات اختبار (باستخدام Vitest)
    5. قارن طريقة التنفيذ التي اتبعتها مع تلك المستخدمة في مكتبة VueUse
    6. توثيق واجهة برمجة التطبيقات (API) لكل مكون قابل للتركيب
Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%