Next.js: المسارات المتوازية ومسارات الاعتراض

آخر تحديث: 2026-08-26

التوجيه المتوازي يشبه الشاشات المتعددة - كل شاشة تعمل بشكل مستقل ولكنها مترابطة؛ توجيه الاعتراض يشبه نقطة استلام الطرود - يعترض الطرود على طول الطريق، ويعالجها، ثم يواصل التسليم.

1. ما ستتعلمه



2. قصة حقيقية لمدير منتج

(1) نقطة الألم: تجربة سيئة للنوافذ المنبثقة في تدفق المحتوى

أليس تعمل على تطوير ميزة تصفح الصور لمنصة محتوى وواجهت بعض مشكلات تجربة المستخدم:

"عندما نقر المستخدم على صورة في تدفق المحتوى، عرضنا نافذة منبثقة بملء الشاشة، وتغير عنوان URL إلى /photos/123. ونتيجة لذلك، عندما حاول المستخدم العودة إلى تدفق المحتوى بالضغط على زر الرجوع في المتصفح، تم تسجيل خروجه من التطبيق بالكامل. اشتكى المستخدم: 'أردت فقط إلقاء نظرة على صورة - لماذا لا يمكنني العودة إلى الصفحة الرئيسية؟'"

المشكلات التي حللتها أليس:

المشكلة التأثير تقييم المستخدم
لا يمكن تحديث النافذة المنبثقة بشكل مستقل تحديث النافذة المنبثقة يؤدي إلى تحديث الصفحة بالكامل 2/5
زر الرجوع يخرج من التطبيق تعطيل تدفق المستخدم 1.5/5
الشريط الجانبي والمحتوى الرئيسي غير متزامنين التنقل مربك 2.5/5
عرض الفريق ولوحة التحكم منفصلان كفاءة إدارة منخفضة 3/5

(2) حل Parallel + Intercepting Routes

استخدم @modal لعرض نافذة منبثقة للتوجيه المتوازي + (.)photo لاعتراض التنقل إلى الصور في تدفق المحتوى.

TEXT 📖 للعرض فقط
src/app/
├── layout.tsx              # التخطيط الرئيسي: @children + @modal
└── (feed)/
    ├── layout.tsx          # تخطيط تدفق المحتوى: @children + @sidebar
    ├── page.tsx            # الصفحة الرئيسية لتدفق المحتوى
    └── photos/
        ├── [id]/
        │   └── page.tsx    # صفحة كاملة: /photos/123
        └── (.)[id]/
            └── page.tsx    # اعتراض: فتح كنافذة منبثقة في تدفق المحتوى

(3) النتائج

البُعد قبل (التوجيه القياسي) بعد (متوازي + اعتراض)
تجربة تصفح الصور إعادة توجيه لصفحة كاملة، تقطع التصفح معاينة منبثقة، تبقى في الصفحة الحالية
سلوك زر الرجوع الخروج من التطبيق إغلاق النافذة المنبثقة والعودة إلى تدفق المحتوى
يمكن تحديث الصفحة غير مدعوم التحديث يعرض الصفحة بالكامل؛ يُصيّر بشكل طبيعي
الشريط الجانبي مستقل غير مستقل @sidebar يُصيّر بشكل مستقل، دون التأثير على المحتوى الرئيسي


3. Parallel Routes

(1) المفاهيم والاستخدام

100%
graph TB
    subgraph "URL: /dashboard"
        A[layout.tsx] --> B[children<br/>المحتوى الرئيسي]
        A --> C[@modal<br/>فتحة النافذة المنبثقة]
        A --> D[@sidebar<br/>فتحة الشريط الجانبي]
        A --> E[@team<br/>فتحة الفريق]
    end

    subgraph "نتائج التصيير"
        F[منطقة المحتوى الرئيسي] & G[منطقة النافذة المنبثقة] & H[منطقة الشريط الجانبي] & I[منطقة الفريق]
    end

    style A fill:#cce5ff
    style B fill:#d4edda
اسم الفتحة بادئة الدليل التأثير على URL حالة الاستخدام
@children لا شيء (افتراضي) URL قياسي محتوى الصفحة الرئيسي
@modal @modal/ لا شيء النوافذ المنبثقة، مربعات الحوار
@sidebar @sidebar/ لا شيء لوحة الشريط الجانبي
@team @team/ لا شيء عرض الفريق

▶ مثال: Parallel Routes الأساسية

المخرجات:

TEXT 📖 للعرض فقط
رسم توضيحي لهيكل المسارات: مسارات نظام الملفات تقابل مسارات URL.
TSX
// ============================================
// أساسيات Parallel Routes: تخطيط متعدد الفتحات للوحة التحكم
// ============================================

// src/app/(dashboard)/layout.tsx — تخطيط التوجيه المتوازي
export default function DashboardLayout({
  children,
  modal,
  sidebar,
  team,
}: {
  children: React.ReactNode;
  modal: React.ReactNode;
  sidebar: React.ReactNode;
  team: React.ReactNode;
}) {
  return (
    <div className="flex h-screen">
      {/* منطقة المحتوى الرئيسي */}
      <main className="flex-1 p-8 overflow-auto">
        {children}
      </main>

      {/* فتحة الشريط الجانبي(تصيير متوازي مستقل) */}
      <aside className="w-72 bg-gray-50 p-4 border-l">
        {sidebar}
      </aside>

      {/* فتحة الفريق(تصيير متوازي مستقل) */}
      <aside className="w-64 bg-gray-900 text-white p-4">
        {team}
      </aside>

      {/* فتحة النافذة المنبثقة(تصيير شرطي) */}
      {modal}
    </div>
  );
}

المخرجات:

TEXT 📖 للعرض فقط
يُصيّر: تخطيط لوحة التحكم مع شريط جانبي داكن (روابط لوحة التحكم/المشاريع/الإعدادات) + منطقة المحتوى الرئيسي.
TSX
// src/app/(dashboard)/@sidebar/default.tsx — الحالة الافتراضية للشريط الجانبي
export default function SidebarDefault() {
  return (
    <div>
      <h3 className="font-bold text-lg mb-4">الشريط الجانبي</h3>
      <div className="space-y-2">
        <div className="p-3 bg-white rounded shadow-sm">
          <p className="font-medium">النشاط الأخير</p>
          <p className="text-sm text-gray-500">لا يوجد نشاط حديث</p>
        </div>
        <div className="p-3 bg-white rounded shadow-sm">
          <p className="font-medium">الإشعارات</p>
          <p className="text-sm text-gray-500">3 غير مقروءة</p>
        </div>
      </div>
    </div>
  );
}
TSX
// src/app/(dashboard)/@team/default.tsx — حالة فتحة الفريق الافتراضية
export default function TeamDefault() {
  return (
    <div className="p-4">
      <h3 className="font-bold mb-4">الفريق</h3>
      <div className="space-y-3">
        {["أليس", "بوب", "تشارلي", "ديانا"].map(name => (
          <div key={name} className="flex items-center gap-2">
            <div className="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center text-white text-sm">
              {name[0]}
            </div>
            <span className="text-sm">{name}</span>
          </div>
        ))}
      </div>
    </div>
  );
}

المخرجات:

TEXT 📖 للعرض فقط
زيارة /dashboard:
┌──────────────────────┬──────────┬──────────┐
│                      │          │          │
│   منطقة المحتوى الرئيسي      │ الشريط الجانبي │  الفريق    │
│   لوحة التحكم         │ النشاط   │  أليس     │
│   مرحبًا بعودتك!      │ الأخير   │  بوب      │
│                      │ إشعارات(3)│  تشارلي   │
│                      │          │  ديانا    │
└──────────────────────┴──────────┴──────────┘

ثلاث مناطق تُصيّر بشكل مستقل، لا تؤثر على بعضها البعض

(2) default.tsx — الحالة الافتراضية المطلوبة

يجب أن يحتوي كل دليل @slot على default.tsx، والذي يُعرض عندما لا يتم العثور على مسار مطابق.

100%
graph TB
    A[تنقل المستخدم] --> B{عنوان URL الحالي<br/>يطابق مسار الفتحة؟}
    B -->|يطابق| C[عرض page.tsx الخاص بالفتحة]
    B -->|لا يطابق| D[عرض default.tsx الخاص بالفتحة]

    style C fill:#d4edda
    style D fill:#f8d7da

▶ مثال: أهمية default.tsx

المخرجات:

TEXT 📖 للعرض فقط
رسم توضيحي: تنقل المستخدم؛ عرض page.tsx الخاص بالفتحة؛ عرض default.tsx الخاص بالفتحة.
TSX
// ============================================
// بدون default.tsx سيحدث خطأ 404
// كل @slot يجب أن يحتوي على default.tsx
// ============================================

// src/app/@modal/default.tsx — النافذة المنبثقة مخفية افتراضيًا
export default function ModalDefault() {
  return null; // لا توجد نافذة منبثقة، لا يُصيّر أي شيء
}

// src/app/(dashboard)/@sidebar/default.tsx
export default function SidebarDefault() {
  return (
    <div className="p-4">
      <h3 className="font-bold text-sm text-gray-500 uppercase">
        روابط سريعة
      </h3>
      <nav className="mt-3 space-y-2">
        <a href="/dashboard" className="block text-blue-600">لوحة التحكم</a>
        <a href="/dashboard/projects" className="block text-blue-600">المشاريع</a>
        <a href="/dashboard/settings" className="block text-blue-600">الإعدادات</a>
      </nav>
    </div>
  );
}

المخرجات:

TEXT 📖 للعرض فقط
يتضمن شريطًا جانبيًا.

المخرجات:

TEXT 📖 للعرض فقط
زيارة /dashboard:
- @modal لم يتم العثور على مسار مطابق → عرض ModalDefault (= null، لا يُصيّر)
- @sidebar لم يتم العثور على مسار مطابق → عرض SidebarDefault(روابط سريعة للتنقل)
- @team لم يتم العثور على مسار مطابق → عرض TeamDefault(قائمة أعضاء الفريق)

زيارة /dashboard/photos/1(بافتراض أن @modal وجد تطابقًا):
- @modal يطابق → عرض محتوى النافذة المنبثقة
- الفتحات الأخرى تعرض default.tsx الخاص بكل منها


4. Intercepting Routes

(1) قواعد مطابقة الاعتراض

100%
graph TB
    A[حاليًا في تدفق المحتوى] --> B{النقر على رابط الصورة}
    B --> C[اعتراض (.)photo]
    C --> D[عرض نافذة منبثقة في تدفق المحتوى]
    D --> E[يقوم المستخدم بتحديث الصفحة]
    E --> F[تجاوز الاعتراض<br/>عرض الصفحة بالكامل]

    style C fill:#cce5ff
    style D fill:#d4edda
    style F fill:#f8d7da
الصيغة مستوى المطابقة مثال
(.) نفس المستوى feed/photos/(.)[id] يعترض feed/photos/[id]
(..) مستوى واحد للأعلى feed/(..)photos/[id] يعترض photos/[id]
(..)(..) مستويين للأعلى feed/(..)(..)photos/[id] يعترض على مستوى الجذر photos/[id]
(...) مستوى الجذر feed/(...)photos/[id] يعترض app/photos/[id]

▶ مثال: اعتراض تصفح الصور

المخرجات:

TEXT 📖 للعرض فقط
رسم توضيحي: حاليًا في تدفق المحتوى؛ اعتراض (.)photo؛ عرض نافذة منبثقة في تدفق المحتوى؛ يقوم المستخدم بتحديث الصفحة؛ تجاوز الاعتراض عرض الصفحة بالكامل.
TSX
// ============================================
// Intercepting Route + Parallel Route
// النقر على الصورة في تدفق المحتوى → عرض نافذة منبثقة في الصفحة الحالية
// ============================================

// هيكل الدليل:
// app/
//   layout.tsx                    # التخطيط الجذري (مع @modal)
//   (feed)/
//     page.tsx                    # تدفق المحتوى
//     photos/
//       [id]/page.tsx             # صفحة كاملة: /photos/1
//   @modal/
//     default.tsx                 # لا نافذة منبثقة
//     (.)photos/
//       [id]/page.tsx             # اعتراض: عرض نافذة منبثقة في تدفق المحتوى

// src/app/layout.tsx — التخطيط الجذري (مع فتحة النافذة المنبثقة)
export default function RootLayout({
  children,
  modal,
}: {
  children: React.ReactNode;
  modal: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        {children}
        {modal}
      </body>
    </html>
  );
}

// src/app/(feed)/page.tsx — صفحة تدفق المحتوى
import Link from "next/link";

export default function FeedPage() {
  const photos = Array.from({ length: 12 }, (_, i) => ({
    id: i + 1,
    url: `https://picsum.photos/seed/${i + 1}/300/300`,
    title: `صورة ${i + 1}`,
  }));

  return (
    <div className="max-w-4xl mx-auto p-8">
      <h1 className="text-3xl font-bold mb-6">تدفق الصور</h1>
      <div className="grid grid-cols-3 gap-4">
        {photos.map(photo => (
          <Link
            key={photo.id}
            href={`/photos/${photo.id}`}
            className="block overflow-hidden rounded-lg hover:opacity-90 transition-opacity"
          >
            <img
              src={photo.url}
              alt={photo.title}
              className="w-full h-64 object-cover"
            />
            <p className="mt-2 text-sm font-medium text-center">{photo.title}</p>
          </Link>
        ))}
      </div>
    </div>
  );
}

المخرجات:

TEXT 📖 للعرض فقط
يُصيّر قائمة ديناميكية من العناصر باستخدام map().
المحتوى المرئي: تدفق الصور
TSX
// ============================================
// مسارات الاعتراض: فتح نافذة منبثقة في تدفق المحتوى
// الملف: src/app/@modal/(.)photos/[id]/page.tsx
// ============================================

'use client';

import { useRouter } from "next/navigation";

export default function PhotoModal({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const router = useRouter();
  const { id } = params;

  return (
    // قناع الخلفية
    <div
      className="fixed inset-0 bg-black/70 flex items-center justify-center z-50"
      onClick={() => router.back()}
    >
      {/* محتوى النافذة المنبثقة — النقر على الخلفية للإغلاق */}
      <div
        className="bg-white rounded-2xl overflow-hidden max-w-2xl w-full mx-4"
        onClick={e => e.stopPropagation()}
      >
        <img
          src={`https://picsum.photos/seed/${id}/800/600`}
          alt={`صورة ${id}`}
          className="w-full h-auto"
        />
        <div className="p-6">
          <div className="flex items-center justify-between">
            <div>
              <h2 className="text-xl font-bold">صورة #{id}</h2>
              <p className="text-gray-500 text-sm mt-1">
                التقطها المصور
              </p>
            </div>
            <button
              onClick={() => router.back()}
              className="w-10 h-10 bg-gray-100 rounded-full flex items-center justify-center hover:bg-gray-200"
            >
              ✕
            </button>
          </div>
          <div className="flex gap-2 mt-4">
            <span className="px-3 py-1 bg-blue-100 text-blue-700 rounded-full text-sm">
              طبيعة
            </span>
            <span className="px-3 py-1 bg-green-100 text-green-700 rounded-full text-sm">
              مناظر طبيعية
            </span>
            <span className="px-3 py-1 bg-purple-100 text-purple-700 rounded-full text-sm">
              عالية الدقة
            </span>
          </div>
        </div>
      </div>
    </div>
  );
}
TSX
// ============================================
// الصفحة الكاملة: عرض الصفحة الكاملة عند الوصول المستقل
// الملف: src/app/photos/[id]/page.tsx
// ============================================

import Link from "next/link";

export default function PhotoPage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = params;

  return (
    <div className="max-w-4xl mx-auto p-8">
      <Link
        href="/"
        className="text-blue-600 hover:underline mb-4 inline-block"
      >
        &larr; العودة إلى تدفق المحتوى
      </Link>
      <img
        src={`https://picsum.photos/seed/${id}/1200/800`}
        alt={`صورة ${id}`}
        className="w-full rounded-lg"
      />
      <div className="mt-6">
        <h1 className="text-3xl font-bold">صورة #{id}</h1>
        <p className="text-gray-500 mt-2">
          عرض الصفحة الكاملة للصورة {id}. هذه الصفحة يمكن الوصول إليها مباشرة
          وتعمل حتى بدون JavaScript.
        </p>
        <div className="flex gap-4 mt-6">
          <Link
            href={`/photos/${Number(id) - 1}`}
            className="px-4 py-2 bg-gray-100 rounded-lg hover:bg-gray-200"
          >
            ← السابق
          </Link>
          <Link
            href={`/photos/${Number(id) + 1}`}
            className="px-4 py-2 bg-gray-100 rounded-lg hover:bg-gray-200"
          >
            التالي →
          </Link>
        </div>
      </div>
    </div>
  );
}

المخرجات:

TEXT 📖 للعرض فقط
السيناريو 1: النقر على صورة من تدفق المحتوى
- حاليًا في / (شبكة تدفق الصور)
- النقر على الصورة 1
- عرض صورة أكبر في نافذة منبثقة(يتغير شريط العنوان إلى /photos/1)
- النقر على قناع الخلفية → router.back() → العودة إلى تدفق المحتوى
- النقر على زر الرجوع في المتصفح → إغلاق النافذة المنبثقة → تدفق المحتوى يبقى دون تغيير

السيناريو 2: الوصول المباشر إلى /photos/1
- تخطي مسارات الاعتراض
- عرض الصفحة الكاملة (صورة كبيرة بعرض كامل + أزرار السابق/التالي)
- يمكن تحديثها مباشرة، مشاركة الرابط

السيناريو 3: تحديث صفحة النافذة المنبثقة
- النافذة المنبثقة في تدفق المحتوى → الضغط على F5 للتحديث
- قواعد الاعتراض لا تعمل(ليس من تنقل تدفق المحتوى)
- عرض الصفحة الكاملة /photos/1

(2) الاعتراض متعدد المستويات

▶ مثال: توجيه الاعتراض متعدد المستويات

المخرجات:

TEXT 📖 للعرض فقط
يتم تصيير الصفحة كما هو موصوف أعلاه، مع تحديث واجهة المستخدم بناءً على السلوك الموصوف.
TSX
// ============================================
// مثال على توجيه الاعتراض متعدد المستويات
// هيكل الدليل:
// app/
//   photos/
//     [id]/page.tsx                    → /photos/1(صفحة كاملة)
//   (feed)/
//     page.tsx                         → / (تدفق المحتوى)
//     categories/
//       [cat]/page.tsx                 → /categories/nature(صفحة الفئة)
//       (..)(..)photos/
//         [id]/page.tsx                → اعتراض /photos/1 → نافذة منبثقة على صفحة الفئات
// ============================================

// شرح قواعد المطابقة:
// مستويات (feed)/categories/[cat] = app/(feed)/categories/[cat]
// الهدف: app/photos/[id] المستويات = app/photos/[id]
// تحتاج إلى الصعود مستويين للمطابقة → (..)(..)

// app/(feed)/categories/(..)(..)photos/[id]/page.tsx
'use client';
import { useRouter } from "next/navigation";

export default function CategoryPhotoModal({ params }) {
  const router = useRouter();

  return (
    <div
      className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center"
      onClick={() => router.back()}
    >
      <div onClick={e => e.stopPropagation()} className="max-w-lg">
        <img
          src={`https://picsum.photos/seed/${params.id}/600/400`}
          alt=""
          className="rounded-lg"
        />
        <button
          onClick={() => router.back()}
          className="mt-2 px-4 py-2 bg-white rounded"
        >
          إغلاق
        </button>
      </div>
    </div>
  );
}

المخرجات:

TEXT 📖 للعرض فقط
يقوم CategoryPhotoModal بتصيير واجهة المستخدم الخاصة به.

المخرجات:

TEXT 📖 للعرض فقط
من /categories/nature، النقر على الصورة:
1. عرض صورة أكبر في نافذة منبثقة(شريط العنوان /photos/1)
2. النقر على إغلاق أو الخلفية → router.back() → العودة إلى /categories/nature
3. الوصول المباشر إلى /photos/1 → عرض الصفحة الكاملة(لا نوافذ منبثقة)
4. النافذة المنبثقة على صفحة الفئة تعمل بشكل صحيح


5. نموذج النافذة المنبثقة + المسار المتوازي المركب

(1) تصميم البنية

100%
graph TB
    subgraph "نافذة منبثقة مدفوعة بعنوان URL"
        A[إجراءات المستخدم] --> B{التنقل إلى /photos/1}
        B --> C[في تدفق المحتوى؟]
        C -->|نعم| D[فتحة @modal<br/>تطابق واعتراض المسارات]
        C -->|لا| E[وصول مباشر<br/>عرض الصفحة الكاملة]
        D --> F[عرض النافذة المنبثقة<br/>تحديث شريط العنوان]
        F --> G[خروج المستخدم]
        G --> H[router.back()]
        H --> I[العودة إلى تدفق المحتوى<br/>اختفاء النافذة المنبثقة]
    end

    style D fill:#cce5ff
    style E fill:#d4edda
    style G fill:#f8d7da
إجراء المستخدم تغيير URL حالة النافذة المنبثقة حالة الصفحة
النقر على صورة تدفق المحتوى //photos/1 عرض النافذة المنبثقة الحفاظ على تدفق المحتوى في الخلفية
إغلاق النافذة المنبثقة /photos/1/ إخفاء تدفق المحتوى دون تغيير
تحديث /photos/1 /photos/1 بدون تغيير لا نوافذ منبثقة عرض الصفحة الكاملة
مشاركة /photos/1 قابل للمشاركة لا نوافذ منبثقة يرى المستلمون الصفحة الكاملة

▶ مثال: النافذة المنبثقة الكاملة

المخرجات:

TEXT 📖 للعرض فقط
رسم توضيحي: إجراءات المستخدم؛ في تدفق المحتوى؟؛ فتحة @modal تطابق واعتراض المسارات؛ وصول مباشر عرض الصفحة الكاملة؛ عرض النافذة المنبثقة تحديث شريط العنوان؛ خروج المستخدم.
TSX
// ============================================
// نافذة منبثقة كاملة + Parallel Route + Intercepting Route
// تنفيذ نظام نوافذ منبثقة "قابل للتحديث، قابل للمشاركة، يمكن التراجع عنه"
// ============================================

// src/app/layout.tsx
export default function RootLayout({
  children,
  modal,
}: {
  children: React.ReactNode;
  modal: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        {children}
        {modal}
      </body>
    </html>
  );
}

// src/app/@modal/default.tsx
export default function Default() {
  return null;
}

// src/app/@modal/(.)photos/[id]/page.tsx — اعتراض النوافذ المنبثقة
'use client';
import { useRouter } from "next/navigation";

export default function PhotoModal({ params }) {
  const router = useRouter();

  return (
    <div
      className="fixed inset-0 bg-black/80 flex items-center justify-center z-50"
      onClick={() => router.back()}
    >
      <div
        className="bg-white rounded-xl max-w-3xl w-full mx-4 shadow-2xl"
        onClick={e => e.stopPropagation()}
      >
        <div className="flex justify-end p-2">
          <button
            onClick={() => router.back()}
            className="w-8 h-8 flex items-center justify-center hover:bg-gray-100 rounded-full"
          >
            ✕
          </button>
        </div>
        <img
          src={`https://picsum.photos/seed/${params.id}/800/600`}
          alt=""
          className="w-full"
        />
        <div className="p-6">
          <h2 className="text-2xl font-bold">صورة #{params.id}</h2>
          <div className="flex gap-4 mt-4">
            <a
              href={`/photos/${params.id}`}
              className="text-sm text-blue-600 hover:underline"
              onClick={() => router.push(`/photos/${params.id}`)}
            >
              فتح في الصفحة الكاملة →
            </a>
          </div>
        </div>
      </div>
    </div>
  );
}

// src/app/(feed)/page.tsx — تدفق المحتوى
import Link from "next/link";

export default function Feed() {
  return (
    <div className="max-w-6xl mx-auto p-8">
      <h1 className="text-3xl font-bold mb-6">معرض الصور</h1>
      <div className="grid grid-cols-4 gap-4">
        {[1, 2, 3, 4, 5, 6, 7, 8].map(id => (
          <Link
            key={id}
            href={`/photos/${id}`}
            className="block group"
          >
            <div className="aspect-square bg-gray-100 rounded-lg overflow-hidden">
              <img
                src={`https://picsum.photos/seed/${id}/400/400`}
                alt=""
                className="w-full h-full object-cover group-hover:scale-105 transition-transform"
              />
            </div>
          </Link>
        ))}
      </div>
    </div>
  );
}

المخرجات:

TEXT 📖 للعرض فقط
يُصيّر: مكوّن تنقل باستخدام useRouter للتوجيه البرمجي.

المخرجات:

TEXT 📖 للعرض فقط
تدفق المستخدم:

1. زيارة / → رؤية شبكة 8 صور
2. النقر على الصورة #3 → عرض صورة أكبر في نافذة منبثقة(URL: /photos/3)
3. النقر على ✕ في الزاوية العلوية → إغلاق النافذة المنبثقة(URL: /)
4. النقر على الصورة #5 مرة أخرى → نافذة منبثقة (URL: /photos/5)
5. النقر على زر الرجوع في المتصفح → إغلاق النافذة المنبثقة(URL: /)
6. الضغط على زر الرجوع مرة أخرى → الخروج من التطبيق(سلوك طبيعي)

سيناريو التحديث:
1. أثناء فتح النافذة المنبثقة، الضغط على F5 → تحديث الصفحة بالكامل
2. قواعد الاعتراض لا تعمل → عرض /photos/3 الصفحة الكاملة
3. الصفحة تعمل بشكل طبيعي، يوجد رابط تنقل للعودة

سيناريوهات المشاركة:
1. نسخ /photos/3 URL وإرساله إلى صديق
2. الصديق يفتح هذا → عرض الصفحة الكاملة(ليست نافذة منبثقة)
3. SEO للصفحة طبيعي، جميع المحتويات قابلة للفهرسة


6. التصيير الشرطي للوحة التحكم

(1) عرض عروض مختلفة بناءً على الدور

100%
graph TB
    A[تخطيط لوحة التحكم] --> B{أدوار المستخدم}
    B -->|admin| C[لوحة @admin]
    B -->|editor| D[لوحة @editor]
    B -->|viewer| E[لوحة @viewer]

    style A fill:#cce5ff
    style C fill:#d4edda
    style D fill:#d4edda
    style E fill:#d4edda

▶ مثال: لوحة تحكم مدفوعة بالدور

المخرجات:

TEXT 📖 للعرض فقط
رسم توضيحي للوصول القائم على الدور: admin (كامل)، editor (قراءة/كتابة)، viewer (قراءة فقط).
TSX
// ============================================
// التصيير الشرطي: عرض لوحة تحكم مختلفة حسب الدور
// ============================================

// src/app/(dashboard)/layout.tsx — تخطيط توجيه الأدوار
export default function DashboardLayout({
  children,
  admin,
  editor,
  viewer,
}: {
  children: React.ReactNode;
  admin: React.ReactNode;
  editor: React.ReactNode;
  viewer: React.ReactNode;
}) {
  // محاكاة الحصول على الدور من Cookie/Session
  const role = "admin";

  return (
    <div className="flex h-screen">
      {/* المحتوى الرئيسي */}
      <main className="flex-1 p-8">
        {children}
      </main>

      {/* عرض فتحات مختلفة بناءً على الدور */}
      <aside className="w-80 border-l p-4">
        {role === "admin" && admin}
        {role === "editor" && editor}
        {role === "viewer" && viewer}
      </aside>
    </div>
  );
}

المخرجات:

TEXT 📖 للعرض فقط
يُصيّر واجهة مستخدم مكوّن ▶ مثال: لوحة تحكم مدفوعة بالدور كما هو موصوف في القسم.
TSX
// src/app/(dashboard)/@admin/default.tsx — لوحة المسؤول
export default function AdminPanel() {
  return (
    <div className="space-y-4">
      <h3 className="font-bold text-lg">عناصر تحكم المسؤول</h3>
      <div className="bg-red-50 border border-red-200 rounded-lg p-4">
        <p className="font-medium text-red-700">صحة النظام</p>
        <div className="mt-2 space-y-2">
          <div className="flex justify-between text-sm">
            <span>استخدام CPU</span>
            <span className="text-green-600">45%</span>
          </div>
          <div className="flex justify-between text-sm">
            <span>الذاكرة</span>
            <span className="text-yellow-600">72%</span>
          </div>
          <div className="flex justify-between text-sm">
            <span>المستخدمون النشطون</span>
            <span className="text-blue-600">1,234</span>
          </div>
        </div>
      </div>
      <div className="bg-white rounded-lg border p-4">
        <p className="font-medium">الموافقات المعلقة</p>
        <p className="text-2xl font-bold text-orange-600 mt-2">12</p>
      </div>
      <button className="w-full p-2 bg-blue-600 text-white rounded-lg">
        عرض جميع الإعدادات
      </button>
    </div>
  );
}

// src/app/(dashboard)/@editor/default.tsx — لوحة المحرر
export default function EditorPanel() {
  return (
    <div className="space-y-4">
      <h3 className="font-bold text-lg">أدوات المحرر</h3>
      <div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
        <p className="font-medium text-blue-700">عدد المسودات</p>
        <p className="text-3xl font-bold mt-2">8</p>
      </div>
      <div className="bg-white rounded-lg border p-4">
        <p className="font-medium">التعديلات الأخيرة</p>
        <div className="mt-2 space-y-2 text-sm">
          <p>• تحديث صورة الصفحة الرئيسية</p>
          <p>• إصلاح خطأ إملائي في صفحة حول</p>
          <p>• إضافة مقالة مدونة جديدة</p>
        </div>
      </div>
      <button className="w-full p-2 bg-green-600 text-white rounded-lg">
        إنشاء مقال جديد
      </button>
    </div>
  );
}

// src/app/(dashboard)/@viewer/default.tsx — لوحة المشاهد
export default function ViewerPanel() {
  return (
    <div className="space-y-4">
      <h3 className="font-bold text-lg">نظرة عامة</h3>
      <div className="bg-gray-50 border rounded-lg p-4">
        <p className="font-medium">لوحة التحكم الخاصة بك</p>
        <p className="text-sm text-gray-500 mt-2">
          لديك وصول للقراءة فقط. اتصل بالمسؤول للحصول على أذونات التحرير.
        </p>
      </div>
      <div className="bg-white border rounded-lg p-4">
        <p className="font-medium">روابط سريعة</p>
        <div className="mt-2 space-y-2 text-sm">
          <a href="/docs" className="block text-blue-600">التوثيق</a>
          <a href="/reports" className="block text-blue-600">التقارير</a>
          <a href="/help" className="block text-blue-600">مركز المساعدة</a>
        </div>
      </div>
    </div>
  );
}

المخرجات:

TEXT 📖 للعرض فقط
المسؤول(role = "admin"):
┌────────────────────────────┬──────────────────────┐
│                            │  عناصر تحكم المسؤول    │
│    لوحة التحكم              │  ┌────────────────┐  │
│    مرحبًا بعودتك، أليس!     │  │ CPU: 45% ✅    │  │
│                            │  │ الذاكرة: 72% ⚠️ │  │
│    ملخص المشروع            │  │ المستخدمون: 1,234│  │
│    12 مشروعًا نشطًا         │  └────────────────┘  │
│    48 مهمة معلقة           │  الموافقات المعلقة     │
│    8 أعضاء فريق            │  12                   │
│                            │  [عرض جميع الإعدادات] │
└────────────────────────────┴──────────────────────┘

المحرر(role = "editor"):
┌────────────────────────────┬──────────────────────┐
│                            │  أدوات المحرر          │
│    لوحة التحكم              │  عدد المسودات: 8       │
│    مرحبًا بعودتك، بوب!      │  التعديلات الأخيرة:     │
│                            │  • تحديث الصفحة الرئيسية│
│    مشاريعي                 │  • إصلاح خطأ إملائي    │
│    5 مشاريع نشطة           │  • إضافة مقالة مدونة   │
│                            │  [إنشاء مقال جديد]    │
└────────────────────────────┴──────────────────────┘

المشاهد(role = "viewer"):
┌────────────────────────────┬──────────────────────┐
│                            │  نظرة عامة             │
│    لوحة التحكم              │  وصول للقراءة فقط      │
│    مرحبًا، تشارلي!          │  روابط سريعة:          │
│                            │  • التوثيق             │
│    نشاط الفريق             │  • التقارير            │
│    10 أعضاء فريق متصلون    │  • مركز المساعدة       │
│                            │                      │
└────────────────────────────┴──────────────────────┘


7. مثال كامل: تدفق المحتوى + النافذة المنبثقة + لوحة التحكم الشرطية

TSX
// ============================================
// مثال شامل: نظام توجيه متكامل لمنصات المحتوى
// يغطي Parallel Routes + Intercepting Routes + لوحة التحكم الشرطية
// ============================================

// src/app/layout.tsx — التخطيط الجذري (مع فتحة النافذة المنبثقة)
export default function RootLayout({
  children,
  modal,
}: {
  children: React.ReactNode;
  modal: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body className="bg-gray-50">
        <header className="bg-white shadow-sm sticky top-0 z-40">
          <div className="max-w-6xl mx-auto px-4 py-3 flex justify-between">
            <a href="/" className="text-xl font-bold text-blue-600">PhotoVault</a>
            <nav className="flex gap-4">
              <a href="/" className="hover:text-blue-600">تدفق المحتوى</a>
              <a href="/dashboard" className="hover:text-blue-600">لوحة التحكم</a>
            </nav>
          </div>
        </header>
        {children}
        {modal}
      </body>
    </html>
  );
}

// src/app/@modal/default.tsx
export default function Default() {
  return null;
}

// src/app/@modal/(.)photos/[id]/page.tsx — اعتراض النوافذ المنبثقة
'use client';
import { useRouter } from "next/navigation";

export default function PhotoModal({ params }) {
  const router = useRouter();

  return (
    <div
      className="fixed inset-0 bg-black/70 flex items-center justify-center z-50"
      onClick={() => router.back()}
    >
      <div
        className="bg-white rounded-xl max-w-2xl w-full mx-4 overflow-hidden shadow-2xl"
        onClick={e => e.stopPropagation()}
      >
        <img
          src={`https://picsum.photos/seed/${params.id}/800/600`}
          alt=""
          className="w-full"
        />
        <div className="p-4 flex justify-between items-center">
          <div>
            <h2 className="font-bold">صورة #{params.id}</h2>
            <p className="text-sm text-gray-500">انقر على الخلفية للإغلاق</p>
          </div>
          <button
            onClick={() => router.push(`/photos/${params.id}`)}
            className="text-sm text-blue-600 hover:underline"
          >
            فتح الصفحة الكاملة
          </button>
        </div>
      </div>
    </div>
  );
}

// src/app/(feed)/page.tsx — تدفق المحتوى
import Link from "next/link";

export default function FeedPage() {
  const photos = Array.from({ length: 12 }, (_, i) => ({
    id: i + 1,
    url: `https://picsum.photos/seed/${i + 1}/400/400`,
    title: `صورة ${i + 1}`,
  }));

  return (
    <div className="max-w-6xl mx-auto p-8">
      <h1 className="text-3xl font-bold mb-6">تدفق الصور</h1>
      <div className="grid grid-cols-4 gap-4">
        {photos.map(photo => (
          <Link
            key={photo.id}
            href={`/photos/${photo.id}`}
            className="block aspect-square rounded-lg overflow-hidden bg-gray-100"
          >
            <img
              src={photo.url}
              alt={photo.title}
              className="w-full h-full object-cover hover:scale-105 transition-transform"
            />
          </Link>
        ))}
      </div>
    </div>
  );
}

// src/app/photos/[id]/page.tsx — صفحة الصورة الكاملة
export default async function PhotoPage({ params }) {
  const { id } = params;

  return (
    <div className="max-w-4xl mx-auto p-8">
      <a href="/" className="text-blue-600 hover:underline mb-4 inline-block">
        &larr; العودة إلى تدفق المحتوى
      </a>
      <img
        src={`https://picsum.photos/seed/${id}/1200/800`}
        alt=""
        className="w-full rounded-lg shadow-lg"
      />
      <div className="mt-6">
        <h1 className="text-3xl font-bold">صورة #{id}</h1>
        <p className="text-gray-500 mt-2">
          هذه هي عرض الصفحة الكاملة. شارك هذا الرابط مباشرة مع الآخرين.
        </p>
      </div>
    </div>
  );
}

// src/app/(dashboard)/layout.tsx — تخطيط التوجيه المتوازي للوحة التحكم
export default function DashboardLayout({
  children,
  admin,
}: {
  children: React.ReactNode;
  admin: React.ReactNode;
}) {
  const role = "admin";

  return (
    <div className="max-w-6xl mx-auto p-8 flex gap-8">
      <div className="flex-1">{children}</div>
      <aside className="w-80">
        {role === "admin" && admin}
      </aside>
    </div>
  );
}

// src/app/(dashboard)/@admin/default.tsx
export default function AdminPanel() {
  return (
    <div className="bg-white rounded-xl shadow-sm border p-6 space-y-4">
      <h3 className="font-bold text-lg">لوحة المسؤول</h3>
      <div className="space-y-2">
        <div className="flex justify-between">
          <span>إجمالي الصور</span>
          <span className="font-bold">1,234</span>
        </div>
        <div className="flex justify-between">
          <span>الرفع اليومي</span>
          <span className="font-bold text-green-600">+48</span>
        </div>
        <div className="flex justify-between">
          <span>المساحة المستخدمة</span>
          <span className="font-bold">237 GB</span>
        </div>
      </div>
      <button className="w-full p-2 bg-blue-600 text-white rounded-lg">
        إدارة المعرض
      </button>
    </div>
  );
}

// src/app/(dashboard)/page.tsx — الصفحة الرئيسية للوحة التحكم
export default function DashboardPage() {
  return (
    <div>
      <h1 className="text-2xl font-bold">لوحة التحكم</h1>
      <p className="text-gray-500 mt-2">نظرة عامة على معرض الصور الخاص بك</p>
      <div className="grid grid-cols-2 gap-4 mt-6">
        <div className="bg-white p-6 rounded-xl shadow-sm border">
          <p className="text-sm text-gray-500">هذا الأسبوع</p>
          <p className="text-3xl font-bold mt-1">342</p>
          <p className="text-sm text-green-600 mt-1">↑ 12% عن الأسبوع الماضي</p>
        </div>
        <div className="bg-white p-6 rounded-xl shadow-sm border">
          <p className="text-sm text-gray-500">إجمالي المشاهدات</p>
          <p className="text-3xl font-bold mt-1">89.4K</p>
          <p className="text-sm text-green-600 mt-1">↑ 8% عن الشهر الماضي</p>
        </div>
      </div>
    </div>
  );
}

المخرجات المتوقعة:

TEXT 📖 للعرض فقط
1. زيارة / → شبكة 12 صورة
2. النقر على الصورة → عرض صورة أكبر في نافذة منبثقة(URL: /photos/5)
3. النقر على الخلفية → إغلاق النافذة المنبثقة، العودة إلى تدفق المحتوى
4. تحديث → عرض الصفحة الكاملة /photos/5
5. مشاركة /photos/5 → يرى الأصدقاء الصفحة الكاملة
6. زيارة /dashboard → عرض لوحة التحكم + لوحة المسؤول
7. من لوحة التحكم، النقر على الصورة → نفس تجربة النافذة المنبثقة (مسارات الاعتراض)

❓ أسئلة شائعة

س هل كل @slot يحتاج إلى ملف default.tsx؟
ج نعم. إذا لم يكن هناك ملف default.tsx، سيعيد Next.js خطأ 404 عندما لا يطابق عنوان URL الحالي مسارًا لتلك الفتحة. حتى إذا كانت فتحة النافذة المنبثقة لا تُعرض معظم الوقت، فلا تزال بحاجة إلى export default function Default() { return null; }.
س هل يمكن للفتحات في Parallel Routes التواصل مع بعضها البعض؟
ج لا يمكنها التواصل مباشرة. كل فتحة هي Server Component مستقل ولا يمكنها مشاركة الحالة. إذا كانت هناك حاجة للتواصل، يمكنك تمرير القيم عبر معاملات URL (searchParams) أو استخدام Context Provider في التخطيط الجذري.
س كيف يتم حساب مستوى (..) لمسارات الاعتراض؟
ج يتم حساب (..) بناءً على هيكل نظام الملفات الفعلي، لكن مجموعات المسارات (group) لا تستهلك مستويات. على سبيل المثال، app/(feed)/photos/(.)[id]/page.tsx يعترض app/(feed)/photos/[id]/page.tsx لأن (feed) لا يتم احتسابه.
س ماذا يحدث عند الوصول المباشر إلى عنوان URL في وضع النافذة المنبثقة + مسار الاعتراض؟
ج عند الوصول المباشر (أو التحديث) للصفحة، لا يعمل اعتراض المسار، ويرى المستخدم الصفحة الكاملة المستقلة. هذا هو جمال هذا النهج - يحقق "تدهورًا رشيقًا" من خلال اعتراض المسار، مع السماح بالوصول إلى عنوان URL ومشاركته مباشرة.
س هل تؤثر Parallel Routes على أداء الصفحة؟
ج هناك تأثير طفيف لأن كل فتحة هي Server Component مستقل يحتاج إلى التصيير بشكل منفصل. ومع ذلك، يقوم Next.js تلقائيًا بتوازي هذه الطلبات، مما يجعلها أسرع من التصيير التسلسلي. توصية: لا تستخدم أكثر من 3-4 فتحات، وتجنب الفتحات غير الضرورية.
س هل يجب استخدام Parallel Routes في التصيير الشرطي أم معالجته من جانب العميل؟
ج نوصي بـ Parallel Routes. على الرغم من أن التصيير الشرطي من جانب العميل (if (role === 'admin')) ممكن أيضًا، إلا أن Parallel Routes تحافظ على استقلالية ملف عرض كل دور، مع سلامة الأنواع، وسهولة أكبر في الاختبار والفصل.

📖 ملخص


📝 تمارين

  1. تمرين أساسي (⭐): أنشئ فتحة @modal في مشروعك ونفّذ الوظيفة الأساسية "النقر على الزر → عرض مقتطف نصي في نافذة منبثقة" (استخدم default.tsx لإرجاع null).

  2. تمرين متقدم (⭐⭐): أنشئ نظام تصفح صور: صفحة تدفق المحتوى (/) تعرض شبكة من الصور؛ النقر على صورة يفتح نافذة منبثقة ((.)photos/[id])؛ والوصول المباشر إلى /photos/1 يعرض الصفحة الكاملة. تحقق من سلوك زر الرجوع ووظيفة التحديث.

  3. تحدٍّ (⭐⭐⭐): نفّذ نظام لوحة تحكم مدفوع بالدور: أنشئ ثلاث فتحات - @admin و@editor و@viewer - بحيث يعرض كل دور محتوى لوحة تحكم مختلفًا (يرى المسؤولون مراقبة النظام، ويرى المحررون إحصائيات المسودات، ويرى المشاهدون تنبيهات القراءة فقط). تحقق من التصيير الشرطي عن طريق تبديل متغير role.

Web-Tutorial.com

فريق Web-Tutorial التقني

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

100%