إطار Next.js

المسارات المتوازية والاعتراضية

25 دقيقة الدرس 31 من 40

مقدمة إلى المسارات المتوازية والاعتراضية

يقدم Next.js 13+ أنماط توجيه قوية تتيح لك عرض صفحات متعددة في نفس الوقت واعتراض التنقل بين المسارات. تمكن هذه الأنماط المتقدمة من سلوكيات واجهة المستخدم المتطورة مثل النوافذ المنبثقة والعروض المنقسمة ولوحات التحكم المعقدة.

المسارات المتوازية

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

الفتحات: يتم تعريف المسارات المتوازية باستخدام فتحات مسماة، وهي مجلدات تعتمد على الاصطلاحات تبدأ برمز @ (على سبيل المثال، @team، @analytics).

بنية المسارات المتوازية الأساسية

app/
  dashboard/
    @team/              # فتحة مسماة لعرض الفريق
      page.tsx
    @analytics/         # فتحة مسماة لعرض التحليلات
      page.tsx
    layout.tsx          # التخطيط الأصلي الذي يستقبل كلتا الفتحتين
    page.tsx            # الصفحة الافتراضية

تنفيذ المسارات المتوازية

// app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
  team,      // فتحة @team
  analytics  // فتحة @analytics
}: {
  children: React.ReactNode;
  team: React.ReactNode;
  analytics: React.ReactNode;
}) {
  return (
    <div className="dashboard-layout">
      <div className="sidebar">{children}</div>
      <div className="main-content">
        <div className="team-section">{team}</div>
        <div className="analytics-section">{analytics}</div>
      </div>
    </div>
  );
}
// app/dashboard/@team/page.tsx
export default async function TeamSlot() {
  const teamData = await fetchTeamData();

  return (
    <div>
      <h2>نظرة عامة على الفريق</h2>
      <ul>
        {teamData.map(member => (
          <li key={member.id}>{member.name}</li>
        ))}
      </ul>
    </div>
  );
}

// حالة تحميل مستقلة
export default function Loading() {
  return <div>جارٍ تحميل بيانات الفريق...</div>;
}
// app/dashboard/@analytics/page.tsx
export default async function AnalyticsSlot() {
  const analytics = await fetchAnalytics();

  return (
    <div>
      <h2>لوحة التحليلات</h2>
      <div className="stats">
        <div>المشاهدات: {analytics.views}</div>
        <div>المستخدمون: {analytics.users}</div>
      </div>
    </div>
  );
}

نصيحة: يمكن أن يحتوي كل مسار متوازٍ على ملفات loading.tsx و error.tsx و not-found.tsx خاصة به، مما يوفر تحكمًا دقيقًا في حالات واجهة المستخدم.

الملفات الافتراضية للمسارات المتوازية

عند الانتقال إلى مسار لا يطابق جميع الفتحات، يحتاج Next.js إلى معرفة ما يجب عرضه. استخدم ملفات default.tsx للتعامل مع هذا:

app/
  dashboard/
    @team/
      page.tsx
      default.tsx       # احتياطي لفتحة @team
    @analytics/
      page.tsx
      default.tsx       # احتياطي لفتحة @analytics
    layout.tsx
// app/dashboard/@team/default.tsx
export default function DefaultTeam() {
  return <div>حدد فريقًا لعرض التفاصيل</div>;
}

المسارات الاعتراضية

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

الاصطلاح: تستخدم المسارات الاعتراضية اصطلاحات تسمية خاصة للمجلدات:

  • (.) - نفس المستوى
  • (..) - مستوى واحد للأعلى
  • (..)(..) - مستويان للأعلى
  • (...) - من الجذر

نمط النوافذ المنبثقة مع المسارات الاعتراضية

app/
  photos/
    page.tsx              # معرض الصور
    [id]/
      page.tsx            # صفحة الصورة الكاملة
  @modal/
    (.)photos/
      [id]/
        page.tsx          # عرض النافذة المنبثقة المعترضة
    default.tsx
  layout.tsx
// app/layout.tsx
export default function RootLayout({
  children,
  modal
}: {
  children: React.ReactNode;
  modal: React.ReactNode;
}) {
  return (
    <html>
      <body>
        {children}
        {modal}
      </body>
    </html>
  );
}
// app/photos/page.tsx
import Link from 'next/link';

export default function PhotoGallery() {
  const photos = [1, 2, 3, 4, 5];

  return (
    <div className="gallery">
      <h1>معرض الصور</h1>
      <div className="grid">
        {photos.map(id => (
          <Link key={id} href={`/photos/${id}`}>
            <img src={`/photo-${id}.jpg`} alt={`صورة ${id}`} />
          </Link>
        ))}
      </div>
    </div>
  );
}
// app/@modal/(.)photos/[id]/page.tsx
import { Modal } from '@/components/Modal';

export default function PhotoModal({ params }: { params: { id: string } }) {
  return (
    <Modal>
      <img
        src={`/photo-${params.id}.jpg`}
        alt={`صورة ${params.id}`}
        className="modal-image"
      />
    </Modal>
  );
}
// components/Modal.tsx
'use client';

import { useRouter } from 'next/navigation';
import { useEffect, useRef } from 'react';

export function Modal({ children }: { children: React.ReactNode }) {
  const router = useRouter();
  const dialogRef = useRef<HTMLDialogElement>(null);

  useEffect(() => {
    dialogRef.current?.showModal();
  }, []);

  const closeModal = () => {
    router.back();
  };

  return (
    <dialog
      ref={dialogRef}
      className="modal"
      onClose={closeModal}
      onClick={(e) => {
        if (e.target === dialogRef.current) {
          closeModal();
        }
      }}
    >
      <button onClick={closeModal} className="close-btn">
        إغلاق
      </button>
      {children}
    </dialog>
  );
}

فتحة النافذة المنبثقة الافتراضية

// app/@modal/default.tsx
export default function DefaultModal() {
  return null; // لا تعرض النافذة المنبثقة افتراضيًا
}

أنماط المسارات المتوازية المتقدمة

العرض الشرطي للفتحات

// app/dashboard/layout.tsx
export default async function DashboardLayout({
  children,
  team,
  analytics
}: {
  children: React.ReactNode;
  team: React.ReactNode;
  analytics: React.ReactNode;
}) {
  const session = await getSession();
  const isAdmin = session?.role === 'admin';

  return (
    <div className="dashboard">
      {children}
      {isAdmin ? (
        <>
          <div className="slot">{team}</div>
          <div className="slot">{analytics}</div>
        </>
      ) : (
        <div className="slot">{team}</div>
      )}
    </div>
  );
}

التنقل المستند إلى علامات التبويب مع المسارات المتوازية

app/
  profile/
    @tabs/
      posts/
        page.tsx
      followers/
        page.tsx
      following/
        page.tsx
      default.tsx
    layout.tsx
    page.tsx
// app/profile/layout.tsx
'use client';

import Link from 'next/link';
import { usePathname } from 'next/navigation';

export default function ProfileLayout({
  children,
  tabs
}: {
  children: React.ReactNode;
  tabs: React.ReactNode;
}) {
  const pathname = usePathname();

  return (
    <div>
      {children}
      <nav className="tabs">
        <Link
          href="/profile/posts"
          className={pathname === '/profile/posts' ? 'active' : ''}
        >
          المنشورات
        </Link>
        <Link
          href="/profile/followers"
          className={pathname === '/profile/followers' ? 'active' : ''}
        >
          المتابعون
        </Link>
        <Link
          href="/profile/following"
          className={pathname === '/profile/following' ? 'active' : ''}
        >
          المتابَعون
        </Link>
      </nav>
      <div className="tab-content">{tabs}</div>
    </div>
  );
}

دمج المسارات المتوازية والاعتراضية

app/
  feed/
    page.tsx
    [id]/
      page.tsx           # صفحة المنشور الكاملة
  @modal/
    (.)feed/
      [id]/
        page.tsx         # النافذة المنبثقة المعترضة
    default.tsx
  layout.tsx
// app/feed/page.tsx
import Link from 'next/link';

export default function Feed() {
  const posts = await getPosts();

  return (
    <div className="feed">
      {posts.map(post => (
        <Link key={post.id} href={`/feed/${post.id}`}>
          <article>
            <h2>{post.title}</h2>
            <p>{post.excerpt}</p>
          </article>
        </Link>
      ))}
    </div>
  );
}

تحذير: عند استخدام المسارات الاعتراضية مع المسارات المتوازية، كن حذرًا من مطابقة المسارات. ستعرض عمليات التحديث الصلبة دائمًا المسار غير المعترض.

أفضل ممارسات التخطيطات المستندة إلى الفتحات

حدود الأخطاء المستقلة

// app/dashboard/@analytics/error.tsx
'use client';

export default function AnalyticsError({
  error,
  reset
}: {
  error: Error;
  reset: () => void;
}) {
  return (
    <div className="error-container">
      <h2>فشل تحميل التحليلات</h2>
      <p>{error.message}</p>
      <button onClick={reset}>حاول مرة أخرى</button>
    </div>
  );
}

أدوات التنقل في الفتحات

// lib/parallel-routes.ts
export function createSlotUrl(base: string, slots: Record<string, string>) {
  const params = new URLSearchParams();
  Object.entries(slots).forEach(([key, value]) => {
    params.set(key, value);
  });
  return `${base}?${params.toString()}`;
}

// الاستخدام
const url = createSlotUrl('/dashboard', {
  team: 'engineering',
  analytics: 'weekly'
});
// النتيجة: /dashboard?team=engineering&analytics=weekly

تمرين: بناء معرض منتجات مع معاينة نافذة منبثقة

أنشئ معرض منتجات يتضمن:

  1. عرض المنتجات في تخطيط شبكي
  2. فتح نافذة منبثقة عند النقر على منتج (مسار اعتراضي)
  3. السماح بالتنقل المباشر إلى صفحة المنتج الكاملة
  4. استخدام المسارات المتوازية للمراجعات والمنتجات ذات الصلة
  5. تنفيذ حالات تحميل مستقلة لكل فتحة

مكافأة: أضف إدارة حالة URL حتى يمكن مشاركة النافذة المنبثقة عبر الرابط.

اعتبارات الأداء

  • البث: يمكن للمسارات المتوازية البث بشكل مستقل، مما يحسن الأداء المُدرَك
  • تقسيم الكود: يتم تقسيم كل فتحة تلقائيًا
  • حدود Suspense: قم بلف الفتحات في Suspense للحصول على تجربة تحميل أفضل
  • التحميل المسبق: استخدم Link prefetch للتنقل المتوقع

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