البث (Streaming)

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

ما هو البث؟

البث (Streaming) هو تقنية نقل بيانات تتيح لك تقسيم المسار إلى "قطع" صغيرة وبثها تدريجيًا من الخادم إلى العميل بمجرد أن تصبح جاهزة.

مخطط يوضح الوقت مع جلب البيانات التسلسلي والجلب المتوازي للبيانات

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

مخطط يوضح الوقت مع جلب البيانات التسلسلي والجلب المتوازي للبيانات

يعمل البث بشكل جيد مع نموذج مكونات React، حيث يمكن اعتبار كل مكون قطعة.

هناك طريقتان لتنفيذ البث في Next.js:

  1. على مستوى الصفحة، باستخدام ملف loading.tsx (الذي ينشئ <Suspense> لك تلقائيًا).
  2. على مستوى المكون، باستخدام <Suspense> للتحكم الأكثر دقة.

دعونا نرى كيف يعمل هذا.

بث صفحة كاملة باستخدام loading.tsx

في مجلد /app/dashboard، أنشئ ملفًا جديدًا يسمى loading.tsx:

/app/dashboard/loading.tsx
export default function Loading() {
  return <div>جار التحميل...</div>;
}

قم بتحديث http://localhost:3000/dashboard، ويجب أن ترى الآن:

صفحة لوحة التحكم مع نص 'جار التحميل...'

هناك عدة أشياء تحدث هنا:

  1. loading.tsx هو ملف خاص في Next.js مبني على React Suspense. يسمح لك بإنشاء واجهة مستخدم احتياطية لعرضها كبديل أثناء تحميل محتوى الصفحة.
  2. نظرًا لأن <SideNav> ثابت، يتم عرضه على الفور. يمكن للمستخدم التفاعل مع <SideNav> أثناء تحميل المحتوى الديناميكي.
  3. لا يحتاج المستخدم إلى انتظار انتهاء تحميل الصفحة قبل الانتقال بعيدًا (يسمى هذا التنقل القابل للمقاطعة).

تهانينا! لقد قمت بتنفيذ البث. لكن يمكننا فعل المزيد لتحسين تجربة المستخدم. دعونا نعرض هيكل تحميل بدلاً من نص جار التحميل....

إضافة هياكل التحميل

هيكل التحميل (loading skeleton) هو نسخة مبسطة من واجهة المستخدم. تستخدم العديد من المواقع الإلكترونية هذه الهياكل كعنصر نائب (أو احتياطي) للإشارة إلى المستخدمين أن المحتوى قيد التحميل. أي واجهة مستخدم تضيفها في loading.tsx سيتم تضمينها كجزء من الملف الثابت وإرسالها أولاً. ثم يتم بث باقي المحتوى الديناميكي من الخادم إلى العميل.

داخل ملف loading.tsx، قم باستيراد مكون جديد يسمى <DashboardSkeleton>:

/app/dashboard/loading.tsx
import DashboardSkeleton from '@/app/ui/skeletons';
 
export default function Loading() {
  return <DashboardSkeleton />;
}

ثم قم بتحديث http://localhost:3000/dashboard، ويجب أن ترى الآن:

صفحة لوحة التحكم مع هياكل التحميل

إصلاح مشكلة هيكل التحميل باستخدام مجموعات المسارات

حاليًا، سيتم تطبيق هيكل التحميل الخاص بك على الفواتير.

نظرًا لأن loading.tsx موجود على مستوى أعلى من /invoices/page.tsx و /customers/page.tsx في نظام الملفات، فإنه يتم تطبيقه أيضًا على هذه الصفحات.

يمكننا تغيير هذا باستخدام مجموعات المسارات (Route Groups). أنشئ مجلدًا جديدًا يسمى /(overview) داخل مجلد لوحة التحكم. ثم انقل ملفات loading.tsx و page.tsx داخل المجلد:

هيكل المجلد يوضح كيفية إنشاء مجموعة مسارات باستخدام الأقواس

الآن، سيتم تطبيق ملف loading.tsx فقط على صفحة نظرة عامة على لوحة التحكم الخاصة بك.

تسمح لك مجموعات المسارات بتنظيم الملفات في مجموعات منطقية دون التأثير على هيكل مسار URL. عند إنشاء مجلد جديد باستخدام الأقواس ()، لن يتم تضمين الاسم في مسار URL. لذا يصبح /dashboard/(overview)/page.tsx هو /dashboard.

هنا، أنت تستخدم مجموعة مسارات للتأكد من أن loading.tsx ينطبق فقط على صفحة نظرة عامة على لوحة التحكم. ومع ذلك، يمكنك أيضًا استخدام مجموعات المسارات لفصل تطبيقك إلى أقسام (مثل مسارات (marketing) ومسارات (shop)) أو حسب الفرق للتطبيقات الأكبر.

بث مكون

حتى الآن، أنت تبث صفحة كاملة. ولكن يمكنك أيضًا أن تكون أكثر دقة وبث مكونات محددة باستخدام React Suspense.

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

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

للقيام بذلك، ستحتاج إلى نقل جلب البيانات إلى المكون، دعونا نحدث الكود لمعرفة كيف سيبدو:

احذف جميع حالات fetchRevenue() وبياناتها من /dashboard/(overview)/page.tsx:

/app/dashboard/(overview)/page.tsx
import { Card } from '@/app/ui/dashboard/cards';
import RevenueChart from '@/app/ui/dashboard/revenue-chart';
import LatestInvoices from '@/app/ui/dashboard/latest-invoices';
import { lusitana } from '@/app/ui/fonts';
import { fetchLatestInvoices, fetchCardData } from '@/app/lib/data'; // إزالة fetchRevenue
 
export default async function Page() {
  const revenue = await fetchRevenue() // احذف هذا السطر
  const latestInvoices = await fetchLatestInvoices();
  const {
    numberOfInvoices,
    numberOfCustomers,
    totalPaidInvoices,
    totalPendingInvoices,
  } = await fetchCardData();
 
  return (
    // ...
  );
}

ثم، قم باستيراد <Suspense> من React، وقم بلفه حول <RevenueChart />. يمكنك تمرير مكون احتياطي يسمى <RevenueChartSkeleton>.

/app/dashboard/(overview)/page.tsx
import { Card } from '@/app/ui/dashboard/cards';
import RevenueChart from '@/app/ui/dashboard/revenue-chart';
import LatestInvoices from '@/app/ui/dashboard/latest-invoices';
import { lusitana } from '@/app/ui/fonts';
import { fetchLatestInvoices, fetchCardData } from '@/app/lib/data';
import { Suspense } from 'react';
import { RevenueChartSkeleton } from '@/app/ui/skeletons';
 
export default async function Page() {
  const latestInvoices = await fetchLatestInvoices();
  const {
    numberOfInvoices,
    numberOfCustomers,
    totalPaidInvoices,
    totalPendingInvoices,
  } = await fetchCardData();
 
  return (
    <main>
      <h1 className={`${lusitana.className} mb-4 text-xl md:text-2xl`}>
        لوحة التحكم
      </h1>
      <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
        <Card title="المحصلة" value={totalPaidInvoices} type="collected" />
        <Card title="المعلقة" value={totalPendingInvoices} type="pending" />
        <Card title="إجمالي الفواتير" value={numberOfInvoices} type="invoices" />
        <Card
          title="إجمالي العملاء"
          value={numberOfCustomers}
          type="customers"
        />
      </div>
      <div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-8">
        <Suspense fallback={<RevenueChartSkeleton />}>
          <RevenueChart />
        </Suspense>
        <LatestInvoices latestInvoices={latestInvoices} />
      </div>
    </main>
  );
}

أخيرًا، قم بتحديث مكون <RevenueChart> لجلب بياناته الخاصة وإزالة الخاصية الممررة إليه:

/app/ui/dashboard/revenue-chart.tsx
import { generateYAxis } from '@/app/lib/utils';
import { CalendarIcon } from '@heroicons/react/24/outline';
import { lusitana } from '@/app/ui/fonts';
import { fetchRevenue } from '@/app/lib/data';
 
// ...
 
export default async function RevenueChart() { // اجعل المكون غير متزامن، أزل الخصائص
  const revenue = await fetchRevenue(); // جلب البيانات داخل المكون
 
  const chartHeight = 350;
  const { yAxisLabels, topLabel } = generateYAxis(revenue);
 
  if (!revenue || revenue.length === 0) {
    return <p className="mt-4 text-gray-400">لا توجد بيانات متاحة.</p>;
  }
 
  return (
    // ...
  );
}
 

الآن قم بتحديث الصفحة، يجب أن ترى معلومات لوحة التحكم على الفور تقريبًا، بينما يتم عرض هيكل احتياطي لـ <RevenueChart>:

صفحة لوحة التحكم مع هيكل مخطط الإيرادات ومكونات البطاقات والفواتير الأخيرة المحملة

تمرين: بث <LatestInvoices>

الآن حان دورك! تدرب على ما تعلمته للتو عن طريق بث مكون <LatestInvoices>.

انقل fetchLatestInvoices() من الصفحة إلى مكون <LatestInvoices>. قم بلف المكون في حدود <Suspense> مع هيكل احتياطي يسمى <LatestInvoicesSkeleton>.

بمجرد أن تكون جاهزًا، قم بتوسيع التبديل لرؤية كود الحل:

تجميع المكونات

رائع! لقد اقتربت من النهاية، الآن تحتاج إلى لف مكونات <Card> في Suspense. يمكنك جلب بيانات لكل بطاقة على حدة، ولكن هذا قد يؤدي إلى تأثير الظهور المفاجئ أثناء تحميل البطاقات، مما قد يكون مزعجًا بصريًا للمستخدم.

إذن، كيف يمكنك حل هذه المشكلة؟

لإنشاء تأثير متدرج أكثر، يمكنك تجميع البطاقات باستخدام مكون غلاف. هذا يعني أنه سيتم عرض <SideNav/> الثابت أولاً، ثم البطاقات، وهكذا.

في ملف page.tsx:

  1. احذف مكونات <Card> الخاصة بك.
  2. احذف دالة fetchCardData().
  3. استورد مكون غلاف جديد يسمى <CardWrapper />.
  4. استورد مكون هيكل جديد يسمى <CardsSkeleton />.
  5. لف <CardWrapper /> في Suspense.
/app/dashboard/(overview)/page.tsx
import CardWrapper from '@/app/ui/dashboard/cards';
// ...
import {
  RevenueChartSkeleton,
  LatestInvoicesSkeleton,
  CardsSkeleton,
} from '@/app/ui/skeletons';
 
export default async function Page() {
  return (
    <main>
      <h1 className={`${lusitana.className} mb-4 text-xl md:text-2xl`}>
        لوحة التحكم
      </h1>
      <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
        <Suspense fallback={<CardsSkeleton />}>
          <CardWrapper />
        </Suspense>
      </div>
      // ...
    </main>
  );
}

ثم، انتقل إلى الملف /app/ui/dashboard/cards.tsx، استورد دالة fetchCardData()، واستدعها داخل مكون <CardWrapper/>. تأكد من إلغاء تعليق أي كود ضروري في هذا المكون.

/app/ui/dashboard/cards.tsx
// ...
import { fetchCardData } from '@/app/lib/data';
 
// ...
 
export default async function CardWrapper() {
  const {
    numberOfInvoices,
    numberOfCustomers,
    totalPaidInvoices,
    totalPendingInvoices,
  } = await fetchCardData();
 
  return (
    <>
      <Card title="المحصلة" value={totalPaidInvoices} type="collected" />
      <Card title="المعلقة" value={totalPendingInvoices} type="pending" />
      <Card title="إجمالي الفواتير" value={numberOfInvoices} type="invoices" />
      <Card
        title="إجمالي العملاء"
        value={numberOfCustomers}
        type="customers"
      />
    </>
  );
}

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

تحديد مكان وضع حدود Suspense الخاصة بك

يعتمد مكان وضع حدود Suspense الخاصة بك على عدة أشياء:

  1. كيف تريد أن يختبر المستخدم الصفحة أثناء بثها.
  2. أي محتوى تريد إعطاؤه الأولوية.
  3. إذا كانت المكونات تعتمد على جلب البيانات.

ألق نظرة على صفحة لوحة التحكم الخاصة بك، هل هناك أي شيء كنت ستفعله بشكل مختلف؟

لا تقلق. لا توجد إجابة صحيحة.

  • يمكنك بث الصفحة بأكملها كما فعلنا مع loading.tsx... ولكن هذا قد يؤدي إلى وقت تحميل أطول إذا كان أحد المكونات لديه جلب بيانات بطيء.
  • يمكنك بث كل مكون على حدة... ولكن هذا قد يؤدي إلى ظهور واجهة المستخدم فجأة في الشاشة عندما تصبح جاهزة.
  • يمكنك أيضًا إنشاء تأثير متدرج عن طريق بث أقسام الصفحة. ولكنك ستحتاج إلى إنشاء مكونات غلاف.

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

لا تخف من تجربة Suspense ومعرفة ما يناسبك بشكل أفضل، إنها واجهة برمجة تطبيقات قوية يمكن أن تساعدك في إنشاء تجارب مستخدم أكثر إمتاعًا.

نظرة مستقبلية

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

في الفصل التالي، ستتعلم عن التصيير الجزئي المسبق (Partial Prerendering)، وهو نموذج تصيير جديد في Next.js مبني مع وضع البث في الاعتبار.