إدارة الحالة Pinia
بعد أن تضيف Alice عنصرًا إلى سلتها، تكون السلة فارغة عند انتقالها لصفحة أخرى—كل صفحة لها حالة مكونات خاصة بها. يلاحظ Bob أن حالة تسجيل دخول المستخدم غير متسقة بين SSR وجانب العميل. يحتاج Charlie إلى Pinia لإدارة الحالة المشتركة عبر المكونات والصفحات.
1. ما ستتعلمه
- تثبيت وإعداد تلقائي لوحدة @pinia/nuxt
- تعريف Store: صياغة الخيارات وصياغة Setup
- آليات الجفاف والإماهة في حالة SSR
- تفكيك storeToRefs التفاعلي و $patch/$reset
- سلة MegaShop: إضافة، حذف، تحديث، واستعلام + حساب السعر الإجمالي
2. قصة حقيقية من مستهلكة
(1) نقطة الألم: حالة سلة تسوق "مفقودة"
أضافت Alice ثلاثة عناصر إلى سلتها على صفحة MegaShop الرئيسية، لكن عند النقر لعرض صفحة تفاصيل المنتج، أظهرت أيقونة السلة 0 عناصر—الحالة غير متزامنة. واجه Bob مشكلة مشابهة: بعد تسجيل الدخول وتحديث الصفحة، اختفت حالة تسجيل الدخول.
(2) حلول إدارة حالة Pinia
يفصل Pinia سلة التسوق وحالة المستخدم عن دورة حياة المكون، ويحولهما إلى بيانات تفاعلية مشتركة عالميًا:
// composables/useCartStore.ts
export const useCartStore = defineStore('cart', () => {
const items = ref<CartItem[]>([])
const total = computed(() => items.value.reduce((s, i) => s + i.price * i.quantity, 0))
return { items, total }
})
(3) الفائدة: استمرارية الحالة عبر الصفحات
تبقى سلة Alice متسقة عبر جميع الصفحات، وحالة تسجيل دخول Bob أصبحت متزامنة بين SSR والعميل، مما يلغي مشاكل فقدان الحالة.
3. تثبيت وإعداد Pinia
(1) خطوات التثبيت
npm install @pinia/nuxt pinia
(1) ▶ مثال: إعداد nuxt.config.ts
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@pinia/nuxt'],
// Pinia يُعد تلقائيًا، لا حاجة لإعداد إضافي
})
الناتج:
// Execution Successful
(2) مقارنة Pinia و Vuex و useState
| البُعد | Pinia | Vuex 4 | Nuxt useState |
|---|---|---|---|
| دعم Vue 3 | ✅ أصلي | ⚠️ وضع التوافق | ✅ حصري لـ Nuxt |
| TypeScript | ✅ استنتاج كامل للأنواع | ❌ يتطلب إعلان يدوي | ✅ دعم الأنواع العامة |
| إماهة SSR | ✅ تلقائي | ⚠️ يتطلب إعداد | ✅ تلقائي |
| تقسيم الكود | ✅ عند الطلب | ❌ عالمي | ❌ عالمي |
| DevTools | ✅ مدعوم | ✅ مدعوم | ❌ غير مدعوم |
| الحجم | ~1 كيلوبايت | ~6 كيلوبايت | مدمج |
| حالات الاستخدام | سيناريوهات معقدة | ترحيل قديم | مشاركة بسيطة |
4. كيفية تعريف Store
(1) Store بنمط الخيارات
(1) ▶ مثال: Store لسلة التسوق بنمط الخيارات
// stores/cart.ts
export const useCartStore = defineStore('cart', {
state: () => ({
items: [] as CartItem[],
couponCode: '' as string
}),
getters: {
totalItems: (state) => state.items.reduce((sum, item) => sum + item.quantity, 0),
totalPrice: (state) => {
return state.items.reduce((sum, item) => sum + item.price * item.quantity, 0)
},
formattedTotal(): string {
return new Intl.NumberFormat('en-US', {
style: 'currency', currency: 'USD'
}).format(this.totalPrice)
}
},
actions: {
addItem(product: Product) {
const existing = this.items.find(i => i.id === product.id)
if (existing) {
existing.quantity++
} else {
this.items.push({ ...product, quantity: 1 })
}
},
removeItem(productId: number) {
this.items = this.items.filter(i => i.id !== productId)
},
updateQuantity(productId: number, quantity: number) {
const item = this.items.find(i => i.id === productId)
if (item) item.quantity = Math.max(0, quantity)
this.items = this.items.filter(i => i.quantity > 0)
},
clearCart() {
this.items = []
this.couponCode = ''
}
}
})
interface CartItem {
id: number; name: string; price: number; quantity: number; image: string
}
interface Product {
id: number; name: string; price: number; image: string
}
الناتج:
// Execution Successful
(2) Store بصياغة Setup
(2) ▶ مثال: Store للمستخدم بصياغة Setup
// stores/user.ts
export const useUserStore = defineStore('user', () => {
// State
const user = ref<User | null>(null)
const isAuthenticated = computed(() => !!user.value)
const fullName = computed(() => user.value ? `${user.value.firstName} ${user.value.lastName}` : '')
// Actions
async function login(email: string, password: string) {
const response = await $fetch('/api/auth/login', {
method: 'POST',
body: { email, password }
})
user.value = response.user
}
function logout() {
user.value = null
}
async function fetchProfile() {
const profile = await $fetch('/api/user/profile')
user.value = profile
}
return {
user, isAuthenticated, fullName,
login, logout, fetchProfile
}
})
interface User {
id: number; email: string; firstName: string; lastName: string; role: 'customer' | 'admin'
}
الناتج:
// Execution Successful
(3) مقارنة الطريقتين
| البُعد | نمط الخيارات | صياغة Setup |
|---|---|---|
| البنية | state/getters/actions | ref/computed/function |
| TypeScript | ⚠️ يتطلب إعلان واجهة | ✅ استنتاج تلقائي |
| المرونة | ⚠️ محدودة | ✅ متاح في أي Composable |
| SSR | ✅ يدعم $reset | ⚠️ لا يدعم $reset (يجب تنفيذه يدويًا) |
| حالات الاستخدام | Store بسيط | Store معقد/يحتاج Composable |
5. الجفاف والإماهة في SSR
(1) انتقالات حالة Pinia في SSR
flowchart LR
A[الخادم: Store مملوء بالبيانات] --> B[تسلسل الحالة إلى HTML payload]
B --> C[العميل: قراءة payload]
C --> D[إماهة Store بحالة الخادم]
D --> E[العميل: Store جاهز، بدون إعادة جلب]
(1) ▶ مثال: تهيئة Store في SSR
// plugins/init-pinia.server.ts
export default defineNuxtPlugin(() => {
const cartStore = useCartStore()
// تهيئة السلة من ملف تعريف الارتباط على الخادم
const cartCookie = useCookie('cart-items')
if (cartCookie.value) {
cartStore.items = cartCookie.value
}
})
الناتج:
// Execution Successful
(2) ▶ مثال: الفروقات بين Pinia و useState في SSR
| البُعد | Pinia | useState |
|---|---|---|
| إماهة SSR | ✅ payload تلقائي | ✅ payload تلقائي |
| متعدد Store | ✅ مساحات أسماء مستقلة | ⚠️ إدارة مفاتيح يدوية |
| DevTools | ✅ تصور | ❌ لا يوجد |
| الاستمرارية | ✅ دعم الإضافات | ⚠️ يتطلب ملفات تعريف ارتباط يدوية |
| منطق معقد | ✅ actions/getters | ❌ ref فقط |
6. storeToRefs والعمليات المجمعة
(1) ▶ مثال: تفكيك storeToRefs التفاعلي
<script setup lang="ts">
const cartStore = useCartStore()
// ✅ تفكيك تفاعلي - يحافظ على التفاعلية
const { items, totalPrice, totalItems } = storeToRefs(cartStore)
// ❌ تفكيك مباشر - يفقد التفاعلية
// const { items, totalPrice } = cartStore
// Actions يمكن تفكيكها مباشرة (لا حاجة للتفاعلية)
const { addItem, removeItem, clearCart } = cartStore
</script>
الناتج:
// Execution Successful
(2) ▶ مثال: تحديث مجموعة بـ $patch
// تحديث مجموعة بـ $patch - مشغل تفاعل واحد
const cartStore = useCartStore()
// نمط الكائن
cartStore.$patch({
couponCode: 'SAVE20',
items: [...cartStore.items, newItem]
})
// نمط الدالة (أفضل لطفرات المصفوفات)
cartStore.$patch((state) => {
state.couponCode = 'SAVE20'
state.items.push(newItem)
state.items[0].quantity = 3
})
الناتج:
// Execution Successful
(3) ▶ مثال: إضافة استمرارية سلة التسوق
// plugins/pinia-persist.client.ts
export default defineNuxtPlugin(({ $pinia }) => {
$pinia.use(({ store }) => {
// التحميل من localStorage على العميل
const saved = localStorage.getItem(`pinia-${store.$id}`)
if (saved) store.$patch(JSON.parse(saved))
// الحفظ في localStorage عند التغيير
store.$subscribe((mutation, state) => {
localStorage.setItem(`pinia-${store.$id}`, JSON.stringify(state))
})
})
})
الناتج:
// Execution Successful
7. مثال شامل: نظام سلة MegaShop
<!-- pages/cart.vue -->
<template>
<div class="cart-page">
<h1>Shopping Cart</h1>
<div v-if="items.length === 0" class="empty-cart">
<p>Your cart is empty</p>
<NuxtLink to="/products">Continue Shopping</NuxtLink>
</div>
<div v-else>
<div class="cart-items">
<div v-for="item in items" :key="item.id" class="cart-item">
<img :src="item.image" :alt="item.name" />
<div class="details">
<h3>{{ item.name }}</h3>
<p>${{ item.price }} USD</p>
<div class="quantity">
<button @click="updateQuantity(item.id, item.quantity - 1)">-</button>
<span>{{ item.quantity }}</span>
<button @click="updateQuantity(item.id, item.quantity + 1)">+</button>
</div>
</div>
<button @click="removeItem(item.id)" class="remove">Remove</button>
</div>
</div>
<div class="cart-summary">
<p>Items: {{ totalItems }}</p>
<p class="total">Total: {{ formattedTotal }}</p>
<button @click="clearCart" class="clear">Clear Cart</button>
<NuxtLink to="/checkout" class="checkout">Proceed to Checkout</NuxtLink>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const cartStore = useCartStore()
const { items, totalItems, formattedTotal } = storeToRefs(cartStore)
const { removeItem, updateQuantity, clearCart } = cartStore
</script>
❓ أسئلة شائعة
📖ملخص
- Pinia هو حل إدارة الحالة الرسمي لـ Vue 3؛ وحدة @pinia/nuxt توفر تكاملًا تلقائيًا
- Store بنمط الخيارات له هيكل واضح (state/getters/actions)، وصياغة Setup أكثر مرونة
- إماهة SSR تكتمل تلقائيًا: بيانات Store من جانب الخادم ← payload ← Store من جانب العميل
- storeToRefs يحافظ على تفاعلية التفكيك؛ $patch يُشغل تصييرًا واحدًا فقط حتى مع التحديثات المجمعة
- MegaShop يستخدم Pinia لإدارة سلة التسوق وحالة المستخدم، ويعتمد على ملفات تعريف الارتباط للاستمرارية
📝تمارين
- تمرين أساسي (الصعوبة: ⭐): أنشئ useCartStore، نفّذ addItem و removeItem، تعامل مع سلة التسوق على صفحتين مختلفتين، وتحقق من مشاركة الحالة.
- تمرين متقدم (الصعوبة: ⭐⭐): استخدم صياغة Setup لتعريف useUserStore، نفّذ login و logout و fetchProfile، وتحقق من عمل إماهة SSR بشكل صحيح.
- تحدٍ (الصعوبة: ⭐⭐⭐): نفّذ استمرارية سلة التسوق (بحيث تُحتفظ البيانات بعد تحديث الصفحة)، وقارن إيجابيات وسلبيات استخدام إضافة localStorage مقابل ملفات تعريف الارتباط للمزامنة.
---|



