Composables: الدوال المركبة
فريق Charlie لديه الكثير من المنطق المكرر—تنسيق الأسعار مكتوب بشكل منفصل في 10 مكونات، ومنطق البحث مبعثر عبر 3 صفحات. ميزة المفضلة لدى Alice تحتاج لمشاركة الحالة عبر الصفحات. Bob يحتاج منطق تصفح عام. Composables هو الحل—استخرج المنطق في دوال قابلة لإعادة الاستخدام.
1. ما ستتعلمه
- نظرة شاملة على Composables المدمجة في Nuxt 3
- Composables المخصصة: اصطلاحات المجلدات وإرشادات التسمية
- نمط تصميم Composable: غير متزامن / تفاعلية المعلمات / تفكيك القيمة المرجعة
- تركيب Composable: Composable يستدعي Composable آخر
- MegaShop: useProductSearch/usePriceFormat/useWishlist
2. قصة حقيقية لمهندس معمارية
(1) نقطة الألم: منطق مكرر مبعثر في الكود
MegaShop لديه 10 مكونات تحتاج لتنسيق الأسعار، وكل واحدة تتطلب تنفيذ Intl.NumberFormat مخصص. نفذت Alice ميزة المفضلة على كل من الصفحة الرئيسية وصفحة تفاصيل المنتج، لكن المنطق غير متسق—العناصر المفضلة على الصفحة الرئيسية لا تظهر على صفحة تفاصيل المنتج.
(2) حل Composable
بمجرد استخراجه في Composable، عرّفه مرة واحدة وأعد استخدامه في كل مكان:
// 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 المدمجة
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
<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>
الناتج:
// Execution Successful
4. Composable مخصص
(1) اصطلاحات التسمية والمجلدات
| القاعدة | الوصف | مثال |
|---|---|---|
| المجلد | composables/ | composables/useCart.ts |
| الاسم | بادئة use | usePriceFormat |
| التصدير | تصدير مسمى | export function usePriceFormat() |
| النوع | واجهة معرّفة في نفس الملف | interface PriceOptions {} |
| الاستيراد التلقائي | ✅ تلقائي | لا حاجة لاستيراد يدوي |
(1) ▶ مثال: usePriceFormat لتنسيق الأسعار
// 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 }
}
الناتج:
// Execution Successful
(2) ▶ مثال: useWishlist للمفضلة
// 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 }
}
الناتج:
// Execution Successful
(3) ▶ مثال: useProductSearch لمنطق البحث
// 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 }
}
الناتج:
// Execution Successful
5. نمط تصميم Composable
(1) مقارنة أنماط التصميم
| النمط | الخصائص | حالات الاستخدام | أمثلة |
|---|---|---|---|
| Composable متزامن | يُرجع computed/ref | التنسيق/الحساب | usePriceFormat |
| Composable غير متزامن | استدعاء داخلي لـ useFetch | استرجاع البيانات | useProductSearch |
| تفاعلية المعلمات | تحديث تلقائي عبر watch | فلتر/بحث | useProductSearch |
| مشاركة الحالة | useState داخلي | حالة عبر المكونات | useWishlist |
| تركيب Composable | استدعاء Composables أخرى | منطق معقد | useCheckout |
(1) ▶ مثال: تركيب Composable—useCheckout
// 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 }
}
الناتج:
// Execution Successful
(2) مبادئ تصميم Composable
| المبدأ | الشرح | المثال المضاد |
|---|---|---|
| المسؤولية الواحدة | Composable واحد يقوم بشيء واحد | useShopAndCart() |
| المعلمات تفاعلية | يقبل معلمات Ref | يقبل قيمًا خامة فقط |
| تفكيك القيمة المرجعة | يُرجع كائنًا مسمى | يُرجع مصفوفة |
| تسمية واضحة | use + فعل/اسم | doStuff() |
| بدون آثار جانبية | لا يعدّل DOM مباشرة | يعالج DOM داخليًا |
6. مثال شامل: بنية Composables في MegaShop
// 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 }
}
// 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 }
}
❓ أسئلة شائعة
📖ملخص
- Nuxt 3 يتضمن أكثر من 20 Composable مدمج: useFetch و useState و useCookie و useRouter وغيرها
- ضع Composables المخصصة في مجلد composables/، سمّها ببادئة use، وسيتم استيرادها تلقائيًا
- أنماط التصميم: متزامن (تنسيق)، غير متزامن (استرجاع بيانات)، تفاعلي (بحث)، مركب (دفع)
- يمكن لـ Composable استدعاء Composables أخرى لتنفيذ منطق معقد متعدد الطبقات
- MegaShop يبني طبقة منطق الأعمال باستخدام usePriceFormat و useWishlist و useProductSearch و useCheckout
📝تمارين
- تمرين أساسي (الصعوبة: ⭐): أنشئ Composable باسم usePriceFormat لتنسيق الأسعار كـ USD/JPY/CNY في مكونات مختلفة.
- تمرين متقدم (الصعوبة: ⭐⭐): أنشئ useProductSearch لتنفيذ بحث بالكلمات المفتاحية وتصفية الفئات والتصفح. تحقق من أن الصفحة تُحدّث تلقائيًا عند تغير معامل watch.
- تحدٍ (الصعوبة: ⭐⭐⭐): أنشئ useCheckout مركب يدمج Store لسلة التسوق وتنسيق الأسعار وتقديم الطلبات لتنفيذ عملية دفع كاملة.
---|



