تمرين شامل — الميزات الأساسية
في المرحلة 2، تعلمنا عن Pinia و Composable و Server API و Middleware و Plugins—حان وقت ربطها جميعاً. متطلبات Charlie: يجب أن تكون سلة التسوق مدمجة بالكامل من API عبر Store إلى الصفحة؛ يجب أن يكون لوحة تحكم Bob ضوابط وصول؛ ويجب أن تكون تجربة تسوق Alice سلسة ومتواصلة.
1. ما ستتعلمه
- تنفيذ واجهة CRUD API كاملة لسلة التسوق
- دمج Pinia Store لسلة التسوق مع مزامنة حالة SSR
- تطوير Composables مخصصة لـ
useCartوuseProductFilter - تكوين وسيط
authالعام لحماية صفحة الدفع - اختبار تكامل شامل من طرف إلى طرف: API → Composable → Store → عرض الصفحة
2. قصة حقيقية عن فريق
(1) نقطة الألم: الوحدات تعمل بشكل مستقل لكن لا يمكن دمجها
صفحة سلة التسوق الخاصة بـ Alice تعرض المنتجات، لكن البيانات تُفقد بعد التحديث. كتب Bob واجهة API، لكن الواجهة الأمامية لا تستدعيها. الوسيط مكتوب لكنه لم يُدمج. كل وحدة تعمل بشكل جيد بمفردها، لكن عند جمعها معاً، ينهار النظام.
(2) حل اختبار التكامل الشامل من طرف إلى طرف
دمج سلسلة البيانات بالكامل من API → Composable → Store → الصفحة، لضمان تدفق واضح للبيانات في كل مرحلة.
(3) الفوائد: تجربة تسوق كاملة
يمكن لـ Alice إضافة عناصر إلى سلة التسوق → العناصر تبقى بعد التحديث → الانتقال إلى صفحة الدفع → لوحة التحكم محمية بضوابط الوصول؛ العملية بالكامل سلسة.
3. بنية تعاون الوحدات الوظيفية الأساسية في المرحلة 2
graph TB
A[MegaShop المرحلة 2] --> B[طبقة Server API]
A --> C[طبقة Composable]
A --> D[طبقة الحالة - Pinia]
A --> E[طبقة الوسائط]
A --> F[طبقة الإضافات]
B --> B1[/api/products CRUD]
B --> B2[/api/cart CRUD]
B --> B3[/api/auth تسجيل/دخول]
C --> C1[useCart → Store + API]
C --> C2[useProductFilter → API]
C --> C3[usePriceFormat → نقي]
D --> D1[cartStore → items/total]
D --> D2[userStore → auth/profile]
E --> E1[auth.global.ts → فحص تسجيل الدخول]
E --> E2[admin.ts → فحص الدور]
F --> F1[logger → تصحيح]
F --> F2[stripe.client → الدفع]
C1 --> D1
C1 --> B2
C2 --> B1
(1) مسار تدفق البيانات الكامل
| العملية | تدفق البيانات | الوحدات المعنية |
|---|---|---|
| تصفح المنتجات | API → useFetch → الصفحة | Server API |
| إضافة إلى السلة | API → useCart → cartStore → الصفحة | API + Composable + Store |
| عرض السلة | cartStore → الصفحة | Store |
| الدفع | cartStore → useCheckout → Stripe → API | Store + Composable + Plugin + API |
| لوحة التحكم | وسيط auth → وسيط admin → الصفحة | Middleware |
4. واجهة API كاملة لسلة التسوق
(1) ▶ مثال: واجهة API كاملة لسلة التسوق
// server/api/cart/index.get.ts
export default defineEventHandler((event) => {
const sessionId = getCookie(event, 'session-id') || 'default'
const cart = mockCarts.find(c => c.sessionId === sessionId)
return cart || { sessionId, items: [], total: 0 }
})
الناتج:
// تم التنفيذ بنجاح
// server/api/cart/add.post.ts
export default defineEventHandler(async (event) => {
const sessionId = getCookie(event, 'session-id') || 'default'
const { productId, quantity = 1 } = await readBody(event)
const product = mockProducts.find(p => p.id === productId)
if (!product) throw createError({ statusCode: 404, message: 'Product not found' })
let cart = mockCarts.find(c => c.sessionId === sessionId)
if (!cart) {
cart = { sessionId, items: [], total: 0 }
mockCarts.push(cart)
}
const existing = cart.items.find(i => i.productId === productId)
if (existing) {
existing.quantity += quantity
} else {
cart.items.push({ productId, name: product.name, price: product.price, quantity, image: product.image })
}
cart.total = cart.items.reduce((s, i) => s + i.price * i.quantity, 0)
setCookie(event, 'session-id', sessionId, { httpOnly: true, maxAge: 86400 * 30 })
return cart
})
// server/api/cart/remove.delete.ts
export default defineEventHandler(async (event) => {
const sessionId = getCookie(event, 'session-id') || 'default'
const { productId } = await readBody(event)
const cart = mockCarts.find(c => c.sessionId === sessionId)
if (!cart) throw createError({ statusCode: 404, message: 'Cart not found' })
cart.items = cart.items.filter(i => i.productId !== productId)
cart.total = cart.items.reduce((s, i) => s + i.price * i.quantity, 0)
return cart
})
5. تكامل Pinia Store + Composable
(1) ▶ مثال: cartStore محسّن
// stores/cart.ts
export const useCartStore = defineStore('cart', () => {
const items = ref<CartItem[]>([])
const isLoading = ref(false)
const totalItems = computed(() => items.value.reduce((s, i) => s + i.quantity, 0))
const totalPrice = computed(() => items.value.reduce((s, i) => s + i.price * i.quantity, 0))
const formattedTotal = computed(() =>
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(totalPrice.value)
)
async function fetchCart() {
isLoading.value = true
try {
const cart = await $fetch('/api/cart')
items.value = cart.items || []
} finally {
isLoading.value = false
}
}
async function addItem(productId: number, quantity = 1) {
const cart = await $fetch('/api/cart/add', {
method: 'POST', body: { productId, quantity }
})
items.value = cart.items
}
async function removeItem(productId: number) {
const cart = await $fetch('/api/cart/remove', {
method: 'DELETE', body: { productId }
})
items.value = cart.items
}
function clearCart() {
items.value = []
}
return { items, isLoading, totalItems, totalPrice, formattedTotal, fetchCart, addItem, removeItem, clearCart }
})
interface CartItem { productId: number; name: string; price: number; quantity: number; image: string }
الناتج:
// تم التنفيذ بنجاح
(2) ▶ مثال: useCart Composable
// composables/useCart.ts
export function useCart() {
const cartStore = useCartStore()
const { $logger } = useNuxtApp()
const { notify } = useNotification()
async function addToCart(productId: number, productName: string) {
try {
await cartStore.addItem(productId)
notify(`${productName} added to cart`, 'success')
$logger.info('Item added to cart', { productId })
} catch (err) {
notify('Failed to add item', 'error')
$logger.error('Add to cart failed', { productId, err })
}
}
async function removeFromCart(productId: number) {
try {
await cartStore.removeItem(productId)
notify('Item removed from cart', 'info')
} catch (err) {
notify('Failed to remove item', 'error')
}
}
return {
items: computed(() => cartStore.items),
totalItems: computed(() => cartStore.totalItems),
totalPrice: computed(() => cartStore.totalPrice),
formattedTotal: computed(() => cartStore.formattedTotal),
isLoading: computed(() => cartStore.isLoading),
addToCart,
removeFromCart
}
}
الناتج:
// تم التنفيذ بنجاح
6. تكوين وسيط الصلاحيات
(1) ▶ مثال: بنية وسيط كاملة
// middleware/01-auth.global.ts
export default defineNuxtRouteMiddleware((to) => {
const publicPaths = ['/', '/products', '/about', '/login', '/register']
if (publicPaths.some(p => to.path === p || to.path.startsWith('/products'))) return
const token = useCookie('auth-token')
if (!token.value) return navigateTo(`/login?redirect=${encodeURIComponent(to.path)}`)
})
الناتج:
// تم التنفيذ بنجاح
// middleware/admin.ts
export default defineNuxtRouteMiddleware((to) => {
const userCookie = useCookie('user-data')
if (!userCookie.value) return navigateTo('/login')
if ((userCookie.value as any).role !== 'admin') {
throw createError({ statusCode: 403, message: 'Admin access required' })
}
})
7. اختبار تكامل شامل من طرف إلى طرف للصفحات
(1) ▶ مثال: صفحة تفاصيل المنتج (تدفق بيانات كامل)
<!-- pages/products/[id].vue -->
<template>
<div v-if="product" class="product-detail">
<img :src="product.image" :alt="product.name" />
<div class="info">
<h1>{{ product.name }}</h1>
<p class="price">{{ formattedPrice }}</p>
<p>{{ product.reviewCount }} reviews</p>
<button @click="handleAddToCart" :disabled="adding">
{{ adding ? 'Adding...' : 'Add to Cart' }}
</button>
</div>
</div>
</template>
<script setup lang="ts">
const route = useRoute()
const { data: product } = await useFetch(`/api/products/${route.params.id}`)
const { formatted } = usePriceFormat(computed(() => product.value?.price || 0))
const formattedPrice = formatted
const { addToCart } = useCart()
const adding = ref(false)
async function handleAddToCart() {
if (!product.value) return
adding.value = true
await addToCart(product.value.id, product.value.name)
adding.value = false
}
</script>
الناتج:
// تم التنفيذ بنجاح
8. قائمة فحص اختبار التكامل
| عنصر الاختبار | طريقة التحقق | النتيجة المتوقعة |
|---|---|---|
| API متاح | curl /api/products | يُرجع JSON للمنتجات |
| مشاركة حالة Store | إضافة عنصر في الصفحة الرئيسية → عرض الكمية في صفحة التفاصيل | الكميات متطابقة |
| استدعاء Composable | النقر على "أضف إلى السلة" | نجاح: تمت الإضافة + إشعار |
| حماية الوسيط | وصول غير مصادق إلى /admin | إعادة توجيه إلى /login |
| الإضافة تعمل | عرض وحدة التحكم | يوجد مخرجات Logger |
| إرواء بيانات SSR | عرض المصدر | HTML يحتوي على بيانات المنتج |
| ثبات Cookie | تحديث الصفحة | بيانات سلة التسوق محفوظة |
❓ أسئلة شائعة
addItem، استخدم القيمة المُرجعة لتحديث مصفوفة items لضمان بقاء Store والخلفية متزامنين. لا تُعدِّل Store فقط دون استدعاء API.📖 ملخص
- تدفق بيانات سلة التسوق الكامل: API → useCart → cartStore → الصفحة؛ البيانات تبقى متسقة في كل مرحلة
- Pinia Store يُحدِّث حالته باستخدام القيمة المُرجعة بعد استدعاء API، مما يضمن التزامن بين العميل والخادم
- Composable
useCartيُغلِّف Store و API و الإشعارات؛ الصفحات تحتاج فقط لاستدعاءaddToCart - وسيط auth العام + وسيط "admin" لتنفيذ حراس تفويض المسارات بشكل شامل
- اختبار تكامل شامل من طرف إلى طرف: تحقق من كل مكون—API و Store و Composable و Middleware و Plugin—خطوة بخطوة
📝 تمارين
- تمرين أساسي (الصعوبة: ⭐): أكمل تكامل واجهة API لسلة التسوق مع cartStore بحيث يتحدث عدد سلة التسوق عند النقر على زر "أضف إلى السلة" في صفحة تفاصيل المنتج.
- تمرين متقدم (الصعوبة: ⭐⭐): نفِّذ نظام وسائط كامل: إذا وصل مستخدم غير مصادق إلى /checkout، أعد توجيهه إلى صفحة تسجيل الدخول؛ إذا وصل مستخدم عادي إلى /admin، أرجئ خطأ 403.
- تحدٍ (الصعوبة: ⭐⭐⭐): نفِّذ عملية تسوق من طرف إلى طرف: تصفح المنتجات → إضافة إلى السلة → عرض السلة → الدفع (محاكاة الدفع باستخدام إضافة Stripe)، مع ضمان اتساق البيانات خلال العملية بالكامل.
---|



