إطار Next.js

التخطيطات والقوالب

30 دقيقة الدرس 5 من 40

فهم التخطيطات في Next.js

التخطيطات واحدة من أقوى ميزات App Router في Next.js. تتيح لك إنشاء بنى واجهة مستخدم قابلة لإعادة الاستخدام تلف حول صفحاتك، مع الحفاظ على الحالة وتجنب إعادة العرض غير الضرورية عند التنقل بين الصفحات. هذا أمر حاسم لبناء تطبيقات فعالة وقابلة للصيانة مع واجهات مستخدم متسقة.

تحل التخطيطات المشاكل الشائعة في تطوير الويب:

  • تجنب تكرار الكود للرؤوس والتذييلات والتنقل عبر الصفحات
  • الحفاظ على حالة المكون عند التنقل (مثل موضع التمرير، مدخلات النموذج)
  • إنشاء تسلسلات هرمية متداخلة للتخطيط للتطبيقات المعقدة
  • تحسين الأداء بمنع إعادة العرض غير الضرورية
مفهوم أساسي: في App Router، لا يتم إعادة عرض التخطيطات عند التنقل بين الصفحات التي تشترك في نفس التخطيط. هذا تحسين كبير في الأداء مقارنة بتنفيذ التخطيطات يدويًا في Pages Router.

التخطيط الجذري - الأساس

كل تطبيق Next.js App Router يجب أن يحتوي على تخطيط جذري. هذا هو التخطيط ذو المستوى الأعلى الذي يلف تطبيقك بالكامل وهو مطلوب في مجلد app.

إنشاء التخطيط الجذري

// app/layout.js
export default function RootLayout({ children }) {
  return (
    <html lang="ar">
      <body>
        {/* الرأس يظهر في جميع الصفحات */}
        <header>
          <nav>
            <a href="/">الرئيسية</a>
            <a href="/about">من نحن</a>
            <a href="/blog">المدونة</a>
          </nav>
        </header>

        {/* محتوى الصفحة يعرض هنا */}
        <main>{children}</main>

        {/* التذييل يظهر في جميع الصفحات */}
        <footer>
          <p>&copy; 2024 موقعي</p>
        </footer>
      </body>
    </html>
  )
}
مهم: يجب أن يتضمن التخطيط الجذري وسوم <html> و<body>. هذه هي الأماكن الوحيدة في تطبيقك التي يجب أن تتضمن فيها هذه الوسوم. سيتولى Next.js بقية بنية HTML تلقائيًا.

متطلبات التخطيط الجذري

  • يجب تعريفه في app/layout.js أو app/layout.tsx
  • يجب أن يقبل خاصية children
  • يجب أن يُرجع وسوم <html> و<body>
  • لا يمكن أن يكون مكون عميل (بدون توجيه 'use client')

التخطيطات المتداخلة

واحدة من أقوى ميزات App Router هي القدرة على إنشاء تخطيطات متداخلة. كل جزء من المسار يمكن أن يحدد تخطيطه الخاص الذي يلف محتوى ذلك الجزء وجميع أبنائه.

إنشاء تخطيطات متداخلة

app/
├── layout.js          # التخطيط الجذري (يلف كل شيء)
├── page.js            # الصفحة الرئيسية
├── blog/
│   ├── layout.js      # تخطيط المدونة (يلف جميع صفحات المدونة)
│   ├── page.js        # فهرس المدونة (/blog)
│   └── [slug]/
│       └── page.js    # مقالة المدونة (/blog/:slug)
└── dashboard/
    ├── layout.js      # تخطيط لوحة التحكم (يلف جميع صفحات لوحة التحكم)
    ├── page.js        # الصفحة الرئيسية للوحة التحكم (/dashboard)
    └── settings/
        └── page.js    # إعدادات لوحة التحكم (/dashboard/settings)

مثال: تخطيط المدونة

// app/blog/layout.js
export default function BlogLayout({ children }) {
  return (
    <div className="blog-container">
      {/* الشريط الجانبي يظهر في جميع صفحات المدونة */}
      <aside className="blog-sidebar">
        <h3>الفئات</h3>
        <ul>
          <li>التكنولوجيا</li>
          <li>التصميم</li>
          <li>الأعمال</li>
        </ul>
      </aside>

      {/* محتوى المدونة يعرض هنا */}
      <div className="blog-content">
        {children}
      </div>
    </div>
  )
}

الآن عندما ينتقل المستخدمون بين /blog و/blog/my-post، لا يتم إعادة عرض الشريط الجانبي—فقط محتوى الأبناء يتغير. هذا يحافظ على الحالة ويحسن الأداء.

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

مثال على تسلسل التخطيط الهرمي

// مع هذه البنية:
app/
├── layout.js          # الجذر: <html><body><Header/>{children}<Footer/></body></html>
└── dashboard/
    ├── layout.js      # لوحة التحكم: <div><Sidebar/>{children}</div>
    └── settings/
        └── page.js    # محتوى صفحة الإعدادات

// البنية النهائية المعروضة لـ /dashboard/settings هي:
<html>
  <body>
    <Header />                    {/* من التخطيط الجذري */}
    <div>
      <Sidebar />                 {/* من تخطيط لوحة التحكم */}
      <SettingsPage />            {/* الصفحة الفعلية */}
    </div>
    <Footer />                    {/* من التخطيط الجذري */}
  </body>
</html>

القوالب مقابل التخطيطات

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

متى تستخدم القوالب

  • عندما تريد تشغيل رسوم متحركة CSS/JavaScript في كل تنقل
  • عندما تحتاج إلى إعادة تعيين حالة النموذج عند التنقل
  • عندما تريد تتبع مشاهدات الصفحة في كل تغيير مسار
  • عندما تحتاج useEffect إلى التشغيل في كل تنقل

إنشاء قالب

// app/blog/template.js
'use client'

import { useEffect } from 'react'

export default function BlogTemplate({ children }) {
  // هذا التأثير يعمل في كل تنقل داخل /blog
  useEffect(() => {
    console.log('الصفحة تغيرت - تتبع المشاهدة')
    // تتبع مشاهدة الصفحة في التحليلات
  }, [])

  return (
    <div className="animate-fade-in">
      {children}
    </div>
  )
}
الفرق: إذا استخدمت تخطيطًا بدلاً من قالب، فسيتم تشغيل useEffect مرة واحدة فقط عند الدخول لأول مرة إلى قسم /blog، وليس في كل تغيير صفحة بداخله.

مقارنة التخطيط مقابل القالب

الميزة                 | التخطيط         | القالب
-----------------------|-----------------|------------------
الحفاظ على الحالة      | نعم             | لا (يعيد التثبيت)
إعادة العرض عند التنقل | لا              | نعم (دائمًا)
تشغيل useEffect        | مرة واحدة       | كل تنقل
الرسوم المتحركة        | المرة الأولى فقط| كل مرة
الأداء                | أفضل            | أبطأ قليلاً
حالة الاستخدام         | معظم السيناريوهات| احتياجات خاصة

إدارة البيانات الوصفية

يمكن للتخطيطات والصفحات تصدير بيانات وصفية للتحكم في محتوى <head> في HTML مثل العنوان والوصف ووسوم meta. هذا حاسم لتحسين محركات البحث (SEO).

البيانات الوصفية الثابتة

// app/layout.js
export const metadata = {
  title: 'موقعي',
  description: 'مرحبًا بكم في موقعي الرائع',
  keywords: ['nextjs', 'react', 'تطوير الويب'],
}

export default function RootLayout({ children }) {
  return (
    <html lang="ar">
      <body>{children}</body>
    </html>
  )
}

البيانات الوصفية الديناميكية

// app/blog/[slug]/page.js
export async function generateMetadata({ params }) {
  // جلب بيانات المقالة
  const post = await fetch(`https://api.example.com/posts/${params.slug}`)
    .then(res => res.json())

  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [post.coverImage],
    },
  }
}

export default async function BlogPost({ params }) {
  const post = await fetch(`https://api.example.com/posts/${params.slug}`)
    .then(res => res.json())

  return <article>{post.content}</article>
}
نصيحة SEO: البيانات الوصفية المحددة في الصفحات/التخطيطات الفرعية ستدمج وتتجاوز البيانات الوصفية الأصلية. هذا يتيح لك تعيين إعدادات افتراضية في التخطيط الجذري وتخصيصها لصفحات محددة.

أولوية البيانات الوصفية

تدمج البيانات الوصفية من الجذر إلى الورقة، مع أخذ الأجزاء الأقرب الأولوية:

// app/layout.js
export const metadata = {
  title: 'موقعي',
  description: 'وصف افتراضي',
}

// app/blog/layout.js
export const metadata = {
  title: 'المدونة - موقعي',
  // الوصف موروث من الجذر
}

// app/blog/[slug]/page.js
export async function generateMetadata({ params }) {
  return {
    title: `${post.title} - المدونة - موقعي`,
    description: post.excerpt, // يتجاوز وصف الأصل
  }
}

// البيانات الوصفية النهائية لـ /blog/my-post:
// title: "عنوان مقالتي - المدونة - موقعي"
// description: "مقتطف مقالتي" (من الصفحة)

قالب البيانات الوصفية

يمكنك تحديد قالب لتجنب تكرار اسم الموقع في كل عنوان:

// app/layout.js
export const metadata = {
  title: {
    template: '%s | موقعي',
    default: 'موقعي', // يُستخدم عندما لا يتم توفير عنوان
  },
  description: 'مرحبًا بكم في موقعي',
}

// app/blog/page.js
export const metadata = {
  title: 'المدونة',
  // العنوان النهائي: "المدونة | موقعي"
}

// app/blog/[slug]/page.js
export async function generateMetadata({ params }) {
  const post = await fetchPost(params.slug)
  return {
    title: post.title,
    // العنوان النهائي: "عنوان المقالة | موقعي"
  }
}

إدارة Head مع generateMetadata

توفر وظيفة generateMetadata تحكمًا متقدمًا في قسم <head>:

// app/blog/[slug]/page.js
export async function generateMetadata({ params, searchParams }) {
  const post = await fetchPost(params.slug)

  return {
    // البيانات الوصفية الأساسية
    title: post.title,
    description: post.excerpt,
    keywords: post.tags,

    // Open Graph (Facebook، LinkedIn)
    openGraph: {
      title: post.title,
      description: post.excerpt,
      type: 'article',
      url: `https://mywebsite.com/blog/${params.slug}`,
      images: [
        {
          url: post.coverImage,
          width: 1200,
          height: 630,
          alt: post.title,
        },
      ],
      publishedTime: post.publishedAt,
      authors: [post.author],
    },

    // بطاقة Twitter
    twitter: {
      card: 'summary_large_image',
      title: post.title,
      description: post.excerpt,
      images: [post.coverImage],
      creator: '@myhandle',
    },

    // وسوم meta إضافية
    robots: {
      index: true,
      follow: true,
    },

    // لغات بديلة
    alternates: {
      canonical: `https://mywebsite.com/blog/${params.slug}`,
      languages: {
        'en-US': `/en/blog/${params.slug}`,
        'ar-SA': `/ar/blog/${params.slug}`,
      },
    },
  }
}

أفضل ممارسات مكونات التخطيط

1. اجعل التخطيطات مكونات خادم

يجب أن تكون التخطيطات مكونات خادم كلما أمكن لتقليل حجم حزمة JavaScript:

// ✅ جيد - مكون خادم (افتراضي)
export default function Layout({ children }) {
  return (
    <div>
      <StaticHeader />
      {children}
    </div>
  )
}

// ❌ تجنب ما لم يكن ضروريًا
'use client'

export default function Layout({ children }) {
  return (
    <div>
      <InteractiveHeader /> {/* فقط إذا كان الرأس يحتاج تفاعلية */}
      {children}
    </div>
  )
}

2. التركيب مع مكونات العميل

عندما تحتاج إلى التفاعلية، أنشئ مكونات عميل منفصلة واستوردها في تخطيط مكون الخادم الخاص بك:

// app/components/ThemeToggle.js
'use client'

import { useState } from 'react'

export default function ThemeToggle() {
  const [theme, setTheme] = useState('light')
  return <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
    تبديل السمة
  </button>
}

// app/layout.js - مكون خادم
import ThemeToggle from './components/ThemeToggle'

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <header>
          <nav>...</nav>
          <ThemeToggle /> {/* مكون عميل */}
        </header>
        {children}
      </body>
    </html>
  )
}

3. تجنب تمرير الخصائص مع Context

للحالة المشتركة عبر التخطيطات، استخدم React Context في مكون عميل:

// app/providers/ThemeProvider.js
'use client'

import { createContext, useState } from 'react'

export const ThemeContext = createContext()

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light')

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  )
}

// app/layout.js
import { ThemeProvider } from './providers/ThemeProvider'

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <ThemeProvider>
          {children}
        </ThemeProvider>
      </body>
    </html>
  )
}

أنماط التخطيط الشائعة

1. تخطيط لوحة التحكم مع شريط جانبي

// app/dashboard/layout.js
import Sidebar from '@/components/Sidebar'

export default function DashboardLayout({ children }) {
  return (
    <div className="dashboard">
      <Sidebar />
      <main className="dashboard-content">
        {children}
      </main>
    </div>
  )
}

2. تخطيط متعدد الأعمدة

// app/blog/layout.js
export default function BlogLayout({ children }) {
  return (
    <div className="container">
      <aside className="sidebar-left">
        {/* الفئات، الوسوم، إلخ. */}
      </aside>

      <main className="content">
        {children}
      </main>

      <aside className="sidebar-right">
        {/* الإعلانات، المقالات الشائعة، إلخ. */}
      </aside>
    </div>
  )
}

3. تخطيط شرطي

// app/dashboard/layout.js
import { headers } from 'next/headers'

export default function DashboardLayout({ children }) {
  const headersList = headers()
  const userAgent = headersList.get('user-agent')
  const isMobile = /mobile/i.test(userAgent)

  return (
    <div className={isMobile ? 'mobile-layout' : 'desktop-layout'}>
      {!isMobile && <Sidebar />}
      <main>{children}</main>
    </div>
  )
}
تمارين عملية:
  1. أنشئ تخطيطًا جذريًا مع رأس وتذييل ومنطقة محتوى رئيسية
  2. ابنِ تخطيط لوحة تحكم متداخل مع شريط جانبي يظهر فقط في قسم /dashboard
  3. نفذ تخطيط مدونة مع شريط جانبي يستمر عبر جميع صفحات المدونة
  4. أنشئ قالبًا يشغل رسمًا متحركًا للتلاشي في كل تنقل صفحة
  5. أعد البيانات الوصفية لصفحة مقالة مدونة تتضمن وسوم Open Graph وTwitter Card
  6. نفذ قالب بيانات وصفية بحيث تتضمن جميع الصفحات " | اسم موقعك" في عناوينها
  7. أنشئ تخطيطًا يعرض بشكل شرطي تنقلًا مختلفًا بناءً على حالة مصادقة المستخدم
  8. ابنِ تخطيطًا متعدد اللغات يضبط سمة HTML lang بناءً على اللغة الحالية
  9. نفذ Context Provider في التخطيط الجذري لتبديل السمة عبر التطبيق بأكمله

الخلاصة

التخطيطات والقوالب هي لبنات بناء أساسية في تطبيقات Next.js App Router. فهم كيفية استخدامها بفعالية أمر حاسم لبناء تطبيقات عالية الأداء وقابلة للصيانة:

  • التخطيطات تحافظ على الحالة وتتجنب إعادة العرض أثناء التنقل، مثالية لعناصر واجهة المستخدم الدائمة
  • القوالب تعيد التثبيت في كل تنقل، مفيدة للرسوم المتحركة والتتبع
  • التخطيطات المتداخلة تتيح تسلسلات هرمية معقدة لواجهة المستخدم مع أداء مثالي
  • إدارة البيانات الوصفية في التخطيطات والصفحات تتحكم في عناصر الرأس الحاسمة لتحسين محركات البحث
  • مكونات الخادم يجب أن تكون الافتراضية للتخطيطات لتقليل JavaScript
  • مكونات العميل يمكن تركيبها ضمن تخطيطات مكونات الخادم عند الحاجة إلى التفاعلية

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