React: البدء في استخدام Next.js
آخر تحديث: 2026-08-26
أنشأ توم موقع مدونة باستخدام
create-react-app. بعد إطلاقه، وجد أن تحميل الشاشة الأولى يستغرق 3 ثوانٍ، وأن محركات البحث لا تستطيع فهرسة المحتوى، وأنه كان عليه تحديث ذاكرة التخزين المؤقتة لشبكة توزيع المحتوى (CDN) يدويًّا في كل مرة يقوم فيها بتحديث منشور. أدرك أن العرض من جانب العميل وحده لم يكن كافيًّا — كان بحاجة إلى إطار عمل قادر على العرض من جانب الخادم، والتوليد الثابت، والتحسين التلقائي. عندها ظهر Next.js في الصورة.
يُعد Next.js، الذي طورته وتقوم بصيانته شركة Vercel، الإطار الأكثر شيوعًا حاليًا لتطوير تطبيقات React متكاملة (full-stack). فهو يعالج المشكلات الأساسية التي لا تستطيع React، بصفتها مكتبة واجهة مستخدم بحتة تعمل من جانب العميل، التعامل معها: أنظمة التوجيه، والعرض من جانب الخادم، وإنشاء المواقع الثابتة، وتوجيه واجهات برمجة التطبيقات (API)، وتحسين الصور، وتحسين الخطوط، والبرمجيات الوسيطة، وغير ذلك الكثير. سواء كنت تقوم بإنشاء مدونة، أو موقع للتجارة الإلكترونية، أو تطبيق SaaS، أو تطبيق على مستوى المؤسسات، فإن Next.js يوفر لك أفضل الممارسات الجاهزة للاستخدام فورًا.
يبدأ هذا الدرس بتوضيح المفاهيم الأساسية والتصميم المعماري لـ Next.js. ستتعرف على الفروق بين أوضاع العرض الأربعة، وكيفية عمل «موجه التطبيق» (App Router)، وتقسيم المهام بين المكونات من جانب الخادم ومن جانب العميل. وتشكل هذه المفاهيم الأساسية ركيزة لمزيد من الدراسة حول استرجاع البيانات، والنشر، والعمل العملي الشامل على المشاريع. ومن خلال دراسة الحالة العملية التي أجراها توم حول عملية الترحيل، ستكتسب فهمًا واضحًا للمسار التقني الكامل للترحيل من CRA إلى Next.js.
1. ما ستتعلمه
- CSR مقابل SSR مقابل SSG مقابل ISR: الاختلافات ومعايير الاختيار لأوضاع العرض الأربعة
- آلية التوجيه القائمة على الملفات في موجه تطبيقات Next.js
- تصميم وإعادة استخدام أنظمة التخطيط المتداخلة
- تقسيم المهام والتعاون بين مكون الخادم ومكون العميل
- مسارات الترحيل لموجه الصفحات وموجه التطبيقات
- كيفية تكوين المسارات الديناميكية، والمسارات الشاملة، ومسارات واجهة برمجة التطبيقات (API)
- استخدام عناصر حالة المسار مثل «قيد التحميل» و«خطأ» و«غير موجود»
- دليل عملي للانتقال من مشروع CRA إلى Next.js
- شجرة قرار اختيار وضع العرض: اختر الحل الأمثل بناءً على نوع الصفحة وخصائص البيانات
- مؤشرات الأداء: مقارنة بين وقت تحميل الشاشة الأولى، ودرجة تحسين محركات البحث (SEO)، ووقت وصول البايت الأول (TTFB) عبر أوضاع العرض المختلفة
2. الرسوم التخطيطية المفاهيمية
أثناء تعلمه لـ Next.js، رسم توم مخططًا انسيابيًا لفهم مسار معالجة الطلبات. وعندما يصل طلب من المستخدم، تختار Next.js مسار عرض مختلفًا بناءً على تكوين الصفحة (ثابت، أو ديناميكي، أو تزايدي)، لتقوم في النهاية بإرجاع كود HTML وتمكين التفاعل من جانب المتصفح.
يوضح هذا الرسم البياني أربع نقاط قرار رئيسية: أولاً، تحديد نوع الصفحة (ثابتة/ديناميكية/تدريجية/من جانب العميل)؛ ثم اختيار محرك العرض المطابق؛ وإنشاء كود HTML وإرساله إلى المتصفح؛ وأخيرًا، استخدام عملية «الترطيب» لتمكين مكونات العميل الموجودة على الصفحة من أن تصبح تفاعلية.
flowchart LR
A[User Request] --> B{Next.js Route Matching}
B -->|Static Page| C[SSG<br/>Generated during build]
B -->|Dynamic Pages| D[SSR<br/>Render on Request]
B -->|Incremental Update| E[ISR<br/>Re-verify on Demand]
B -->|Client Interaction| F[CSR<br/>Browser Execution]
C --> G[Back HTML]
D --> G
E --> G
F --> G
G --> H[Hydration<br/>Activate Interaction]
H --> I[Client Component<br/>Run JS]
3. سيناريو واقعي
تم تطوير مدونة توم في الأصل باستخدام create-react-app، حيث كانت جميع الصفحات تُعرض على جانب العميل. وعندما يفتح المستخدمون مقالًا، كانوا يقومون أولاً بتنزيل قالب HTML فارغ، ثم ينتظرون حتى ينتهي تحميل جافا سكريبت قبل استرداد البيانات من واجهة برمجة التطبيقات (API) لعرض المحتوى. وكان هذا الأمر كارثةً لموقع ويب يعتمد على المحتوى — حيث لم ترَ برامج الزحف التابعة لمحركات البحث سوى صفحات فارغة، وكان وقت ظهور المحتوى الأول (TFC) يصل إلى 3 ثوانٍ.
وبعد إجراء بحثه، توصل إلى أن الأمر يتطلب اتباع نهج «العرض الهجين»: حيث يتم إنشاء منشورات المدونة بشكل ثابت (يتم إنشاء كود HTML أثناء عملية البناء وتوزيعه عبر شبكة توزيع المحتوى CDN)، بينما يتم عرض صفحات ملفات تعريف المستخدمين من جانب الخادم (مع إنشاء أحدث البيانات لكل طلب)، ويتم عرض قسم التعليقات من جانب العميل (مع معالجة التفاعلات داخل المتصفح). ويصادف أن Next.js يوفر كل ذلك.
أمضى أسبوعًا في ترحيل مدونته من CRA إلى Next.js، حيث قام بثلاثة أمور محددة: أولًا، حوّل صفحات المنشورات إلى SSG، مستخدمًا generateStaticParams لتوليد كود HTML لجميع صفحات المنشورات أثناء عملية البناء؛ وبعد النشر على Vercel، تقوم شبكة CDN بتقديم الصفحات المُعالجة مسبقًا مباشرةً، مما قلل وقت ظهور المحتوى الأول (TFC) إلى 0.3 ثانية. ثانيًا، قام بتحويل صفحات ملفات تعريف المستخدمين إلى SSR، حيث يتم جلب أحدث البيانات من قاعدة البيانات مع كل طلب؛ ثالثًا، تم تنفيذ قسم التعليقات باستخدام مكونات من جانب العميل، بحيث يتم تحميل جافا سكريبت التفاعلي فقط من جانب المتصفح. بعد عملية النقل، تحسنت درجة تحسين محركات البحث (SEO) للموقع على Google Lighthouse من 45 إلى 98، وانخفض وقت ظهور المحتوى الأول (TFC) بنسبة 90%.
كما واجه توم عددًا لا بأس به من العقبات أثناء عملية الترحيل. على سبيل المثال، استخدم في البداية «موجه الصفحات» (Pages Router) في الدليل pages/، لكنه اكتشف لاحقًا أن «موجه التطبيق» (App Router) يعمل بشكل أفضل، لذا أمضى وقتًا إضافيًا في الترحيل إليه. ويوصي باستخدام «App Router» مباشرةً في المشاريع الجديدة لتجنب الحاجة إلى الترحيل مرة ثانية. كما اكتشف أن useRouter وusePathname لا يمكن استخدامهما مباشرةً في مكونات الخادم (Server Components)؛ بل يجب استخراج منطق التوجيه هذا إلى مكونات العميل (Client Components). وقد منحته هذه الدروس فهمًا عميقًا لفلسفة تصميم Next.js، وهي «الخادم أولاً، والعميل حسب الطلب».
(1) مقارنة بين أوضاع العرض
يوفر Next.js أربعة أوضاع للعرض، كل منها مصمم لحالات استخدام مختلفة. ويعد فهم الاختلافات بينها عاملاً أساسياً لاختيار البنية المناسبة.
CSR (العرض من جانب العميل): يقوم المتصفح بتنزيل هيكل HTML فارغ ثم ينفذ جافا سكريبت لعرض المحتوى. كانت مدونة توم تستخدم هذا النموذج في الأصل. تتمثل مزايا هذا النموذج في التفاعل السلس وتقليل الحمل على الخادم؛ أما عيوبه فهي بطء تحميل الشاشة الأولى وضعف تحسين محركات البحث (SEO). وهو مناسب للتطبيقات التفاعلية التي تتطلب تسجيل دخول المستخدم، مثل لوحات التحكم والواجهات الإدارية الخلفية.
SSR (العرض من جانب الخادم): في كل مرة يرسل فيها المستخدم طلبًا، يقوم الخادم ديناميكيًّا بإنشاء كود HTML الكامل وإرجاعه. استخدم توم هذه الطريقة لحل مشكلات تحسين محركات البحث (SEO) في مدونته، ولكن نظرًا لأن كل طلب يجب أن ينتظر حتى ينتهي الخادم من العرض قبل إرجاع الرد، فإن ذلك يضع عبئًا ثقيلًا على الخادم. وهي مناسبة للصفحات التي تتطلب بيانات في الوقت الفعلي، مثل مواقع الأخبار وصفحات المنتجات في مواقع التجارة الإلكترونية.
SSG (إنشاء المواقع الثابتة): يقوم بإنشاء ملفات HTML لجميع الصفحات خلال مرحلة البناء؛ وبعد النشر على شبكة توزيع المحتوى (CDN)، يصل المستخدمون إلى الملفات الثابتة مباشرةً. ويوفر هذا أسرع سرعة تحميل وأفضل تحسين لمحركات البحث (SEO)، لكن تحديثات المحتوى تتطلب إعادة بناء كاملة. وتعد صفحات مدونة «توم» مثالاً جيدًا على هذا النموذج — فبمجرد كتابة المقالة، يظل محتواها ثابتًا. وهو مناسب للمدونات ومواقع التوثيق وصفحات التسويق.
ISR (التوليد الثابت التزايدي): نسخة محسّنة من SSG تتيح إعادة التحقق من صحة صفحات محددة وتحديثها عند الطلب بعد عملية البناء، دون الحاجة إلى إعادة بناء الموقع بالكامل. على سبيل المثال، إذا تم ضبط صفحة كتالوج منتجات «توم» على إعادة التحقق من صحتها كل 60 ثانية، فستظهر المنتجات الجديدة في غضون 60 ثانية من إضافتها. ويُعد هذا الخيار مثاليًّا لصفحات منتجات التجارة الإلكترونية والمواقع التي يتم تحديث محتواها بشكل متكرر.
يمكن تلخيص استراتيجية الاختيار على النحو التالي: استخدم SSG في السيناريوهات التي تعطي الأولوية للمحتوى، وSSR للبيانات في الوقت الفعلي، وCSR للسيناريوهات التي تتطلب تفاعلات مكثفة، وISR للسيناريوهات التي تتطلب تحديثات متكررة ولا ترغب فيها في إعادة بناء الموقع بالكامل.
جدول مقارنة أداء أوضاع العرض
| المؤشر | SSG | ISR | SSR | CSR |
|---|---|---|---|---|
| سرعة تحميل الشاشة الأولى | الأسرع | سريع | متوسط | بطيء |
| ملاءمة محركات البحث | عالية | عالية | عالية | منخفضة |
| توقيت البيانات | يتم تحديدها عند الإنشاء | يتم تحديثها عند الطلب | أحدث البيانات مع كل طلب | بعد تحميل المتصفح |
| حمل الخادم | لا شيء | منخفض | مرتفع | منخفض |
| حالات الاستخدام | المدونات/الوثائق | التجارة الإلكترونية/الأخبار | الصفحات المخصصة | إدارة النظام الخلفي |
| متوسط زمن الاستجابة الأولي (TTFB) | <50 مللي ثانية | <100 مللي ثانية | 200-500 مللي ثانية | 200-500 مللي ثانية |
▶ المثال 1: تطبيق أوضاع العرض الأربعة في Next.js
يوضح الكود أدناه كيفية تنفيذ أوضاع العرض الثلاثة في مدونة توم. لاحظ أن الدالة generateStaticParams يتم استدعاؤها أثناء عملية البناء وتُرجع جميع تركيبات المعلمات الممكنة؛ ويستخدم Next.js ذلك لإنشاء كود HTML ثابت لجميع الصفحات. أما الدالة dynamic = 'force-dynamic'، فهي تُجبر الصفحة على إعادة العرض مع كل طلب.
// === Static Generation SSG(Default behavior) ===
// app/blog/[slug]/page.tsx
// Automatically scan all articles during the build process slug,Generate the corresponding HTML Page
// After generation CDN Return static files directly,No server-side rendering required
export async function generateStaticParams() {
// Called during build:Get a list of all articles
const posts = await fetch('https://api.example.com/posts').then(r => r.json())
// Back slug Array,Next.js It will do this for each slug Generate a page
return posts.map((post: any) => ({ slug: post.slug }))
}
async function BlogPost({ params }: { params: { slug: string } }) {
// Each page retrieves data independently when it is rendered.
const post = await fetch(`https://api.example.com/posts/${params.slug}`).then(r => r.json())
return (
<article>
<h1>{post.title}</h1>
<time>{post.publishedAt}</time>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
)
}
export default BlogPost
// === Server-Side Rendering SSR(Dynamic Rendering) ===
// app/dashboard/page.tsx
// Every user request starts from API Get the latest data,Ensure the data is up to date
export const dynamic = 'force-dynamic' // Force the static cache to close,Leaving every time SSR
async function DashboardPage() {
// This will be executed with every request fetch
const stats = await fetch('https://api.example.com/dashboard/stats').then(r => r.json())
return (
<div>
<h1>Real-Time Dashboard</h1>
<p>Current Online Users:{stats.onlineUsers}</p>
<p>Today's Orders:{stats.todayOrders}</p>
<p>Income for This Month:${stats.monthlyRevenue}</p>
</div>
)
}
export default DashboardPage
// === Incremental Static Generation ISR ===
// app/products/[id]/page.tsx
// Generate static pages during the build process,But every 60 The first visit after X seconds will trigger a regenerate.
async function ProductPage({ params }: { params: { id: string } }) {
const product = await fetch(`https://api.example.com/products/${params.id}`, {
next: { revalidate: 60 } // ISR:60 Please try again in a few seconds.
}).then(r => r.json())
return (
<div>
<h1>{product.name}</h1>
<p>Price:${product.price}</p>
<p>Inventory:{product.stock}</p>
<p>Description:{product.description}</p>
</div>
)
}
export default ProductPage
في مدونته، اختار توم اتباع نهج هجين يجمع بين SSG و ISR: فهو يستخدم SSG لصفحات المقالات لضمان التحميل السريع للصفحة الرئيسية، ويستخدم ISR لصفحات قوائم المنتجات، التي تُحدَّث الأسعار والمخزون فيها كل 60 ثانية. ويضمن هذا النهج الحصول على نتائج جيدة في تحسين محركات البحث (SEO) مع الحفاظ في الوقت نفسه على حداثة البيانات.
(2) توجيه ملفات «App Router»
يقدم Next.js 13+ ميزة «App Router»، التي تقوم تلقائيًا بإنشاء مسارات بناءً على نظام الملفات. كل ما عليك فعله هو إنشاء مجلد باسم app/ وملف باسم page.tsx داخل هذا المجلد، وسيقوم Next.js تلقائيًا بتعيين مسارات عناوين URL المقابلة. لم تعد بحاجة إلى صيانة ملفات تكوين المسارات يدويًّا — فهياكل الدلائل هي نفسها هياكل المسارات.
أنواع الملفات الأساسية
page.tsx— يحدد صفحة واجهة المستخدم المرتبطة بمسار معين؛ ولا يُسمح بوجود سوى ملف واحد من هذا النوع في كل مجلدlayout.tsx— يحدد التخطيط المشترك لهذا الجزء من المسار وجميع المسارات الفرعية التابعة له؛ ويدعم التداخلloading.tsx— عرض واجهة المستخدم أثناء تحميل أجزاء المسار، باستخدام React Suspenseerror.tsx— واجهة المستخدم التي تظهر عند حدوث خطأ في مقطع التوجيه؛ يمكن إعادة المحاولة باستخدام وظيفة إعادة الضبط الموجودة في ملف error.tsxnot-found.tsx— صفحة 404، مع دقة تصل إلى كل مقطع من مسار الرحلةroute.ts— يحدد نقاط نهاية واجهة برمجة التطبيقات (API) ويدعم طرقًا مثل GET وPOST وPUT وDELETE
التوجيه الديناميكي والأوضاع المتقدمة
استخدم صيغة [param] لإنشاء مسارات ديناميكية؛ على سبيل المثال، تتطابق app/blog/[slug]/page.tsx مع /blog/hello-world. استخدم صيغة التعميم [...param] لمطابقة المسارات متعددة المستويات؛ على سبيل المثال، تتطابق app/docs/[...slug]/page.tsx مع /docs/guide/getting-started/installation.
تدعم ملفات التخطيط التخطيطات المتداخلة بعمق — حيث يحيط تخطيط «أب» بمحتوى الصفحات «الفرعية». ولا يلزم كتابة الأقسام الشائعة، مثل شريط التنقل والشريط الجانبي والتذييل، سوى مرة واحدة؛ حيث تتولى Next.js إدارة الحالة الدائمة للتخطيط تلقائيًا. وعند التبديل بين الصفحات، لا يتم إعادة عرض التخطيط؛ بل يتم تحديث الصفحات الفرعية فقط.
▶ المثال 2: هيكل توجيه المدونة الكامل
يُظهر هذا هيكل الدلائل الكامل لـ «App Router» الخاص بمدونة توم. لاحظ أن أسماء الملفات الخاصة في كل مجلد (page، layout، loading، error، not-found) تتبع قواعد Next.js — حيث يتعرف الإطار تلقائيًا على هذه الملفات ويخصص لها سلوكيات توجيه محددة. يقع dashboard/layout.tsx داخل المجلد الجذر layout.tsx، مما يشكل تخطيطًا من مستويين: تتكون الطبقة الخارجية من شريط التنقل العام وتذييل الصفحة، بينما تحتوي الطبقة الداخلية على الشريط الجانبي الخاص بلوحة التحكم.
app/
├── page.tsx # → / Home
├── layout.tsx # → Global Layout(Navigation + Footer)
├── loading.tsx # → Global Loading State(Skeleton screen during page transitions)
├── error.tsx # → Global Error Page(500 A Flawed Fallback UI)
├── not-found.tsx # → 404 Page
├── about/
│ └── page.tsx # → /about About This Page
├── blog/
│ ├── page.tsx # → /blog Article List Page
│ └── [slug]/ # → Dynamic routing: matches /blog/any-value
│ ├── page.tsx # → /blog/hello-world Article Details
│ └── loading.tsx # → Custom Loading State for Article Detail Pages
├── dashboard/
│ ├── page.tsx # → /dashboard Dashboard Overview
│ ├── layout.tsx # → Dashboard-Exclusive Layout(With sidebar navigation)
│ └── settings/
│ └── page.tsx # → /dashboard/settings Settings Page
└── api/
└── hello/
└── route.ts # → /api/hello API Endpoint
جدول مرجعي لرسم خرائط المسارات
| مسار الملف | عنوان URL المقابل | الوصف |
|---|---|---|
app/page.tsx |
/ |
الصفحة الرئيسية |
app/about/page.tsx |
/about |
المسارات الثابتة |
app/blog/[slug]/page.tsx |
/blog/:slug |
التوجيه الديناميكي |
app/blog/[slug]/loading.tsx |
/blog/:slug |
حالات التحميل على نفس المسار |
app/dashboard/layout.tsx |
/dashboard/* |
تخطيط متداخل |
app/api/hello/route.ts |
/api/hello |
نقطة نهاية واجهة برمجة التطبيقات |
// app/layout.tsx - Global Layout
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh">
<body>
<header>
<nav>
<a href="/">Home</a>
<a href="/blog">Blog</a>
<a href="/about">About</a>
</nav>
</header>
<main>{children}</main>
<footer>© 2026 Tom 's blog. All rights reserved.</footer>
</body>
</html>
)
}
// app/dashboard/layout.tsx - Dashboard-Exclusive Layout(Nested within the global layout)
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div style={{ display: 'flex' }}>
<aside style={{ width: 240, background: '#f5f5f5', padding: 16 }}>
<nav>
<a href="/dashboard">Overview</a>
<a href="/dashboard/settings">Settings</a>
</nav>
</aside>
<section style={{ flex: 1, padding: 16 }}>{children}</section>
</div>
)
}
// app/blog/[slug]/loading.tsx - Article Details Loading...
export default function Loading() {
return (
<div style={{ padding: 24 }}>
<div style={{ height: 32, width: '60%', background: '#eee', borderRadius: 4, marginBottom: 16 }} />
<div style={{ height: 16, width: '30%', background: '#eee', borderRadius: 4, marginBottom: 24 }} />
<div style={{ height: 200, background: '#eee', borderRadius: 4 }} />
</div>
)
}
(3) مكون الخادم ومكون العميل
يُعد «مكون الخادم» (Server Component) أحد التصاميم الثورية في Next.js. وهذه هي المرة الأولى في منظومة React التي يتم فيها تقسيم المكونات إلى بيئتي تنفيذ: بيئة جانب الخادم وبيئة جانب العميل. وبشكل افتراضي، تكون جميع المكونات الموجودة في «موجه التطبيق» (App Router) مكونات خادم — فهي تعمل على الخادم، ويمكنها الوصول مباشرةً إلى قاعدة البيانات ونظام الملفات ومتغيرات البيئة الحساسة، والأهم من ذلك أنها لا ترسل أي كود JavaScript إلى المتصفح.
عندما تحتاج إلى وظائف تفاعلية (مثل النقر على زر، أو إدخال نص، أو استخدام useState أو useEffect، أو الاستجابة لأحداث النافذة)، فإن المكون من جانب الخادم وحده لا يكفي. في هذه الحالة، تحتاج إلى إضافة إعلان 'use client' في أعلى الملف. سيقوم Next.js بعد ذلك بتمييز هذا المكون وجميع مكوناته الفرعية كمكونات عميل (Client Components) وتجميعها ليتم تنفيذها على جانب المتصفح.
ميزات مكون الخادم
- يمكنك استخدام
async/awaitلاسترداد البيانات مباشرةً، دون الحاجة إلى useEffect أو SWR - الوصول المباشر إلى قواعد البيانات وأنظمة الملفات والخدمات الخلفية
- لا ترسل جافا سكريبت إلى المتصفح لتقليل حجم الحزمة
- الوصول الآمن إلى متغيرات البيئة (التي لا تبدأ بـ NEXT_PUBLIC_)
- لا يمكنك استخدام واجهات برمجة التطبيقات (API) الخاصة بالمتصفح، مثل useState وuseEffect وonClick
ميزات مكون العميل
- تفاعل كامل مع المتصفح (الحالة، والأحداث، وواجهة برمجة تطبيقات DOM)
- يمكن دمجها بحرية مع مكونات الخادم
- لا يزال يدعم SSR (العرض من جانب الخادم لملف HTML الأولي)
- كلما زاد حجم الملف، زادت كمية جافا سكريبت التي يتم إرسالها إلى المتصفح
نماذج أفضل الممارسات
وهي تعتمد بنية متعددة الطبقات حيث يعمل «مكون الخادم» كحاوية، بينما يعمل «مكون العميل» كعنصر نهائي. ويتولى «مكون الخادم» في الطبقة الخارجية مسؤولية جلب البيانات والتحكم في التخطيط، في حين يتولى «مكون العميل» في الطبقة الداخلية مسؤولية العناصر التي تتطلب تفاعلًا فقط. وبهذه الطريقة، تتم معالجة معظم العمليات المنطقية وعمليات استرجاع البيانات على جانب الخادم، ولا يقوم المتصفح بتشغيل سوى الحد الأدنى الضروري من جافا سكريبت.
▶ المثال 3: التقسيم الأمثل للمسؤوليات بين مكونات الخادم والعميل
يوضح الكود أدناه النموذج الطبقي الموصوف في مدونة توم: «استرجاع البيانات على الخادم، والتفاعلات في المتصفح». PostsPage هو مكون خادم — فهو يسترد البيانات من الخادم، ويعرضها بتنسيق HTML، ويرسلها مباشرةً إلى المتصفح. يرى المستخدمون الصفحة كاملةً دون الحاجة إلى انتظار تحميل JavaScript. PostList هو مكون عميل — يستقبل البيانات التي أعدها الخادم، ويكون مسؤولاً فقط عن تفاعلات البحث والتصفية والحذف من جانب المتصفح. لاحظ أن 'use client' يُطبق فقط على «المكونات الطرفية» التي تتطلب تفاعلًا، وليس على الصفحة بأكملها.
// app/posts/page.tsx - Server Component(Default)
// This component runs on the server.,Do not send any JS Go to the browser
import PostList from './PostList'
async function PostsPage() {
// Directly on the server fetch,Data is being compiled HTML It was already ready at that time
// Users see the full page,None loading Status
const posts = await fetch('https://api.example.com/posts').then(r => r.json())
return (
<div>
<h1>All Articles</h1>
<PostList posts={posts} />
</div>
)
}
export default PostsPage
// app/posts/PostList.tsx - Annotation 'use client' Become Client Component
// Only this file will be sent JS Go to the browser,The parent component that contains it(Server Component)No
'use client'
import { useState } from 'react'
interface Post {
id: number
title: string
body: string
}
function PostList({ posts: initialPosts }: { posts: Post[] }) {
// useState Only at 'use client' Available in the component
const [posts, setPosts] = useState(initialPosts)
const [search, setSearch] = useState('')
const filtered = posts.filter(p =>
p.title.toLowerCase().includes(search.toLowerCase())
)
function handleDelete(id: number) {
setPosts(prev => prev.filter(p => p.id !== id))
}
return (
<div>
<input
type="text"
placeholder="Search Articles..."
value={search}
onChange={e => setSearch(e.target.value)}
style={{ width: '100%', padding: 8, marginBottom: 16, border: '1px solid #ddd', borderRadius: 4 }}
/>
{filtered.map(post => (
<div key={post.id} style={{
border: '1px solid #ddd', padding: 16, marginBottom: 8, borderRadius: 8,
display: 'flex', justifyContent: 'space-between', alignItems: 'center'
}}>
<div>
<h2 style={{ margin: 0 }}>{post.title}</h2>
<p style={{ margin: '8px 0', color: '#666' }}>{post.body}</p>
</div>
<button
onClick={() => handleDelete(post.id)}
style={{ padding: '4px 12px', background: '#ff4444', color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer' }}
>
Delete
</button>
</div>
))}
{filtered.length === 0 && <p>No matching articles were found.</p>}
</div>
)
}
export default PostList
// pages/old-posts.js - Pages Router Pattern(Compatibility with Legacy Projects)
// If you are maintaining Next.js 12 or earlier projects,Pages Router Usage getStaticProps/getServerSideProps
export async function getStaticProps() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json())
return { props: { posts }, revalidate: 60 }
}
export default function OldPostsPage({ posts }: { posts: Post[] }) {
return (
<div>
<h1>Pages Router Pattern</h1>
{posts.map(p => <p key={p.id}>{p.title}</p>)}
</div>
)
}
القيود المفروضة على مكونات الخادم مقابل مكونات العميل
| الإمكانية | مكون الخادم | مكون العميل |
|---|---|---|
| استخدام useState/useEffect | ❌ | ✅ |
| استخدام onClick/onChange | ❌ | ✅ |
| الوصول المباشر إلى قاعدة البيانات | ✅ | ❌ |
| الوصول إلى متغيرات البيئة (بدون بادئة) | ✅ | ❌ |
| إرسال جافا سكريبت إلى المتصفح | ❌ | ✅ |
| استخدام async/await | ✅ | ❌ |
| استخدام useRouter/usePathname | ❌ | ✅ |
| استخدم requestAnimationFrame | ❌ | ✅ |
مخطط انسيابي: مكون الخادم أم مكون العميل؟
Components require interaction(Click、Input、Scrolling, etc.)?
├── is → Required useState/useEffect?
│ ├── Yes → add 'use client' → Client Component
│ └── No → only accept props for rendering?
│ ├── is → Retain Server Component
│ └── No → add 'use client'
└── No → keep Server Component
4. خارطة طريق توم للتحول
لقد استعرضنا الآن جميع المفاهيم الأساسية لـ Next.js. والآن، دعونا نجمع هذه المعرفة معًا ونرى كيف قام توم بترحيل مدونته التي كانت تعمل بنظام CRA إلى Next.js خطوة بخطوة. وتُعد هذه العملية أيضًا النهج القياسي لتطبيق Next.js في مشاريعكم الخاصة.
يشرح توم العملية الكاملة لنقل مدونة من CRA إلى Next.js في خمس خطوات، تتوافق كل منها مع مفهوم رئيسي تمت تغطيته في هذا الدرس:
- الخطوة 1: تهيئة المشروع — قم بإنشاء مشروع جديد باستخدام
create-next-app، واختر «App Router»، ثم انقل كودsrc/القديم إلى الدليلapp/. وتكمن النقطة الأساسية في هذه الخطوة في فهم الاختلافات في الدلائل بين «App Router» و«Pages Router». - الخطوة 2: إعادة هيكلة المسارات — استبدال تكوين مسار
react-router-domفي CRA بالتوجيه القائم على الملفات في App Router. تم تحويل التكوين اليدوي السابق للمسار (<Routes><Route path="..." /></Routes>) إلى بنية مجلدات سهلة الفهم. ويستخدم التوجيه الديناميكي الآن صيغة[param]بدلاً من صيغة:param. - الخطوة 3: تحديد وضع العرض حسب نوع الصفحة — اختر استراتيجية العرض المناسبة لأنواع الصفحات المختلفة: استخدم SSG (مع
generateStaticParamsللتوليد المسبق) لمنشورات المدونة، وSSR (معdynamic = 'force-dynamic') لملفات تعريف المستخدمين، وCSR + مكون العميل (مع'use client') لأقسام التعليقات. - الخطوة 4: تكامل التخطيط — استخراج كود شريط التنقل وتذييل الصفحة، اللذين كانا مكررين سابقًا في كل صفحة، ودمجهما في تخطيط عام، مما يقلل من تكرار الكود بنسبة 60٪. استخدم التخطيطات المتداخلة في منطقة لوحة التحكم لإضافة شريط التنقل الجانبي، وبذلك يتم تحقيق فصل الاهتمامات في التخطيط.
- الخطوة 5: التحسين والاختبار — أضف
loading.tsxلتنفيذ الشاشة الأولية أثناء التحميل، وerror.tsxلتنفيذ واجهة المستخدم الاحتياطية في حالة حدوث خطأ. استخدم Chrome Lighthouse للتحقق من مقاييس تحسين محركات البحث (SEO) والأداء، مع التأكد من أن درجة تحسين محركات البحث بعد الترحيل تصل إلى 90+ أو أكثر.
▶ المثال 4: ملفات «loading.tsx» و«error.tsx» في «App Router»
// app/dashboard/loading.tsx - Automatically Displayed Loading Status
export default function DashboardLoading() {
return (
<div style={{ padding: 24 }}>
<div style={{ height: 32, width: 200, background: '#f0f0f0', borderRadius: 4, marginBottom: 16 }} />
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 16 }}>
{[1, 2, 3].map(i => (
<div key={i} style={{
height: 120, background: 'linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%)',
backgroundSize: '200% 100%', borderRadius: 8, animation: 'shimmer 1.5s infinite',
}} />
))}
</div>
</div>
)
}
// app/dashboard/error.tsx - Automatically Displayed Error Status
'use client'
export default function DashboardError({ error, reset }) {
return (
<div style={{ padding: 40, textAlign: 'center' }}>
<h2>Something went wrong!</h2>
<p style={{ color: '#999' }}>{error.message}</p>
<button onClick={reset} style={{ padding: '8px 24px', cursor: 'pointer' }}>Try again</button>
</div>
)
}
// app/dashboard/page.tsx - It will automatically use the one above loading and error
async function DashboardPage() {
const data = await fetch('https://api.example.com/dashboard').then(r => r.json())
return (
<div>
<h1>Dashboard</h1>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 16 }}>
{data.stats.map(stat => (
<div key={stat.label} style={{ padding: 16, border: '1px solid #eee', borderRadius: 8 }}>
<p style={{ color: '#999', margin: 0 }}>{stat.label}</p>
<p style={{ fontSize: 24, fontWeight: 'bold', margin: '4px 0 0' }}>{stat.value}</p>
</div>
))}
</div>
</div>
)
}
export default DashboardPage
▶ المثال 5: التخطيطات والقوالب المتداخلة
// app/layout.tsx - Root Layout(Must have html and body)
export default function RootLayout({ children }) {
return (
<html lang="zh">
<body style={{ margin: 0, fontFamily: 'sans-serif' }}>
{children}
</body>
</html>
)
}
// app/(dashboard)/layout.tsx - Dashboard Layout(Route groups have no effect URL)
import Sidebar from '@/components/Sidebar'
export default function DashboardLayout({ children }) {
return (
<div style={{ display: 'flex', minHeight: '100vh' }}>
<Sidebar />
<main style={{ flex: 1, padding: 24 }}>
{children}
</main>
</div>
)
}
// app/(auth)/layout.tsx - Authentication Page Layout(Centered Card)
export default function AuthLayout({ children }) {
return (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: '#f5f5f5' }}>
<div style={{ background: 'white', padding: 32, borderRadius: 8, boxShadow: '0 2px 8px rgba(0,0,0,0.1)', width: 400 }}>
{children}
</div>
</div>
)
}
// app/(auth)/login/page.tsx - Login Page
function LoginPage() {
return (
<>
<h2 style={{ textAlign: 'center' }}>Sign In</h2>
<form style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<input type="email" placeholder="Email" style={{ padding: 8, borderRadius: 4 }} />
<input type="password" placeholder="Password" style={{ padding: 8, borderRadius: 4 }} />
<button type="submit" style={{ padding: 10, background: '#1890ff', color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer' }}>
Sign In
</button>
</form>
</>
)
}
export default LoginPage
❓ أسئلة شائعة
'use client' إلا عندما يحتاج أحد المكونات إلى استخدام useState أو useEffect أو onClick أو واجهات برمجة تطبيقات المتصفح. من الأنماط الشائعة أن يقوم مكون الخادم بجلب البيانات وتمريرها إلى مكون عميل داخلي للتعامل مع التفاعلات. بهذه الطريقة، تتم معالجة استرجاع البيانات على الخادم، بينما يتم تشغيل منطق التفاعل في المتصفح — وهو ما يجمع بين مزايا كلا العالمين. من الأخطاء الشائعة بين المبتدئين إضافة 'use client' إلى الصفحة بأكملها — حيث يجب تطبيقه فقط على أصغر العقد الطرفية التفاعلية. إذا وجدت مكونًا لا يتطلب تفاعلًا ولا حالة، فلا تضف 'use client'.pages/ وتقوم بتعيين المسارات بناءً على أسماء الملفات. أما «موجه التطبيقات» (App Router) فهو نظام التوجيه الجديد الذي تم تقديمه في Next.js 13. ويستخدم الدليل app/ ويدعم ميزات جديدة مثل التخطيطات المتداخلة ومكونات الخادم وعرض البث المباشر. بالنسبة للمشاريع الجديدة، يُنصح باستخدام «موجه التطبيق» مباشرةً؛ أما المشاريع الحالية فيمكن ترحيلها تدريجيًا. وقد تم استبدال بنية getStaticProps/getServerSideProps في «موجه الصفحات» بـ async component + fetch في «موجه التطبيق». يمكن أن يتعايش نظاما التوجيه معًا، وأثناء عملية الترحيل، يمكنك نقل الصفحات تدريجيًا من «موجه الصفحات» إلى «موجه التطبيق».next build && next start للنشر على أي خادم Node.js، أو استخدام Docker للنشر في حاويات. كما تدعم Netlify وAWS Amplify وCloudflare Pages نشر Next.js. ومع ذلك، إذا كنت تستخدم ميزات متقدمة مثل ISR والبرمجيات الوسيطة، فإن Vercel توفر أفضل توافق.pages/ إلى app/، مع التحقق من كل صفحة أثناء نقلها — فلا داعي لترحيل كل شيء دفعة واحدة. أثناء الترحيل، يرجى ملاحظة ما يلي: قم بتغيير getStaticProps إلى fetch مباشرةً داخل مكون الخادم؛ وانقل التخطيط إلى layout.tsx، الذي تم إنشاؤه ضمن دليل app/؛ وقم بترحيل المنطق من pages/_app.tsx إلى الجذر layout.tsx. نوصي بالبدء بالصفحات الأبسط والتعامل مع الصفحات الأكثر تعقيدًا بمجرد اكتساب بعض الخبرة.params، ومعلمات سلسلة الاستعلام عبر الخاصية searchParams. على سبيل المثال، function Page({ params, searchParams }: { params: { slug: string }, searchParams: { q: string } }). ومع ذلك، لاحظ أنه لا يمكنك استخدام useRouter أو usePathname في مكون الخادم — فهذه الخطافات متاحة فقط في مكونات العميل.📖 ملخص
- Next.js هو إطار عمل React متكامل يوفر ميزات جاهزة للاستخدام مثل التوجيه، وتحسين العرض، وتوجيه واجهات برمجة التطبيقات (API)، وتحسين الصور، والبرمجيات الوسيطة.
- أربعة أوضاع للعرض: يُعد SSG الأسرع والأكثر ملاءمة للمواقع الغنية بالمحتوى؛ بينما يوفر SSR أداءً في الوقت الفعلي وهو الأنسب للمواقع الغنية بالبيانات؛ أما ISR فهو حل وسط (إعادة التحقق عند الطلب)؛ وCSR هو الأنسب للتطبيقات التي تتطلب تفاعلات مكثفة.
- مقارنة الأداء: يتميز SSG بأسرع وقت لتحميل الصفحة الأولى دون أي عبء على جانب الخادم؛ بينما يوفر SSR بيانات في الوقت الفعلي لكنه يضع عبئًا ثقيلًا على الخادم؛ ويحقق ISR التوازن بين السرعة والأداء في الوقت الفعلي؛ أما CSR فهو مناسب لسيناريوهات التفاعل في الخلفية.
- يستخدم «موجه التطبيقات» (App Router) التوجيه القائم على نظام الملفات؛ حيث تتوافق كل من المجلدات
app/وpage.tsxوlayout.tsxوloading.tsxوerror.tsxوnot-found.tsxمع قدرة توجيه منفصلة. - تستخدم المسارات الديناميكية صيغة
[param]، بينما تستخدم المسارات الشاملة صيغة[...param]؛ وتستخدم مسارات واجهة برمجة التطبيقات (API) ملفroute.ts - تدعم التخطيطات التداخل العميق واستمرار الحالة — فالانتقال إلى صفحة فرعية لا يؤدي إلى إزالة التخطيط الذي يجري تحميله حاليًا
- تعمل مكونات الخادم على الخادم بشكل افتراضي ولا ترسل جافا سكريبت إلى المتصفح؛ أما المكونات التي تتطلب تفاعلًا فيجب تعريفها كمكونات عميل باستخدام
'use client'. - بنية متعددة الطبقات: يعمل مكون الخادم كحاوية للبيانات، بينما يعمل مكون العميل كعقدة طرفية تفاعلية، مما يقلل حجم جافا سكريبت على جانب المتصفح إلى أقصى حد ممكن
- تم استبدال موجه الصفحات
getStaticProps/getServerSidePropsفي موجه التطبيق بمكون غير متزامن + fetch - عملية اتخاذ القرار لاختيار وضع العرض: هل يتغير محتوى الصفحة بشكل متكرر؟ هل يحتاج المستخدمون إلى بيانات في الوقت الفعلي؟ هل تتطلب الصفحة تحسين محركات البحث (SEO)؟ ضع في اعتبارك جميع هذه العوامل عند الاختيار بين SSG وISR وSSR وCSR.
- عملية الترحيل: CRA ->
create-next-appإنشاء -> ترحيل المكونات إلى الدليلapp/-> تحديد وضع العرض حسب الصفحة -> استخراج التخطيط -> إضافة حالات التحميل/الخطأ - تُعد هذه الدورة الأساس للدورات اللاحقة التي تتناول استرجاع البيانات باستخدام Next.js، والنشر في بيئة التكامل المستمر/التسليم المستمر (CI/CD)، وتكامل مكتبات المكونات، بالإضافة إلى تمارين عملية شاملة على المشاريع.
- ضع درس توم في اعتبارك: اختر «App Router» منذ البداية للمشاريع الجديدة لتجنب تكلفة عملية ترحيل ثانية.
📝 تمارين
-
استخدم
npx create-next-app@latest my-blogلإنشاء مشروع جديد، مع التأكد من استخدام بنية مجلدات «App Router». ضمنapp/، أنشئ ثلاثة مسارات للصفحات:/aboutو/blogو/blog/[slug]. يجب أن تحتوي كل صفحة على عنوان واحد على الأقل ووصف. قم بتشغيلnpm run devوقم بزيارة كل مسار (/aboutو/blogو/blog/test-article) للتحقق من صحة التعيينات. -
قم بتنفيذ تخطيط عام (
app/layout.tsx) يتضمن شريط تنقل علوي (يحتوي على ثلاثة روابط: الصفحة الرئيسية، والمدونة، و«نبذة عنا») وتذييل الصفحة. بعد ذلك، قم بإنشاء تخطيط متداخل للمسار/blog/*لعرض جدول محتويات المقالة على الجانب الأيسر من صفحة تفاصيل المقالة. افتح React DevTools في متصفحك للتحقق من صحة التسلسل الهرمي للتداخل في التخطيط، ومن أن شريط التنقل والشريط الجانبي يحتفظان بحالتهما دون إعادة عرض عند التبديل بين الصفحات. -
قم بتغيير الصفحة الرئيسية للمدونة إلى مكون خادم يسترد قائمة المقالات مباشرةً من واجهة برمجة تطبيقات JSONPlaceholder (
https://jsonplaceholder.typicode.com/posts) ويعرضها. بعد ذلك، أنشئ مكون عميل لتنفيذ وظائف البحث عن الكلمات المفتاحية والتصفية، مما يتيح للمستخدمين البحث عن المقالات وتصفيتها في الوقت الفعلي من جانب المتصفح. تأكد من أن مكون الخادم يتولى استرداد البيانات، بينما يتولى مكون العميل منطق التصفية التفاعلي فقط. بمجرد الانتهاء من ذلك، أضفloading.tsx(لعرض شاشة مؤقتة) وerror.tsx(لعرض رسائل الخطأ وزر إعادة المحاولة) لتجربة التكوين الكامل لمقطع التوجيه. -
اختياري: انشر مدونتك على Vercel (مجانًا)، وراقب عملية إعادة التحقق من صلاحية ISR في وحدة التحكم الخاصة بـ Vercel — بعد تعديل البيانات التي تعيدها واجهة برمجة التطبيقات (API)، تحقق مما إذا كانت الصفحة تتحديث تلقائيًا بعد انتهاء صلاحية
revalidate.