404 Not Found

404 Not Found


nginx

تطوير المشروع

بمجرد اكتمال التصميم، قاد Charlie الفريق للبدء في كتابة الكود. بدأت وحدة المصادقة أولاً—لا دخول، لا وصول. ثم جاءت وحدة المنتجات—عمليات CRUD وتخزين ISR المؤقت لملايين المنتجات. وتبعتها وحدتا سلة التسوق والطلبات. اختبر Alice تدفق العميل، بينما تحقق Bob من الوظائف الإدارية؛ خضعت كل وحدة لاختبار شامل.

1. ما ستتعلمه


2. قصة حقيقية عن فريق

(1) نقطة الألم: خطط التصميم لا يمكن تنفيذها

لدى Charlie مجموعة كاملة من وثائق التصميم، لكن الفريق لا يعرف من أين يبدأ وماذا يفعل بعد ذلك. كتب Alice صفحات المنتجات، لكن سلة التسوق لا تملك واجهة API بعد؛ كتب Bob لوحة الإدارة، لكن المصادقة لم تُعد بعد. الوحدات تنتظر بعضها البعض، وتعطل التقدم.

(2) حل للتطوير التدريجي حسب الوحدات

طوّر كل وحدة بترتيب التبعية: المصادقة → المنتجات → سلة التسوق → الطلبات → التدويل. اختبر كل وحدة فور اكتمالها، دون الاعتماد على وحدات لم تُنتهى بعد.

(3) النتائج: تقدم ثابت

تستغرق كل وحدة 3-5 أيام للإكمال، وستُطلق جميع الميزات الأساسية خلال أسبوعين، وعندها ستتمكن Alice من إكمال عملية التسوق بالكامل.


3. معالم التطوير

(1) مخطط جانت لتطوير الوحدات

100%
gantt
    title معالم تطوير MegaShop
    dateFormat YYYY-MM-DD
    section وحدة المصادقة
    JWT + OAuth2 :a1, 2026-01-01, 3d
    وسيط المصادقة :a2, after a1, 2d
    section وحدة المنتجات
    CRUD API :p1, after a2, 3d
    بحث + تصفية :p2, after p1, 2d
    تخزين ISR مؤقت :p3, after p2, 1d
    section وحدة السلة
    Store + API :c1, after a2, 3d
    Composable :c2, after c1, 1d
    section وحدة الطلبات
    معاملة Prisma :o1, after c1, 3d
    دفع Stripe :o2, after o1, 2d
    section i18n + أداء
    ثلاث لغات :i1, after p3, 2d
    تحسين الصور :i2, after i1, 1d
    تدقيق Lighthouse :i3, after i2, 1d

4. وحدة المصادقة

(1) ▶ مثال: أداة JWT + واجهة API للتسجيل/تسجيل الدخول

TYPESCRIPT
// server/utils/jwt.ts
import jwt from 'jsonwebtoken'

const config = useRuntimeConfig()

export function signTokenPair(payload: { userId: number; email: string; role: string }) {
  return {
    accessToken: jwt.sign(payload, config.jwtAccessSecret, { expiresIn: '15m' }),
    refreshToken: jwt.sign(payload, config.jwtRefreshSecret, { expiresIn: '7d' })
  }
}

export function setAuthCookies(event: any, tokens: { accessToken: string; refreshToken: string }) {
  setCookie(event, 'access-token', tokens.accessToken, { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 900 })
  setCookie(event, 'refresh-token', tokens.refreshToken, { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 604800 })
}

الناتج:

TEXT
// التنفيذ ناجح
TYPESCRIPT
// server/api/auth/register.post.ts
export default defineEventHandler(async (event) => {
  const { email, password, name } = await readBody(event)
  const existing = await prisma.user.findUnique({ where: { email } })
  if (existing) throw createError({ statusCode: 409, message: 'Email already registered' })

  const hashedPassword = await hashPassword(password)
  const user = await prisma.user.create({
    data: { email, name, password: hashedPassword, role: 'CUSTOMER' }
  })

  const tokens = signTokenPair({ userId: user.id, email: user.email, role: user.role })
  setAuthCookies(event, tokens)

  return { user: { id: user.id, email: user.email, name: user.name, role: user.role } }
})

5. وحدة المنتجات

(1) ▶ مثال: واجهة CRUD API للمنتجات (Prisma + تخزين مؤقت)

TYPESCRIPT
// server/api/products/index.get.ts
export default cachedEventHandler(
  async (event) => {
    const { page = '1', limit = '20', category, search, sort } = getQuery(event)

    const where = {
      AND: [
        category ? { category: { slug: category as string } } : {},
        search ? { OR: [
          { name: { contains: search as string, mode: 'insensitive' } },
          { description: { contains: search as string, mode: 'insensitive' } }
        ]} : {}
      ]
    }

    const orderBy = sort === 'price-asc' ? { price: 'asc' }
      : sort === 'price-desc' ? { price: 'desc' }
      : { createdAt: 'desc' }

    const [items, total] = await Promise.all([
      prisma.product.findMany({
        where, orderBy,
        skip: (Number(page) - 1) * Number(limit),
        take: Number(limit),
        include: { category: { select: { name: true, slug: true } } }
      }),
      prisma.product.count({ where })
    ])

    return { items, total, page: Number(page), limit: Number(limit) }
  },
  { maxAge: 60, swr: true, getKey: (event) => `products:${getRequestURL(event).search}` }
)

الناتج:

TEXT
// التنفيذ ناجح

(2) ▶ مثال: تفاصيل المنتج + SEO

VUE
<!-- pages/products/[id].vue -->
<template>
  <div v-if="product">
    <h1>{{ product.name }}</h1>
    <p>{{ formatFromUSD(product.price) }}</p>
    <button @click="handleAddToCart">{{ t('product.addToCart') }}</button>
  </div>
</template>

<script setup lang="ts">
const route = useRoute()
const config = useRuntimeConfig()
const { t } = useI18n()
const { formatFromUSD } = useLocalizedPrice()
const { addToCart } = useCart()

const { data: product } = await useFetch(`/api/products/${route.params.id}`)

// SEO ديناميكي لكل منتج
useSeoMeta({
  title: () => `${product.value?.name} - MegaShop`,
  description: () => `Buy ${product.value?.name} - $${product.value?.price} USD`,
  ogTitle: () => product.value?.name,
  ogImage: () => product.value?.image,
  ogType: 'product'
})

// بيانات منظمة JSON-LD
useHead({ script: [{ type: 'application/ld+json', innerHTML: computed(() => JSON.stringify({
  '@context': 'https://schema.org', '@type': 'Product',
  name: product.value?.name,
  offers: { '@type': 'Offer', priceCurrency: 'USD', price: product.value?.price }
}))}]})

async function handleAddToCart() {
  if (product.value) await addToCart(product.value.id, product.value.name)
}
</script>

الناتج:

TEXT
// التنفيذ ناجح

6. وحدة سلة التسوق

(1) ▶ مثال: cartStore + useCart

TYPESCRIPT
// stores/cart.ts
export const useCartStore = defineStore('cart', () => {
  const items = ref<CartItem[]>([])
  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))

  async function addItem(productId: number) {
    const cart = await $fetch('/api/cart/add', { method: 'POST', body: { productId } })
    items.value = cart.items
  }
  async function removeItem(productId: number) {
    const cart = await $fetch('/api/cart/remove', { method: 'DELETE', body: { productId } })
    items.value = cart.items
  }
  async function fetchCart() {
    const cart = await $fetch('/api/cart')
    items.value = cart.items || []
  }

  return { items, totalItems, totalPrice, addItem, removeItem, fetchCart }
})

الناتج:

TEXT
// التنفيذ ناجح

(2) ▶ مثال: واجهة API لسلة التسوق (مع التحقق من المخزون)

TYPESCRIPT
// server/api/cart/add.post.ts
export default defineEventHandler(async (event) => {
  const userId = event.context.user?.id
  const { productId, quantity = 1 } = await readBody(event)

  // التحقق من المخزون
  const product = await prisma.product.findUnique({ where: { id: productId } })
  if (!product?.inStock) throw createError({ statusCode: 400, message: 'Product out of stock' })

  // Upsert عنصر السلة
  await prisma.cartItem.upsert({
    where: { userId_productId: { userId, productId } },
    update: { quantity: { increment: quantity } },
    create: { userId, productId, quantity }
  })

  const cart = await prisma.cartItem.findMany({
    where: { userId },
    include: { product: { select: { name: true, price: true, image: true } } }
  })

  const total = cart.reduce((s, i) => s + Number(i.product.price) * i.quantity, 0)
  return { items: cart, total }
})

الناتج:

TEXT
// التنفيذ ناجح

7. وحدة الطلبات

(1) ▶ مثال: معاملة إنشاء الطلب

TYPESCRIPT
// server/api/orders/index.post.ts
export default defineEventHandler(async (event) => {
  const userId = event.context.user?.id
  if (!userId) throw createError({ statusCode: 401, message: 'Login required' })

  const order = await prisma.$transaction(async (tx) => {
    // جلب عناصر السلة
    const cartItems = await tx.cartItem.findMany({
      where: { userId },
      include: { product: true }
    })

    if (cartItems.length === 0) {
      throw createError({ statusCode: 400, message: 'Cart is empty' })
    }

    // التحقق من المخزون لجميع العناصر
    for (const item of cartItems) {
      if (!item.product.inStock) {
        throw createError({ statusCode: 400, message: `${item.product.name} out of stock` })
      }
    }

    // حساب الإجمالي
    const total = cartItems.reduce((sum, item) => sum + Number(item.product.price) * item.quantity, 0)

    // إنشاء الطلب مع العناصر
    const newOrder = await tx.order.create({
      data: {
        userId,
        total,
        status: 'PENDING',
        items: {
          create: cartItems.map(item => ({
            productId: item.productId,
            quantity: item.quantity,
            price: item.product.price
          }))
        }
      },
      include: { items: { include: { product: true } } }
    })

    // مسح السلة
    await tx.cartItem.deleteMany({ where: { userId } })

    return newOrder
  })

  return { order, message: 'Order created' }
})

الناتج:

TEXT
// التنفيذ ناجح

8. التدويل والأداء

(1) ▶ مثال: تكوين إنتاج كامل في nuxt.config.ts

TYPESCRIPT
// nuxt.config.ts
export default defineNuxtConfig({
  ssr: true,

  modules: [
    '@pinia/nuxt',
    '@nuxtjs/tailwindcss',
    '@nuxtjs/i18n',
    '@nuxtjs/sitemap',
    '@nuxt/image',
    '~/modules/analytics'
  ],

  app: {
    head: {
      title: 'MegaShop',
      titleTemplate: '%s | MegaShop',
      meta: [
        { name: 'description', content: 'Over 1 million products shipped worldwide' }
      ]
    }
  },

  i18n: {
    locales: [
      { code: 'en', name: 'English', file: 'en.json', currency: 'USD' },
      { code: 'zh', name: 'Chinese', file: 'zh.json', currency: 'CNY' },
      { code: 'ja', name: 'Japanese', file: 'ja.json', currency: 'JPY' }
    ],
    defaultLocale: 'en',
    lazy: true,
    langDir: 'locales/',
    strategy: 'prefix_except_default'
  },

  image: {
    quality: 80,
    format: ['webp', 'avif']
  },

  sitemap: {
    hostname: 'https://megashop.com',
    gzip: true
  },

  runtimeConfig: {
    databaseUrl: process.env.DATABASE_URL,
    redisUrl: process.env.REDIS_URL,
    jwtAccessSecret: process.env.JWT_ACCESS_SECRET,
    jwtRefreshSecret: process.env.JWT_REFRESH_SECRET,
    public: {
      apiBase: process.env.API_BASE || '/api',
      siteUrl: process.env.SITE_URL || 'https://megashop.com',
      stripePublishableKey: process.env.STRIPE_PUBLISHABLE_KEY
    }
  },

  routeRules: {
    '/': { prerender: true },
    '/about': { prerender: true },
    '/products': { swr: 3600 },
    '/products/**': { swr: 86400 },
    '/categories/**': { swr: 21600 },
    '/admin/**': { ssr: false },
    '/cart': { ssr: false },
    '/checkout': { ssr: false },
    '/profile/**': { ssr: false },
    '/api/**': { cors: true },
    '/_nuxt/**': { headers: { 'cache-control': 'public, max-age=31536000, immutable' } }
  },

  nitro: {
    preset: process.env.DEPLOY_TARGET || 'node-server',
    storage: {
      products: { driver: process.env.NODE_ENV === 'production' ? 'redis' : 'memory' }
    }
  },

  components: [{ path: '~/components', pathPrefix: false }]
})

الناتج:

TEXT
// التنفيذ ناجح

❓ أسئلة شائعة

س هل ترتيب تطوير الوحدات مهم؟
ج مهم جدًا. يجب تنفيذ المصادقة أولاً (لأن الوحدات الأخرى تعتمد على حالة تسجيل الدخول)، ثم المنتجات (لأن سلة التسوق والطلبات تعتمد على بيانات المنتجات)، وأخيرًا التدويل (لأنه لا يؤثر على الوظائف الأساسية).
س كم كود تقريبًا تحتاجه كل وحدة؟
ج وحدة المصادقة ~500 سطر، وحدة المنتجات ~800 سطر، سلة التسوق ~400 سطر، الطلبات ~500 سطر، التدويل ~200 سطر. بما في ذلك التكوين والاختبارات، الإجمالي حوالي 3000-4000 سطر.
س كيف نضمن عقود API بين الوحدات؟
ج استخدم واجهات TypeScript لتعريف أنواع طلب واستجابة API، وضعها في shared/types.ts. الواجهة الأمامية والخلفية تشترك في تعريفات الأنواع هذه، ويتم التحقق من الاتساق في وقت التجميع.
س كيف أنتقل من البيانات الوهمية إلى Prisma أثناء التطوير؟
ج حافظ على تنسيق واجهة API دون تغيير؛ فقط استبدل التنفيذ الداخلي. حوّل من مصفوفة وهمية إلى prisma.findMany()؛ يبقى المسار وتنسيقات الاستجابة كما هي.
س كيف أختبر مدفوعات Stripe؟
ج استخدم مفتاح اختبار Stripe ورقم بطاقة اختبار (4242 4242 4242 4242). لن تُعالج أي رسوم فعلية أبدًا. حوّل إلى مفتاح الإنتاج قبل الإطلاق.
س متى يجب إجراء ترجمة i18n؟
ج يجب إجراء i18n بعد اكتمال الميزات الأساسية. أولاً، رمّز النص الإنجليزي بشكل ثابت؛ بمجرد استقرار الميزات، استخرجها كمفاتيح ترجمة. بدء i18n مبكرًا سيزيد من تعقيد التطوير.

📖 ملخص


📝 تمارين

  1. تمرين أساسي (الصعوبة: ⭐): تنفيذ وحدة مصادقة (تسجيل/تسجيل دخول/تحديث token)، والتحقق من أن ملف تعريف الارتباط معيّن بشكل صحيح
  2. تمرين متقدم (الصعوبة: ⭐⭐): تنفيذ سير عمل CRUD كامل للمنتجات، بما في ذلك سلة التسوق والدفع، كتطبيق شامل من النهاية إلى النهاية.
  3. تحدي (الصعوبة: ⭐⭐⭐): إكمال جميع الوحدات الأساسية لـ MegaShop + i18n + تحسين الأداء، مع تحقيق درجة Lighthouse تبلغ 90+

---|

Web-Tutorial.com

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

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

100%