التخطيطات والقوالب
فهم التخطيطات في Next.js
التخطيطات واحدة من أقوى ميزات App Router في Next.js. تتيح لك إنشاء بنى واجهة مستخدم قابلة لإعادة الاستخدام تلف حول صفحاتك، مع الحفاظ على الحالة وتجنب إعادة العرض غير الضرورية عند التنقل بين الصفحات. هذا أمر حاسم لبناء تطبيقات فعالة وقابلة للصيانة مع واجهات مستخدم متسقة.
تحل التخطيطات المشاكل الشائعة في تطوير الويب:
- تجنب تكرار الكود للرؤوس والتذييلات والتنقل عبر الصفحات
- الحفاظ على حالة المكون عند التنقل (مثل موضع التمرير، مدخلات النموذج)
- إنشاء تسلسلات هرمية متداخلة للتخطيط للتطبيقات المعقدة
- تحسين الأداء بمنع إعادة العرض غير الضرورية
التخطيط الجذري - الأساس
كل تطبيق 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>© 2024 موقعي</p>
</footer>
</body>
</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 | مرة واحدة | كل تنقل الرسوم المتحركة | المرة الأولى فقط| كل مرة الأداء | أفضل | أبطأ قليلاً حالة الاستخدام | معظم السيناريوهات| احتياجات خاصة
إدارة البيانات الوصفية
يمكن للتخطيطات والصفحات تصدير بيانات وصفية للتحكم في محتوى <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>
}
أولوية البيانات الوصفية
تدمج البيانات الوصفية من الجذر إلى الورقة، مع أخذ الأجزاء الأقرب الأولوية:
// 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>
)
}
- أنشئ تخطيطًا جذريًا مع رأس وتذييل ومنطقة محتوى رئيسية
- ابنِ تخطيط لوحة تحكم متداخل مع شريط جانبي يظهر فقط في قسم /dashboard
- نفذ تخطيط مدونة مع شريط جانبي يستمر عبر جميع صفحات المدونة
- أنشئ قالبًا يشغل رسمًا متحركًا للتلاشي في كل تنقل صفحة
- أعد البيانات الوصفية لصفحة مقالة مدونة تتضمن وسوم Open Graph وTwitter Card
- نفذ قالب بيانات وصفية بحيث تتضمن جميع الصفحات " | اسم موقعك" في عناوينها
- أنشئ تخطيطًا يعرض بشكل شرطي تنقلًا مختلفًا بناءً على حالة مصادقة المستخدم
- ابنِ تخطيطًا متعدد اللغات يضبط سمة HTML lang بناءً على اللغة الحالية
- نفذ Context Provider في التخطيط الجذري لتبديل السمة عبر التطبيق بأكمله
الخلاصة
التخطيطات والقوالب هي لبنات بناء أساسية في تطبيقات Next.js App Router. فهم كيفية استخدامها بفعالية أمر حاسم لبناء تطبيقات عالية الأداء وقابلة للصيانة:
- التخطيطات تحافظ على الحالة وتتجنب إعادة العرض أثناء التنقل، مثالية لعناصر واجهة المستخدم الدائمة
- القوالب تعيد التثبيت في كل تنقل، مفيدة للرسوم المتحركة والتتبع
- التخطيطات المتداخلة تتيح تسلسلات هرمية معقدة لواجهة المستخدم مع أداء مثالي
- إدارة البيانات الوصفية في التخطيطات والصفحات تتحكم في عناصر الرأس الحاسمة لتحسين محركات البحث
- مكونات الخادم يجب أن تكون الافتراضية للتخطيطات لتقليل JavaScript
- مكونات العميل يمكن تركيبها ضمن تخطيطات مكونات الخادم عند الحاجة إلى التفاعلية
من خلال إتقان التخطيطات والقوالب، يمكنك إنشاء بنى تطبيقات متطورة عالية الأداء وقابلة للصيانة. في الدرس القادم، سنستكشف استراتيجيات جلب البيانات في Next.js، بما في ذلك العرض من جانب الخادم والتوليد الثابت والجلب من جانب العميل.