404 Not Found

404 Not Found


nginx

إدارة الحالة Pinia

بعد أن تضيف Alice عنصرًا إلى سلتها، تكون السلة فارغة عند انتقالها لصفحة أخرى—كل صفحة لها حالة مكونات خاصة بها. يلاحظ Bob أن حالة تسجيل دخول المستخدم غير متسقة بين SSR وجانب العميل. يحتاج Charlie إلى Pinia لإدارة الحالة المشتركة عبر المكونات والصفحات.

1. ما ستتعلمه


2. قصة حقيقية من مستهلكة

(1) نقطة الألم: حالة سلة تسوق "مفقودة"

أضافت Alice ثلاثة عناصر إلى سلتها على صفحة MegaShop الرئيسية، لكن عند النقر لعرض صفحة تفاصيل المنتج، أظهرت أيقونة السلة 0 عناصر—الحالة غير متزامنة. واجه Bob مشكلة مشابهة: بعد تسجيل الدخول وتحديث الصفحة، اختفت حالة تسجيل الدخول.

(2) حلول إدارة حالة Pinia

يفصل Pinia سلة التسوق وحالة المستخدم عن دورة حياة المكون، ويحولهما إلى بيانات تفاعلية مشتركة عالميًا:

TYPESCRIPT
// 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) خطوات التثبيت

BASH
npm install @pinia/nuxt pinia

(1) ▶ مثال: إعداد nuxt.config.ts

TYPESCRIPT
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@pinia/nuxt'],
  // Pinia يُعد تلقائيًا، لا حاجة لإعداد إضافي
})

الناتج:

TEXT
// 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 لسلة التسوق بنمط الخيارات

TYPESCRIPT
// 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
}

الناتج:

TEXT
// Execution Successful

(2) Store بصياغة Setup

(2) ▶ مثال: Store للمستخدم بصياغة Setup

TYPESCRIPT
// 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'
}

الناتج:

TEXT
// Execution Successful

(3) مقارنة الطريقتين

البُعد نمط الخيارات صياغة Setup
البنية state/getters/actions ref/computed/function
TypeScript ⚠️ يتطلب إعلان واجهة ✅ استنتاج تلقائي
المرونة ⚠️ محدودة ✅ متاح في أي Composable
SSR ✅ يدعم $reset ⚠️ لا يدعم $reset (يجب تنفيذه يدويًا)
حالات الاستخدام Store بسيط Store معقد/يحتاج Composable

5. الجفاف والإماهة في SSR

(1) انتقالات حالة Pinia في SSR

100%
flowchart LR
    A[الخادم: Store مملوء بالبيانات] --> B[تسلسل الحالة إلى HTML payload]
    B --> C[العميل: قراءة payload]
    C --> D[إماهة Store بحالة الخادم]
    D --> E[العميل: Store جاهز، بدون إعادة جلب]

(1) ▶ مثال: تهيئة Store في SSR

TYPESCRIPT
// plugins/init-pinia.server.ts
export default defineNuxtPlugin(() => {
  const cartStore = useCartStore()
  // تهيئة السلة من ملف تعريف الارتباط على الخادم
  const cartCookie = useCookie('cart-items')
  if (cartCookie.value) {
    cartStore.items = cartCookie.value
  }
})

الناتج:

TEXT
// Execution Successful

(2) ▶ مثال: الفروقات بين Pinia و useState في SSR

البُعد Pinia useState
إماهة SSR ✅ payload تلقائي ✅ payload تلقائي
متعدد Store ✅ مساحات أسماء مستقلة ⚠️ إدارة مفاتيح يدوية
DevTools ✅ تصور ❌ لا يوجد
الاستمرارية ✅ دعم الإضافات ⚠️ يتطلب ملفات تعريف ارتباط يدوية
منطق معقد ✅ actions/getters ❌ ref فقط

6. storeToRefs والعمليات المجمعة

(1) ▶ مثال: تفكيك storeToRefs التفاعلي

VUE
<script setup lang="ts">
const cartStore = useCartStore()

// ✅ تفكيك تفاعلي - يحافظ على التفاعلية
const { items, totalPrice, totalItems } = storeToRefs(cartStore)

// ❌ تفكيك مباشر - يفقد التفاعلية
// const { items, totalPrice } = cartStore

// Actions يمكن تفكيكها مباشرة (لا حاجة للتفاعلية)
const { addItem, removeItem, clearCart } = cartStore
</script>

الناتج:

TEXT
// Execution Successful

(2) ▶ مثال: تحديث مجموعة بـ $patch

TYPESCRIPT
// تحديث مجموعة بـ $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
})

الناتج:

TEXT
// Execution Successful

(3) ▶ مثال: إضافة استمرارية سلة التسوق

TYPESCRIPT
// 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))
    })
  })
})

الناتج:

TEXT
// Execution Successful

7. مثال شامل: نظام سلة MegaShop

VUE
<!-- 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 Store و Composable؟
ج استخدم Store عندما تحتاج مشاركة حالة عبر المكونات، واستخدم Composable لإعادة استخدام المنطق داخل مكون. استخدم Store لسلة التسوق وحالة المستخدم، واستخدم Composable لتنسيق الأسعار.
س لماذا يؤدي تفكيك Store إلى فقدان التفاعلية؟
ج تفكيك Store مباشرة يُرجع نسخة من القيمة، وليس ref. استخدم storeToRefs للتفكيك مع الحفاظ على التفاعلية؛ actions يمكن تفكيكها مباشرة (الدوال لا تتطلب تفاعلية).
س ماذا أفعل إذا كان هناك عدم اتساق في بيانات Store بين العميل والخادم أثناء SSR؟
ج تأكد من عدم تعديل Store قبل onMounted (القيم المعدلة أثناء مرحلة SSR ستُستبدل أثناء الإماهة). إماهة SSR في Pinia تلقائية وعادةً لا تتطلب معالجة يدوية.
س هل $reset في Pinia غير متاح في صياغة Setup؟
ج صياغة Setup لا تتضمن $reset. يمكنك تنفيذ ذلك يدويًا بتعريف إجراء $reset أو إعادة تعيين جميع refs في $patch.
س هل ستؤدي تعديلات Store على صفحات متعددة في وقت واحد إلى تعارضات؟
ج العميل أحادي الخيوط، لذلك لن تكون هناك تعارضات. أثناء SSR، كل طلب له نسخة Pinia مستقلة خاصة به، لذلك لن تكون هناك تعارضات أيضًا.
س كيف يمكن الاحتفاظ ببيانات سلة التسوق بعد تحديث الصفحة؟
ج استخدم إضافة استمرارية (تخزين البيانات في localStorage) أو مزامنتها مع ملف تعريف ارتباط. يوصي MegaShop باستخدام ملفات تعريف الارتباط للمزامنة، لأنها يمكن قراءتها حتى أثناء تصيير جانب الخادم (SSR).

📖ملخص


📝تمارين

  1. تمرين أساسي (الصعوبة: ⭐): أنشئ useCartStore، نفّذ addItem و removeItem، تعامل مع سلة التسوق على صفحتين مختلفتين، وتحقق من مشاركة الحالة.
  2. تمرين متقدم (الصعوبة: ⭐⭐): استخدم صياغة Setup لتعريف useUserStore، نفّذ login و logout و fetchProfile، وتحقق من عمل إماهة SSR بشكل صحيح.
  3. تحدٍ (الصعوبة: ⭐⭐⭐): نفّذ استمرارية سلة التسوق (بحيث تُحتفظ البيانات بعد تحديث الصفحة)، وقارن إيجابيات وسلبيات استخدام إضافة localStorage مقابل ملفات تعريف الارتباط للمزامنة.

---|

Web-Tutorial.com

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

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

100%