404 Not Found

404 Not Found


nginx

Composables: الدوال المركبة

فريق Charlie لديه الكثير من المنطق المكرر—تنسيق الأسعار مكتوب بشكل منفصل في 10 مكونات، ومنطق البحث مبعثر عبر 3 صفحات. ميزة المفضلة لدى Alice تحتاج لمشاركة الحالة عبر الصفحات. Bob يحتاج منطق تصفح عام. Composables هو الحل—استخرج المنطق في دوال قابلة لإعادة الاستخدام.

1. ما ستتعلمه


2. قصة حقيقية لمهندس معمارية

(1) نقطة الألم: منطق مكرر مبعثر في الكود

MegaShop لديه 10 مكونات تحتاج لتنسيق الأسعار، وكل واحدة تتطلب تنفيذ Intl.NumberFormat مخصص. نفذت Alice ميزة المفضلة على كل من الصفحة الرئيسية وصفحة تفاصيل المنتج، لكن المنطق غير متسق—العناصر المفضلة على الصفحة الرئيسية لا تظهر على صفحة تفاصيل المنتج.

(2) حل Composable

بمجرد استخراجه في Composable، عرّفه مرة واحدة وأعد استخدامه في كل مكان:

TYPESCRIPT
// composables/usePriceFormat.ts
export function usePriceFormat(price: Ref<number>, currency = 'USD') {
  return computed(() => new Intl.NumberFormat('en-US', {
    style: 'currency', currency
  }).format(price.value))
}

(3) الفوائد: DRY + الاتساق

عشرة مكونات تشترك في نفس منطق تنسيق الأسعار، وحالة المفضلة تبقى متسقة عبر جميع الصفحات بفضل useWishlist. انخفض حجم الكود بنسبة 60%، وانخفضت الأخطاء بنسبة 80%.


3. نظرة شاملة على Composables المدمجة

(1) رسم تبعيات Composables المدمجة

100%
graph TB
    A[Nuxt Built-in Composables] --> B[Data Fetching]
    A --> C[State Management]
    A --> D[Navigation]
    A --> E[Head Management]
    A --> F[Context]
    A --> G[Utility]

    B --> B1[useFetch]
    B --> B2[useAsyncData]
    B --> B3[$fetch]
    C --> C1[useState]
    C --> C2[useCookie]
    D --> D1[useRouter]
    D --> D2[useRoute]
    D --> D3[navigateTo]
    E --> E1[useHead]
    E --> E2[useSeoMeta]
    F --> F1[useRequestHeaders]
    F --> F2[useRuntimeConfig]
    G --> G1[useAppConfig]
    G --> G2[useHydration]

(2) مرجع سريع لـ Composables المدمجة الأساسية

Composable الغرض يدعم SSR القيمة المرجعة
useFetch استرجاع البيانات { data, pending, error, refresh }
useAsyncData غير متزامن عام { data, pending, error, refresh }
useState حالة مشتركة Ref
useCookie عمليات ملفات تعريف الارتباط Ref
useRouter نسخة الموجه ⚠️ جانب العميل فقط Router
useRoute المسار الحالي Route
useHead إدارة الرأس void
useSeoMeta بيانات SEO الوصفية void
useRuntimeConfig إعداد وقت التشغيل RuntimeConfig
useRequestHeaders ترويسات الطلب ✅ جانب الخادم فقط Headers

(1) ▶ مثال: مشاركة الحالة عبر المكونات بـ useState

VUE
<script setup lang="ts">
// مشاركة حالة الإشعارات عبر المكونات
const notifications = useState<Notification[]>('notifications', () => [])

function addNotification(message: string, type: 'success' | 'error' = 'success') {
  notifications.value.push({ id: Date.now(), message, type })
  setTimeout(() => {
    notifications.value = notifications.value.filter(n => n.id !== Date.now())
  }, 3000)
}
</script>

الناتج:

TEXT
// Execution Successful

4. Composable مخصص

(1) اصطلاحات التسمية والمجلدات

القاعدة الوصف مثال
المجلد composables/ composables/useCart.ts
الاسم بادئة use usePriceFormat
التصدير تصدير مسمى export function usePriceFormat()
النوع واجهة معرّفة في نفس الملف interface PriceOptions {}
الاستيراد التلقائي ✅ تلقائي لا حاجة لاستيراد يدوي

(1) ▶ مثال: usePriceFormat لتنسيق الأسعار

TYPESCRIPT
// composables/usePriceFormat.ts
interface PriceFormatOptions {
  currency?: string
  locale?: string
  showDecimals?: boolean
}

export function usePriceFormat(
  price: Ref<number> | number,
  options: PriceFormatOptions = {}
) {
  const { currency = 'USD', locale = 'en-US', showDecimals = true } = options

  const formatted = computed(() => {
    const value = unref(price)
    return new Intl.NumberFormat(locale, {
      style: 'currency',
      currency,
      minimumFractionDigits: showDecimals ? 2 : 0,
      maximumFractionDigits: showDecimals ? 2 : 0
    }).format(value)
  })

  return { formatted }
}

الناتج:

TEXT
// Execution Successful

(2) ▶ مثال: useWishlist للمفضلة

TYPESCRIPT
// composables/useWishlist.ts
export function useWishlist() {
  const wishlist = useState<number[]>('wishlist', () => [])

  function toggle(productId: number) {
    const index = wishlist.value.indexOf(productId)
    if (index === -1) {
      wishlist.value.push(productId)
    } else {
      wishlist.value.splice(index, 1)
    }
  }

  function isWishlisted(productId: number): boolean {
    return wishlist.value.includes(productId)
  }

  const count = computed(() => wishlist.value.length)

  return { wishlist, toggle, isWishlisted, count }
}

الناتج:

TEXT
// Execution Successful

(3) ▶ مثال: useProductSearch لمنطق البحث

TYPESCRIPT
// composables/useProductSearch.ts
interface SearchParams {
  query?: string
  category?: string
  minPrice?: number
  maxPrice?: number
  sortBy?: 'price' | 'name' | 'rating'
  page?: number
}

export function useProductSearch(initialParams: SearchParams = {}) {
  const params = reactive<SearchParams>({
    query: '',
    category: '',
    minPrice: 0,
    maxPrice: 10000,
    sortBy: 'rating',
    page: 1,
    ...initialParams
  })

  const { data, pending, error, refresh } = useFetch('/api/products/search', {
    query: params,
    watch: [params],
    default: () => ({ items: [], total: 0 })
  })

  function resetFilters() {
    params.query = ''
    params.category = ''
    params.minPrice = 0
    params.maxPrice = 10000
    params.sortBy = 'rating'
    params.page = 1
  }

  return { params, data, pending, error, refresh, resetFilters }
}

الناتج:

TEXT
// Execution Successful

5. نمط تصميم Composable

(1) مقارنة أنماط التصميم

النمط الخصائص حالات الاستخدام أمثلة
Composable متزامن يُرجع computed/ref التنسيق/الحساب usePriceFormat
Composable غير متزامن استدعاء داخلي لـ useFetch استرجاع البيانات useProductSearch
تفاعلية المعلمات تحديث تلقائي عبر watch فلتر/بحث useProductSearch
مشاركة الحالة useState داخلي حالة عبر المكونات useWishlist
تركيب Composable استدعاء Composables أخرى منطق معقد useCheckout

(1) ▶ مثال: تركيب Composable—useCheckout

TYPESCRIPT
// composables/useCheckout.ts
export function useCheckout() {
  // تركيب composables أخرى
  const cartStore = useCartStore()
  const { formatted: total } = usePriceFormat(
    computed(() => cartStore.totalPrice)
  )
  const { isAuthenticated, user } = storeToRefs(useUserStore())

  const isProcessing = ref(false)
  const orderId = ref<string | null>(null)

  async function processCheckout() {
    if (!isAuthenticated.value) {
      navigateTo('/login')
      return
    }

    isProcessing.value = true
    try {
      const order = await $fetch('/api/orders', {
        method: 'POST',
        body: {
          items: cartStore.items,
          userId: user.value?.id,
          total: cartStore.totalPrice
        }
      })
      orderId.value = order.id
      cartStore.clearCart()
      navigateTo(`/orders/${order.id}`)
    } catch (err) {
      console.error('Checkout failed:', err)
    } finally {
      isProcessing.value = false
    }
  }

  return { total, isProcessing, orderId, processCheckout, isAuthenticated }
}

الناتج:

TEXT
// Execution Successful

(2) مبادئ تصميم Composable

المبدأ الشرح المثال المضاد
المسؤولية الواحدة Composable واحد يقوم بشيء واحد useShopAndCart()
المعلمات تفاعلية يقبل معلمات Ref يقبل قيمًا خامة فقط
تفكيك القيمة المرجعة يُرجع كائنًا مسمى يُرجع مصفوفة
تسمية واضحة use + فعل/اسم doStuff()
بدون آثار جانبية لا يعدّل DOM مباشرة يعالج DOM داخليًا

6. مثال شامل: بنية Composables في MegaShop

TYPESCRIPT
// composables/usePagination.ts
export function usePagination(totalItems: Ref<number>, pageSize = 20) {
  const route = useRoute()
  const router = useRouter()

  const currentPage = computed(() => Number(route.query.page) || 1)
  const totalPages = computed(() => Math.ceil(totalItems.value / pageSize))
  const hasNext = computed(() => currentPage.value < totalPages.value)
  const hasPrev = computed(() => currentPage.value > 1)

  function goToPage(page: number) {
    router.push({ query: { ...route.query, page: String(page) } })
  }

  function nextPage() {
    if (hasNext.value) goToPage(currentPage.value + 1)
  }

  function prevPage() {
    if (hasPrev.value) goToPage(currentPage.value - 1)
  }

  const offset = computed(() => (currentPage.value - 1) * pageSize)

  return { currentPage, totalPages, hasNext, hasPrev, offset, goToPage, nextPage, prevPage }
}
TYPESCRIPT
// composables/useNotification.ts
interface Notification { id: number; message: string; type: 'success' | 'error' | 'info' }

export function useNotification() {
  const notifications = useState<Notification[]>('notifications', () => [])

  function notify(message: string, type: Notification['type'] = 'success') {
    const id = Date.now()
    notifications.value = [...notifications.value, { id, message, type }]
    setTimeout(() => {
      notifications.value = notifications.value.filter(n => n.id !== id)
    }, 3000)
  }

  function dismiss(id: number) {
    notifications.value = notifications.value.filter(n => n.id !== id)
  }

  return { notifications, notify, dismiss }
}

❓ أسئلة شائعة

س كيف أختار بين Composables و Pinia Store؟
ج استخدم Store للحالة المستمرة عبر المكونات (مثل سلة التسوق، بيانات المستخدم)، واستخدم Composables لإعادة استخدام المنطق (مثل التنسيق، البحث). Composables أخف وزنًا، بينما Store يوفر دعم DevTools.
س هل يمكنني استخدام useFetch في Composable؟
ج نعم، هذا يسمى Composable غير متزامن. تحتاج لاستخدام await عند استدعائه، تمامًا كما تفعل في الصفحة. useProductSearch مصمم بهذه الطريقة.
س لماذا يجب أن تبدأ Composables بـ "use"؟
ج ميزة الاستيراد التلقائي في Nuxt 3 تمسح الملفات تحت مجلد composables/ ولا تتطلب بادئة "use". لكن اصطلاح مجتمع Vue هو البدء بـ "use" لتسهيل التعريف ودعم الأدوات.
س هل يمكنني استخدام useState في Composable؟
ج نعم. الحالة التي ينشئها useState تُماه تلقائيًا أثناء SSR وعلى جانب العميل، مما يجعلها مناسبة للمشاركة عبر المكونات. useWishlist يستخدم useState للتخزين.
س هل يجب أن تكون معلمات Composable من نوع Refs أو قيمًا خامة؟
ج نوصي بقبول Refs حتى يتمكن Composable من التحديث تفاعليًا عند تغير المعلمات. استخدم unref() لدعم كلا النوعين.
س هل تشارك مكونات متعددة تستدعي نفس Composable الحالة؟
ج إذا استُخدمت useState داخل Composable، فإن الحالة مشتركة. إذا استُخدمت ref أو reactive، تُنشأ حالة منفصلة في كل مرة يُستدعى فيها Composable. يعتمد الاختيار على احتياجاتك.

📖ملخص


📝تمارين

  1. تمرين أساسي (الصعوبة: ⭐): أنشئ Composable باسم usePriceFormat لتنسيق الأسعار كـ USD/JPY/CNY في مكونات مختلفة.
  2. تمرين متقدم (الصعوبة: ⭐⭐): أنشئ useProductSearch لتنفيذ بحث بالكلمات المفتاحية وتصفية الفئات والتصفح. تحقق من أن الصفحة تُحدّث تلقائيًا عند تغير معامل watch.
  3. تحدٍ (الصعوبة: ⭐⭐⭐): أنشئ useCheckout مركب يدمج Store لسلة التسوق وتنسيق الأسعار وتقديم الطلبات لتنفيذ عملية دفع كاملة.

---|

Web-Tutorial.com

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

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

100%