إطار Next.js

العرض من جانب الخادم (SSR)

45 دقيقة الدرس 10 من 40

العرض من جانب الخادم (SSR) في Next.js

العرض من جانب الخادم (SSR) يعرض الصفحات عند الطلب لكل طلب، مما يوفر بيانات جديدة ومحتوى ديناميكي. يغطي هذا الدرس العرض الديناميكي والبث وحدود React Suspense وحالات التحميل وتقنيات تحسين الأداء.

ما هو SSR؟ العرض من جانب الخادم يولد HTML على الخادم لكل طلب. على عكس التوليد الثابت، يضمن SSR أن المستخدمين يرون دائماً المحتوى الأكثر حداثة، مما يجعله مثالياً للبيانات الشخصية أو المتغيرة بشكل متكرر.

اختيار العرض الديناميكي

تصبح الصفحات معروضة ديناميكياً عندما تستخدم ميزات معينة أو تختار عدم التخزين المؤقت:

1. استخدام الدوال الديناميكية

// app/dashboard/page.tsx import { cookies, headers } from 'next/headers'; // هذه الصفحة يتم عرضها ديناميكياً لأنها تستخدم cookies() export default async function DashboardPage() { const cookieStore = cookies(); const theme = cookieStore.get('theme'); return ( <div> <h1>لوحة التحكم</h1> <p>السمة الحالية: {theme?.value || 'افتراضي'}</p> </div> ); }

الدوال الديناميكية التي تشغل SSR:

  • cookies() - قراءة أو تعيين ملفات تعريف الارتباط
  • headers() - قراءة رؤوس الطلب
  • searchParams - الوصول إلى معلمات بحث URL في مكونات الصفحة
  • fetch() مع cache: 'no-store' أو next: { revalidate: 0 }

2. استخدام معلمات البحث

// app/search/page.tsx // يتم عرضها ديناميكياً لأنها تستخدم searchParams export default async function SearchPage({ searchParams }: { searchParams: { q?: string; filter?: string } }) { const query = searchParams.q || ''; const filter = searchParams.filter || 'all'; // جلب النتائج بناءً على معلمات البحث const results = await fetch( `https://api.example.com/search?q=${query}&filter=${filter}`, { cache: 'no-store' } ).then(res => res.json()); return ( <div> <h1>نتائج البحث عن: {query}</h1> <p>التصفية: {filter}</p> {results.length === 0 ? ( <p>لم يتم العثور على نتائج</p> ) : ( <ul> {results.map(item => ( <li key={item.id}>{item.title}</li> ))} </ul> )} </div> ); }

3. تعيين تكوين المسار الديناميكي

// app/profile/page.tsx // فرض العرض الديناميكي export const dynamic = 'force-dynamic'; export default async function ProfilePage() { const user = await getCurrentUser(); return ( <div> <h1>ملف تعريف {user.name}</h1> <p>البريد الإلكتروني: {user.email}</p> <p>آخر تسجيل دخول: {new Date(user.lastLogin).toLocaleString()}</p> </div> ); }
خيارات التكوين الديناميكي:
  • 'auto' (افتراضي) - تحديد تلقائي بناءً على الاستخدام
  • 'force-dynamic' - فرض العرض الديناميكي
  • 'force-static' - فرض العرض الثابت (قد يسبب أخطاء إذا تم استخدام ميزات ديناميكية)
  • 'error' - إطلاق خطأ إذا حاولت الصفحة استخدام ميزات ديناميكية

البث مع React Suspense

البث يسمح لك بعرض واجهة المستخدم تدريجياً، إرسال المحتوى إلى العميل عندما يصبح جاهزاً بدلاً من انتظار تحميل جميع البيانات:

الاستخدام الأساسي لـ Suspense

// app/posts/page.tsx import { Suspense } from 'react'; import PostsList from '@/components/PostsList'; import PostsListSkeleton from '@/components/PostsListSkeleton'; export default function PostsPage() { return ( <div> <h1>منشورات المدونة</h1> {/* يتم عرض هيكل الصفحة فوراً */} <Suspense fallback={<PostsListSkeleton />}> {/* يتم بث PostsList عندما تكون البيانات جاهزة */} <PostsList /> </Suspense> </div> ); } // components/PostsList.tsx async function PostsList() { // جلب البيانات يحدث في الخلفية const posts = await fetch('https://api.example.com/posts') .then(res => res.json()); return ( <ul> {posts.map(post => ( <li key={post.id}> <h2>{post.title}</h2> <p>{post.excerpt}</p> </li> ))} </ul> ); }
كيف يعمل البث:
  1. الخادم يرسل هيكل HTML الأولي فوراً (h1، Suspense fallback)
  2. المستخدم يرى هيكل التحميل أثناء جلب البيانات في الخلفية
  3. عندما تكون البيانات جاهزة، يبث الخادم المحتوى الفعلي
  4. React يميّه ويستبدل الاحتياطي بالمحتوى الحقيقي

حدود Suspense متعددة

// app/dashboard/page.tsx import { Suspense } from 'react'; import UserProfile from '@/components/UserProfile'; import RecentActivity from '@/components/RecentActivity'; import Analytics from '@/components/Analytics'; export default function DashboardPage() { return ( <div> <h1>لوحة التحكم</h1> {/* كل قسم يمكن بثه بشكل مستقل */} <Suspense fallback={<div>تحميل الملف الشخصي...</div>}> <UserProfile /> </Suspense> <div className="grid"> <Suspense fallback={<div>تحميل النشاط...</div>}> <RecentActivity /> </Suspense> <Suspense fallback={<div>تحميل التحليلات...</div>}> <Analytics /> </Suspense> </div> </div> ); }
التحميل الدقيق: حدود Suspense المتعددة تسمح لأجزاء مختلفة من صفحتك بالتحميل بشكل مستقل. البيانات السريعة تظهر بسرعة بينما البيانات البطيئة تبث لاحقاً، مما يحسن الأداء المدرك.

حدود Suspense المتداخلة

// app/posts/[id]/page.tsx import { Suspense } from 'react'; import PostContent from '@/components/PostContent'; import PostComments from '@/components/PostComments'; import RelatedPosts from '@/components/RelatedPosts'; export default function PostPage({ params }: { params: { id: string } }) { return ( <article> {/* المحتوى الحرج يتم تحميله أولاً */} <Suspense fallback={<div>تحميل المنشور...</div>}> <PostContent id={params.id} /> {/* المحتوى الأقل أهمية يمكن تحميله بعد ذلك */} <Suspense fallback={<div>تحميل التعليقات...</div>}> <PostComments postId={params.id} /> </Suspense> </Suspense> {/* المنشورات ذات الصلة تحمل أخيراً */} <Suspense fallback={<div>تحميل المنشورات ذات الصلة...</div>}> <RelatedPosts postId={params.id} /> </Suspense> </article> ); }

حالات التحميل

يوفر Next.js ملف loading.tsx خاص لواجهة مستخدم التحميل التلقائية:

// app/posts/loading.tsx export default function Loading() { return ( <div className="loading-container"> <div className="skeleton"> <div className="skeleton-title"></div> <div className="skeleton-text"></div> <div className="skeleton-text"></div> <div className="skeleton-text"></div> </div> </div> ); } // app/posts/page.tsx // loading.tsx يلف هذا تلقائياً في Suspense export default async function PostsPage() { const posts = await fetch('https://api.example.com/posts') .then(res => res.json()); return ( <div> <h1>المنشورات</h1> {posts.map(post => ( <article key={post.id}> <h2>{post.title}</h2> </article> ))} </div> ); }
سلوك loading.tsx: يلف Next.js صفحتك تلقائياً في حد Suspense مع loading.tsx كاحتياطي. هذا يعادل لف الصفحة يدوياً مع Suspense.

المحتوى الشخصي مع SSR

SSR مثالي للمحتوى الخاص بالمستخدم الذي لا يمكن تخزينه مؤقتاً:

// app/profile/page.tsx import { cookies } from 'next/headers'; import { redirect } from 'next/navigation'; async function getUser() { const cookieStore = cookies(); const sessionToken = cookieStore.get('session'); if (!sessionToken) { redirect('/login'); } const res = await fetch('https://api.example.com/user', { headers: { Authorization: `Bearer ${sessionToken.value}` }, cache: 'no-store' // لا تخزن البيانات الخاصة بالمستخدم أبداً }); return res.json(); } export default async function ProfilePage() { const user = await getUser(); return ( <div> <h1>مرحباً، {user.name}!</h1> <div> <h2>تفاصيل الحساب</h2> <p>البريد الإلكتروني: {user.email}</p> <p>عضو منذ: {new Date(user.createdAt).toLocaleDateString()}</p> <p>نوع الحساب: {user.subscription}</p> </div> <div> <h2>الطلبات الأخيرة</h2> <ul> {user.orders.map(order => ( <li key={order.id}> طلب #{order.id} - ${order.total} </li> ))} </ul> </div> </div> ); }

البيانات الديناميكية مع التحميل المتوازي

جلب مصادر بيانات متعددة بشكل متوازٍ لأداء أفضل:

// app/dashboard/page.tsx import { Suspense } from 'react'; async function getUser() { const res = await fetch('https://api.example.com/user', { cache: 'no-store' }); return res.json(); } async function getNotifications() { const res = await fetch('https://api.example.com/notifications', { cache: 'no-store' }); return res.json(); } async function getStats() { const res = await fetch('https://api.example.com/stats', { cache: 'no-store' }); return res.json(); } // مكونات تجلب البيانات بشكل مستقل async function UserSection() { const user = await getUser(); return ( <div> <h2>{user.name}</h2> <p>{user.email}</p> </div> ); } async function NotificationsSection() { const notifications = await getNotifications(); return ( <div> <h2>الإشعارات ({notifications.length})</h2> {/* عرض الإشعارات */} </div> ); } async function StatsSection() { const stats = await getStats(); return ( <div> <h2>الإحصائيات</h2> <p>المشاهدات: {stats.views}</p> </div> ); } export default function DashboardPage() { return ( <div> <h1>لوحة التحكم</h1> {/* جميع الأقسام الثلاثة تحمل بشكل متوازٍ */} <Suspense fallback={<div>تحميل المستخدم...</div>}> <UserSection /> </Suspense> <Suspense fallback={<div>تحميل الإشعارات...</div>}> <NotificationsSection /> </Suspense> <Suspense fallback={<div>تحميل الإحصائيات...</div>}> <StatsSection /> </Suspense> </div> ); }
فائدة الأداء: مع Suspense، تبدأ جميع عمليات جلب البيانات الثلاث في وقت واحد. الصفحة لا تنتظر الطلب الأبطأ—كل قسم يظهر بمجرد أن تكون بياناته جاهزة.

معالجة الأخطاء في SSR

معالجة الأخطاء بشكل رشيق مع حدود الأخطاء:

// app/posts/error.tsx 'use client'; // حدود الأخطاء يجب أن تكون مكونات عميل import { useEffect } from 'react'; export default function Error({ error, reset }: { error: Error & { digest?: string }; reset: () => void; }) { useEffect(() => { console.error(error); }, [error]); return ( <div> <h2>حدث خطأ ما!</h2> <p>{error.message}</p> <button onClick={() => reset()}> حاول مرة أخرى </button> </div> ); } // app/posts/page.tsx export default async function PostsPage() { const res = await fetch('https://api.example.com/posts', { cache: 'no-store' }); if (!res.ok) { throw new Error('فشل جلب المنشورات'); } const posts = await res.json(); return ( <div> {posts.map(post => ( <article key={post.id}> <h2>{post.title}</h2> </article> ))} </div> ); }

رؤوس التحكم في التخزين المؤقت

التحكم في سلوك التخزين المؤقت على مستوى CDN/المتصفح:

// app/api/posts/route.ts export async function GET() { const posts = await fetchPosts(); return Response.json(posts, { headers: { // التخزين المؤقت لمدة 60 ثانية، إعادة التحقق في الخلفية 'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=30' } }); } // app/feed/route.ts export async function GET() { const feed = await generateFeed(); return new Response(feed, { headers: { 'Content-Type': 'application/xml', // لا تخزين مؤقت للخلاصات الشخصية 'Cache-Control': 'private, no-cache, no-store, must-revalidate' } }); }

تحسين الأداء

1. التحميل المسبق للبيانات

// lib/data.ts import { cache } from 'react'; // إزالة تكرار الطلبات عبر المكونات export const getPost = cache(async (id: string) => { const res = await fetch(`https://api.example.com/posts/${id}`, { cache: 'no-store' }); return res.json(); }); // دالة التحميل المسبق export function preloadPost(id: string) { void getPost(id); // بدء الجلب دون انتظار } // app/posts/[id]/layout.tsx import { preloadPost } from '@/lib/data'; export default function PostLayout({ children, params }: { children: React.ReactNode; params: { id: string }; }) { // بدء تحميل البيانات مبكراً preloadPost(params.id); return <div>{children}</div>; }

2. العرض الجزئي المسبق (تجريبي)

// next.config.js module.exports = { experimental: { ppr: true // تمكين العرض الجزئي المسبق } }; // app/dashboard/page.tsx // هيكل ثابت مع محتوى ديناميكي export default function DashboardPage() { return ( <div> {/* الأجزاء الثابتة تعرض فوراً */} <header> <h1>لوحة التحكم</h1> <nav>{/* التنقل */}</nav> </header> {/* الأجزاء الديناميكية تبث */} <Suspense fallback={<Skeleton />}> <DynamicContent /> </Suspense> </div> ); }
ملاحظة PPR: العرض الجزئي المسبق هو ميزة تجريبية تجمع بين العرض الثابت والديناميكي في نفس الصفحة. يتم عرض الهيكل الثابت مسبقاً بينما الأجزاء الديناميكية تبث عبر Suspense.

تمرين عملي

المهمة: أنشئ لوحة تحكم مستخدم مع SSR والبث:

  1. أنشئ صفحة لوحة تحكم تجلب بيانات خاصة بالمستخدم
  2. نفذ حدود Suspense متعددة لأقسام مختلفة
  3. أضف هياكل تحميل لكل قسم
  4. أنشئ حد خطأ للتعامل مع فشل الجلب
  5. نفذ جلب البيانات المتوازي لـ 3+ مصادر بيانات
  6. أضف فحص المصادقة باستخدام cookies()
  7. قم بالتحسين مع التحميل المسبق في التخطيط

المتطلبات:

  • استخدم TypeScript مع الأنواع الصحيحة
  • فرض العرض الديناميكي مع cache: 'no-store'
  • أنشئ حالات تحميل مميزة لكل قسم
  • عالج الأخطاء بشكل رشيق مع رسائل ودية للمستخدم
  • نفذ تدفق المصادقة الصحيح مع إعادة التوجيه

التحدي الإضافي: نفذ قسم إشعارات في الوقت الفعلي يجلب بيانات جديدة كل 30 ثانية باستخدام مكون عميل مع إجراءات خادم لتحديثات البيانات.

الملخص

  • SSR يعرض الصفحات عند الطلب لكل طلب مع بيانات جديدة
  • استخدم الدوال الديناميكية مثل cookies() و headers() و searchParams لتشغيل SSR
  • فرض العرض الديناميكي مع export const dynamic = 'force-dynamic'
  • البث مع Suspense يسمح بالعرض التدريجي للمحتوى
  • حدود Suspense المتعددة تمكن حالات التحميل الدقيقة
  • ملفات loading.tsx توفر واجهة مستخدم تحميل تلقائية لقطاعات المسار
  • SSR مثالي للمحتوى الشخصي الذي لا يمكن تخزينه مؤقتاً
  • جلب البيانات المتوازي مع Suspense يحسن الأداء المدرك
  • حدود الأخطاء تتعامل مع الفشل بشكل رشيق
  • استراتيجيات التحميل المسبق والتخزين المؤقت تحسن أداء SSR