إطار Next.js

بناء تطبيق Next.js كامل - الجزء الثاني

20 دقيقة الدرس 40 من 80

بناء تطبيق Next.js كامل - الجزء الثاني

مرحباً بك في الدرس الأخير! في هذا الدرس الشامل، سنكمل تطبيق المدونة الخاص بنا بميزات متقدمة، تنفيذ الاختبارات، التحضير للنشر في بيئة الإنتاج، ومراجعة كل ما تعلمته في سلسلة الدروس هذه.

إضافة ميزات متقدمة

دعنا نحسن تطبيقنا بميزات احترافية تحسن تجربة المستخدم والوظائف.

1. وظيفة البحث

// app/search/page.tsx import { Suspense } from 'react'; import { Metadata } from 'next'; import SearchResults from '@/components/SearchResults'; import SearchForm from '@/components/SearchForm'; export const metadata: Metadata = { title: 'البحث في المقالات', description: 'ابحث في مقالات مدونتنا', }; export default function SearchPage({ searchParams, }: { searchParams: { q?: string }; }) { const query = searchParams.q || ''; return ( <div className="container"> <h1>البحث في المقالات</h1> <SearchForm initialQuery={query} /> <Suspense fallback={<div>جاري البحث...</div>}> <SearchResults query={query} /> </Suspense> </div> ); }
// components/SearchForm.tsx 'use client'; import { useState, useTransition } from 'react'; import { useRouter } from 'next/navigation'; export default function SearchForm({ initialQuery = '', }: { initialQuery?: string; }) { const [query, setQuery] = useState(initialQuery); const [isPending, startTransition] = useTransition(); const router = useRouter(); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); startTransition(() => { router.push(`/search?q=${encodeURIComponent(query)}`); }); }; return ( <form onSubmit={handleSubmit} className="search-form"> <input type="search" value={query} onChange={(e) => setQuery(e.target.value)} placeholder="ابحث في المقالات..." disabled={isPending} /> <button type="submit" disabled={isPending || !query.trim()}> {isPending ? 'جاري البحث...' : 'بحث'} </button> </form> ); }
// components/SearchResults.tsx import { getPosts } from '@/lib/blog'; import PostCard from './PostCard'; export default async function SearchResults({ query, }: { query: string; }) { if (!query.trim()) { return <p>أدخل كلمة بحث للعثور على المقالات.</p>; } const posts = await getPosts(); const filteredPosts = posts.filter(post => post.title.toLowerCase().includes(query.toLowerCase()) || post.excerpt.toLowerCase().includes(query.toLowerCase()) || post.tags.some(tag => tag.toLowerCase().includes(query.toLowerCase())) ); if (filteredPosts.length === 0) { return <p>لم يتم العثور على مقالات لـ "{query}"</p>; } return ( <div className="search-results"> <h2>{filteredPosts.length} نتيجة لـ "{query}"</h2> <div className="posts-grid"> {filteredPosts.map(post => ( <PostCard key={post.slug} post={post} /> ))} </div> </div> ); }

2. مؤشر تقدم القراءة

// components/ReadingProgress.tsx 'use client'; import { useEffect, useState } from 'react'; export default function ReadingProgress() { const [progress, setProgress] = useState(0); useEffect(() => { const updateProgress = () => { const scrollTop = window.scrollY; const docHeight = document.documentElement.scrollHeight - window.innerHeight; const scrollPercent = (scrollTop / docHeight) * 100; setProgress(scrollPercent); }; window.addEventListener('scroll', updateProgress); return () => window.removeEventListener('scroll', updateProgress); }, []); return ( <div className="reading-progress" style={{ position: 'fixed', top: 0, left: 0, width: `${progress}%`, height: '3px', backgroundColor: 'var(--primary-color)', zIndex: 9999, transition: 'width 0.1s ease-out', }} /> ); }

3. جدول المحتويات

// components/TableOfContents.tsx 'use client'; import { useEffect, useState } from 'react'; interface Heading { id: string; text: string; level: number; } export default function TableOfContents() { const [headings, setHeadings] = useState<Heading[]>([]); const [activeId, setActiveId] = useState<string>(''); useEffect(() => { const elements = Array.from( document.querySelectorAll('article h2, article h3') ); const headingsData = elements.map(elem => ({ id: elem.id, text: elem.textContent || '', level: Number(elem.tagName.charAt(1)), })); setHeadings(headingsData); // Intersection Observer للعنوان النشط const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { setActiveId(entry.target.id); } }); }, { rootMargin: '-100px 0px -80% 0px' } ); elements.forEach((elem) => observer.observe(elem)); return () => observer.disconnect(); }, []); if (headings.length === 0) return null; return ( <nav className="table-of-contents"> <h3>جدول المحتويات</h3> <ul> {headings.map((heading) => ( <li key={heading.id} style={{ marginLeft: `${(heading.level - 2) * 1}rem` }} > <a href={`#${heading.id}`} className={activeId === heading.id ? 'active' : ''} onClick={(e) => { e.preventDefault(); document.getElementById(heading.id)?.scrollIntoView({ behavior: 'smooth', }); }} > {heading.text} </a> </li> ))} </ul> </nav> ); }
ملاحظة: هذه الميزات تحسن تجربة المستخدم بشكل كبير. شريط تقدم القراءة يوفر تعليقات بصرية، جدول المحتويات يحسن التنقل، والبحث يساعد المستخدمين في العثور على المحتوى ذي الصلة بسرعة.

اختبار تطبيق Next.js الخاص بك

تنفيذ الاختبارات الشاملة يضمن عمل تطبيقك بشكل صحيح ويمنع التراجع في الوظائف.

1. اختبار الوحدات مع Jest

// تثبيت التبعيات npm install --save-dev jest @testing-library/react @testing-library/jest-dom jest-environment-jsdom // jest.config.js const nextJest = require('next/jest'); const createJestConfig = nextJest({ dir: './', }); const customJestConfig = { setupFilesAfterEnv: ['<rootDir>/jest.setup.js'], testEnvironment: 'jest-environment-jsdom', moduleNameMapper: { '^@/(.*)$': '<rootDir>/$1', }, }; module.exports = createJestConfig(customJestConfig);
// jest.setup.js import '@testing-library/jest-dom'; // __tests__/components/PostCard.test.tsx import { render, screen } from '@testing-library/react'; import PostCard from '@/components/PostCard'; describe('PostCard', () => { const mockPost = { slug: 'test-post', title: 'مقالة تجريبية', date: '2024-01-01', excerpt: 'مقتطف تجريبي', image: '/test.jpg', author: 'أحمد محمد', tags: ['اختبار'], }; it('يعرض معلومات المقالة بشكل صحيح', () => { render(<PostCard post={mockPost} />); expect(screen.getByText('مقالة تجريبية')).toBeInTheDocument(); expect(screen.getByText('مقتطف تجريبي')).toBeInTheDocument(); expect(screen.getByText('أحمد محمد')).toBeInTheDocument(); }); it('يربط بعنوان URL الصحيح للمقالة', () => { render(<PostCard post={mockPost} />); const link = screen.getByRole('link'); expect(link).toHaveAttribute('href', '/blog/test-post'); }); });

2. الاختبار الشامل مع Playwright

// تثبيت Playwright npm init playwright@latest // playwright.config.ts import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ testDir: './e2e', fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 1 : undefined, reporter: 'html', use: { baseURL: 'http://localhost:3000', trace: 'on-first-retry', }, projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] }, }, ], webServer: { command: 'npm run dev', url: 'http://localhost:3000', reuseExistingServer: !process.env.CI, }, });
// e2e/blog.spec.ts import { test, expect } from '@playwright/test'; test.describe('وظائف المدونة', () => { test('يجب عرض مقالات المدونة على الصفحة الرئيسية', async ({ page }) => { await page.goto('/'); await expect(page.locator('h1')).toContainText('مدونتي'); await expect(page.locator('.post-card')).toHaveCount(3); }); test('يجب الانتقال إلى صفحة تفاصيل المقالة', async ({ page }) => { await page.goto('/'); const firstPost = page.locator('.post-card').first(); const postTitle = await firstPost.locator('h2').textContent(); await firstPost.locator('a').click(); await expect(page.locator('article h1')).toContainText(postTitle!); }); test('يجب البحث عن المقالات', async ({ page }) => { await page.goto('/search'); await page.fill('input[type="search"]', 'Next.js'); await page.click('button[type="submit"]'); await expect(page.locator('.search-results')).toBeVisible(); await expect(page.locator('.post-card')).toHaveCount.greaterThan(0); }); test('يجب إرسال نموذج التعليق', async ({ page }) => { await page.goto('/blog/first-post'); await page.fill('input[name="name"]', 'مستخدم تجريبي'); await page.fill('input[name="email"]', 'test@example.com'); await page.fill('textarea[name="comment"]', 'مقالة رائعة!'); await page.click('button[type="submit"]'); await expect(page.locator('.success-message')).toBeVisible(); }); });
نصيحة: قم بتشغيل الاختبارات قبل النشر: npm test لـ Jest، npx playwright test للاختبارات الشاملة. قم بإعداد CI/CD لتشغيل الاختبارات تلقائياً على كل commit.

تحسين الإنتاج

1. متغيرات البيئة

// .env.local (التطوير) DATABASE_URL=postgresql://localhost:5432/mydb NEXT_PUBLIC_API_URL=http://localhost:3000/api NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX // .env.production (الإنتاج) DATABASE_URL=postgresql://production-host/mydb NEXT_PUBLIC_API_URL=https://myapp.com/api NEXT_PUBLIC_GA_ID=G-YYYYYYYYYY
تحذير: لا تقم أبداً بإضافة .env.local أو .env.production إلى نظام التحكم في الإصدارات. أضفهما إلى .gitignore. فقط المتغيرات المسبوقة بـ NEXT_PUBLIC_ يتم عرضها في المتصفح.

2. قائمة تحسين الأداء

// next.config.js - تحسينات الإنتاج /** @type {import('next').NextConfig} */ const nextConfig = { // ضغط المخرجات compress: true, // تحسين الصور images: { formats: ['image/avif', 'image/webp'], deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840], imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], }, // محلل الحزم (علق عليه في الإنتاج) // webpack: (config, { isServer }) => { // if (!isServer) { // const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); // config.plugins.push( // new BundleAnalyzerPlugin({ // analyzerMode: 'static', // openAnalyzer: false, // }) // ); // } // return config; // }, // ميزات الإنتاج فقط swcMinify: true, reactStrictMode: true, poweredByHeader: false, // رؤوس الأمان async headers() { return [ { source: '/:path*', headers: [ { key: 'X-DNS-Prefetch-Control', value: 'on', }, { key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload', }, { key: 'X-Content-Type-Options', value: 'nosniff', }, { key: 'X-Frame-Options', value: 'DENY', }, { key: 'Referrer-Policy', value: 'origin-when-cross-origin', }, ], }, ]; }, }; module.exports = nextConfig;

دليل النشر

1. النشر على Vercel (موصى به)

# تثبيت Vercel CLI npm install -g vercel # تسجيل الدخول إلى Vercel vercel login # النشر vercel # النشر للإنتاج vercel --prod

Vercel توفر:

  • HTTPS وشهادات SSL تلقائية
  • CDN عالمية للأصول الثابتة
  • عمليات نشر تلقائية من Git
  • عمليات نشر معاينة لطلبات السحب
  • تحليلات ومراقبة مدمجة

2. النشر على منصات أخرى

# البناء للإنتاج npm run build # بدء خادم الإنتاج npm start # أو التصدير كموقع ثابت (إذا كان قابلاً للتطبيق) npm run build && npx next export
خيارات النشر:
  • Vercel: الأفضل لـ Next.js، بدون تكوين
  • Netlify: بديل جيد، إعداد سهل
  • AWS Amplify: تكامل AWS، قابل للتوسع
  • Docker: استضافة ذاتية، تحكم كامل
  • DigitalOcean App Platform: بسيط وبأسعار معقولة

3. نشر Docker

# Dockerfile FROM node:18-alpine AS deps WORKDIR /app COPY package*.json ./ RUN npm ci --only=production FROM node:18-alpine AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . RUN npm run build FROM node:18-alpine AS runner WORKDIR /app ENV NODE_ENV production RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs COPY --from=builder /app/public ./public COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static USER nextjs EXPOSE 3000 ENV PORT 3000 CMD ["node", "server.js"]
# بناء وتشغيل حاوية Docker docker build -t my-nextjs-app . docker run -p 3000:3000 my-nextjs-app

قائمة تحقق الإنتاج

✅ قائمة تحقق ما قبل النشر: [ ] جميع الاختبارات تعمل (الوحدة + الشاملة) [ ] متغيرات البيئة مكونة [ ] تم تشغيل هجرات قاعدة البيانات [ ] تم إعداد تتبع الأخطاء (Sentry, LogRocket) [ ] تم تكوين التحليلات (Google Analytics, Plausible) [ ] البيانات الوصفية لتحسين محركات البحث مكتملة [ ] تم إنشاء خريطة الموقع [ ] تم تكوين robots.txt [ ] تم تكوين رؤوس الأمان [ ] تم تفعيل HTTPS [ ] تم التحقق من تحسين الصور [ ] تم اختبار الأداء (نقاط Lighthouse >90) [ ] تم اختبار إمكانية الوصول (WCAG 2.1 AA) [ ] تم التحقق من الاستجابة للأجهزة المحمولة [ ] اكتمل الاختبار عبر المتصفحات [ ] استراتيجية النسخ الاحتياطي في مكانها [ ] تم تكوين المراقبة والتنبيهات

المراقبة والصيانة

// lib/monitoring.ts export function reportWebVitals(metric: any) { // إرسال إلى التحليلات if (typeof window.gtag !== 'undefined') { window.gtag('event', metric.name, { value: Math.round(metric.value), event_label: metric.id, non_interaction: true, }); } // تسجيل في وحدة التحكم في التطوير if (process.env.NODE_ENV === 'development') { console.log(metric); } } // app/layout.tsx - أضف هذا التصدير export { reportWebVitals } from '@/lib/monitoring';

مراجعة الدورة: ما تعلمته

تهانينا على إكمال هذه الدورة الشاملة لـ Next.js! لنراجع كل ما أتقنته:

المفاهيم الأساسية (الدروس 1-10)

  • أساسيات Next.js: App Router، التوجيه القائم على الملفات، مكونات الخادم مقابل العميل
  • التوجيه: المسارات الديناميكية، التخطيطات المتداخلة، مجموعات المسارات، المسارات المتوازية
  • جلب البيانات: من جانب الخادم، من جانب العميل، البث المباشر، حدود التعليق
  • التقديم: SSR، SSG، ISR، استراتيجيات التقديم من جانب العميل

الميزات المتقدمة (الدروس 11-20)

  • مسارات API: واجهات RESTful، وظائف بدون خادم، البرمجيات الوسيطة
  • المصادقة: NextAuth.js، إدارة الجلسات، المسارات المحمية
  • تكامل قاعدة البيانات: Prisma ORM، استعلامات قاعدة البيانات، العلاقات
  • تحسين الصور: مكون Next/Image، التحسين التلقائي

التطوير الاحترافي (الدروس 21-30)

  • إدارة الحالة: Context API، Zustand، حالة الخادم
  • النماذج: React Hook Form، التحقق، تحميل الملفات
  • التنسيق: CSS Modules، Tailwind CSS، styled-components
  • الأداء: تقسيم الكود، التحميل الكسول، استراتيجيات التخزين المؤقت

مهارات الإنتاج (الدروس 31-40)

  • تحسين محركات البحث: واجهة البيانات الوصفية، OpenGraph، JSON-LD، إنشاء خريطة الموقع
  • الاختبار: اختبارات الوحدة (Jest)، الاختبارات الشاملة (Playwright)
  • النشر: Vercel، Docker، خطوط CI/CD
  • المراقبة: Web Vitals، تتبع الأخطاء، التحليلات

الخطوات التالية: تابع رحلتك

  1. بناء مشاريع حقيقية: طبق ما تعلمته على تطبيقات حقيقية
  2. استكشاف موضوعات متقدمة: إعادة التوليد الثابت التدريجي، وظائف Edge، أنماط البرمجيات الوسيطة
  3. انضم إلى المجتمع: GitHub، Discord، Stack Overflow، Reddit r/nextjs
  4. ابق محدثاً: تابع مدونة Next.js، ملاحظات الإصدار، ومناقشات RFC
  5. ساهم: مساهمات المصادر المفتوحة، اكتب مقالات المدونة، أنشئ دروساً

الموارد الموصى بها

  • الوثائق الرسمية: nextjs.org/docs - دائماً محدثة
  • أمثلة Next.js: github.com/vercel/next.js/tree/canary/examples
  • منصة التعلم: nextjs.org/learn - دروس تفاعلية
  • قنوات YouTube: Vercel، Lee Robinson، Fireship
  • دورات: Frontend Masters، Egghead.io، Udemy
  • كتب: "The Next.js Handbook"، "React and Next.js"
تحدي المشروع النهائي:
  1. أكمل تطبيق المدونة بجميع الميزات من الدروس 39-40
  2. أضف 3 ميزات مخصصة على الأقل (مثل الإشارات المرجعية، الفئات، المقالات ذات الصلة)
  3. نفذ اختبارات شاملة (تغطية الكود أكثر من 80%)
  4. انشر للإنتاج بنطاق مخصص
  5. احصل على نقاط Lighthouse: الأداء 90+، إمكانية الوصول 100، أفضل الممارسات 100، تحسين محركات البحث 100
  6. وثق الكود الخاص بك واكتب README
  7. شارك مشروعك على GitHub ووسائل التواصل الاجتماعي
نصائح نهائية للنجاح:
  • اقرأ دائماً رسائل الخطأ بعناية - عادة ما تخبرك بالضبط بما هو خطأ
  • استخدم TypeScript لتحسين جودة الكود وتجربة المطور
  • قم بالتحليل قبل التحسين - لا تخمن ما هو بطيء
  • اكتب اختبارات للوظائف الحرجة
  • حافظ على تحديث التبعيات وراقب الثغرات الأمنية
  • تعلم من كود الآخرين - اقرأ مشاريع Next.js مفتوحة المصدر الشائعة
  • لا تبالغ في الهندسة - ابدأ بسيطاً وأضف التعقيد حسب الحاجة

شكراً لك!

شكراً لك على إكمال هذه الدورة الشاملة لـ Next.js! أنت الآن تمتلك المهارات لبناء تطبيقات ويب حديثة وجاهزة للإنتاج باستخدام Next.js. تذكر أن التعلم رحلة مستمرة - استمر في الممارسة والبناء ومشاركة معرفتك مع الآخرين.

مجتمع تطوير الويب مرحب وداعم. لا تتردد في طرح الأسئلة ومشاركة عملك ومساعدة الآخرين في رحلتهم. حظاً سعيداً مع مشاريع Next.js الخاصة بك، وبرمجة سعيدة!

ملاحظة نهائية: غطت هذه الدورة Next.js 14 مع App Router. يتطور Next.js بسرعة، لذا ارجع دائماً إلى الوثائق الرسمية للحصول على أحدث الميزات وأفضل الممارسات. الأساسيات التي تعلمتها هنا ستبقى ذات صلة عبر الإصدارات.