Next.js: المسارات المتوازية ومسارات الاعتراض
آخر تحديث: 2026-08-26
التوجيه المتوازي يشبه الشاشات المتعددة - كل شاشة تعمل بشكل مستقل ولكنها مترابطة؛ توجيه الاعتراض يشبه نقطة استلام الطرود - يعترض الطرود على طول الطريق، ويعالجها، ثم يواصل التسليم.
1. ما ستتعلمه
- Parallel Routes: تصيير عدة عروض مستقلة من عنوان URL واحد
- كيفية استخدام فتحات التسمية
@modalو@sidebarو@team - Intercepting Routes: قواعد المطابقة لـ
(.)و(..)و(...) - النافذة المنبثقة + التوجيه المتوازي المركب: نوافذ منبثقة مدفوعة بعنوان URL
- التصيير الشرطي: عرض عروض لوحة تحكم مختلفة بناءً على دور المستخدم
2. قصة حقيقية لمدير منتج
(1) نقطة الألم: تجربة سيئة للنوافذ المنبثقة في تدفق المحتوى
أليس تعمل على تطوير ميزة تصفح الصور لمنصة محتوى وواجهت بعض مشكلات تجربة المستخدم:
"عندما نقر المستخدم على صورة في تدفق المحتوى، عرضنا نافذة منبثقة بملء الشاشة، وتغير عنوان URL إلى
/photos/123. ونتيجة لذلك، عندما حاول المستخدم العودة إلى تدفق المحتوى بالضغط على زر الرجوع في المتصفح، تم تسجيل خروجه من التطبيق بالكامل. اشتكى المستخدم: 'أردت فقط إلقاء نظرة على صورة - لماذا لا يمكنني العودة إلى الصفحة الرئيسية؟'"
المشكلات التي حللتها أليس:
| المشكلة | التأثير | تقييم المستخدم |
|---|---|---|
| لا يمكن تحديث النافذة المنبثقة بشكل مستقل | تحديث النافذة المنبثقة يؤدي إلى تحديث الصفحة بالكامل | 2/5 |
| زر الرجوع يخرج من التطبيق | تعطيل تدفق المستخدم | 1.5/5 |
| الشريط الجانبي والمحتوى الرئيسي غير متزامنين | التنقل مربك | 2.5/5 |
| عرض الفريق ولوحة التحكم منفصلان | كفاءة إدارة منخفضة | 3/5 |
(2) حل Parallel + Intercepting Routes
استخدم
@modalلعرض نافذة منبثقة للتوجيه المتوازي +(.)photoلاعتراض التنقل إلى الصور في تدفق المحتوى.
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) المفاهيم والاستخدام
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 الأساسية
المخرجات:
رسم توضيحي لهيكل المسارات: مسارات نظام الملفات تقابل مسارات URL.
// ============================================
// أساسيات 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>
);
}
المخرجات:
يُصيّر: تخطيط لوحة التحكم مع شريط جانبي داكن (روابط لوحة التحكم/المشاريع/الإعدادات) + منطقة المحتوى الرئيسي.
// 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>
);
}
// 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>
);
}
المخرجات:
زيارة /dashboard:
┌──────────────────────┬──────────┬──────────┐
│ │ │ │
│ منطقة المحتوى الرئيسي │ الشريط الجانبي │ الفريق │
│ لوحة التحكم │ النشاط │ أليس │
│ مرحبًا بعودتك! │ الأخير │ بوب │
│ │ إشعارات(3)│ تشارلي │
│ │ │ ديانا │
└──────────────────────┴──────────┴──────────┘
ثلاث مناطق تُصيّر بشكل مستقل، لا تؤثر على بعضها البعض
(2) default.tsx — الحالة الافتراضية المطلوبة
يجب أن يحتوي كل دليل @slot على default.tsx، والذي يُعرض عندما لا يتم العثور على مسار مطابق.
graph TB
A[تنقل المستخدم] --> B{عنوان URL الحالي<br/>يطابق مسار الفتحة؟}
B -->|يطابق| C[عرض page.tsx الخاص بالفتحة]
B -->|لا يطابق| D[عرض default.tsx الخاص بالفتحة]
style C fill:#d4edda
style D fill:#f8d7da
▶ مثال: أهمية default.tsx
المخرجات:
رسم توضيحي: تنقل المستخدم؛ عرض page.tsx الخاص بالفتحة؛ عرض default.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>
);
}
المخرجات:
يتضمن شريطًا جانبيًا.
المخرجات:
زيارة /dashboard:
- @modal لم يتم العثور على مسار مطابق → عرض ModalDefault (= null، لا يُصيّر)
- @sidebar لم يتم العثور على مسار مطابق → عرض SidebarDefault(روابط سريعة للتنقل)
- @team لم يتم العثور على مسار مطابق → عرض TeamDefault(قائمة أعضاء الفريق)
زيارة /dashboard/photos/1(بافتراض أن @modal وجد تطابقًا):
- @modal يطابق → عرض محتوى النافذة المنبثقة
- الفتحات الأخرى تعرض default.tsx الخاص بكل منها
4. Intercepting Routes
(1) قواعد مطابقة الاعتراض
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] |
▶ مثال: اعتراض تصفح الصور
المخرجات:
رسم توضيحي: حاليًا في تدفق المحتوى؛ اعتراض (.)photo؛ عرض نافذة منبثقة في تدفق المحتوى؛ يقوم المستخدم بتحديث الصفحة؛ تجاوز الاعتراض عرض الصفحة بالكامل.
// ============================================
// 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>
);
}
المخرجات:
يُصيّر قائمة ديناميكية من العناصر باستخدام map().
المحتوى المرئي: تدفق الصور
// ============================================
// مسارات الاعتراض: فتح نافذة منبثقة في تدفق المحتوى
// الملف: 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>
);
}
// ============================================
// الصفحة الكاملة: عرض الصفحة الكاملة عند الوصول المستقل
// الملف: 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"
>
← العودة إلى تدفق المحتوى
</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>
);
}
المخرجات:
السيناريو 1: النقر على صورة من تدفق المحتوى
- حاليًا في / (شبكة تدفق الصور)
- النقر على الصورة 1
- عرض صورة أكبر في نافذة منبثقة(يتغير شريط العنوان إلى /photos/1)
- النقر على قناع الخلفية → router.back() → العودة إلى تدفق المحتوى
- النقر على زر الرجوع في المتصفح → إغلاق النافذة المنبثقة → تدفق المحتوى يبقى دون تغيير
السيناريو 2: الوصول المباشر إلى /photos/1
- تخطي مسارات الاعتراض
- عرض الصفحة الكاملة (صورة كبيرة بعرض كامل + أزرار السابق/التالي)
- يمكن تحديثها مباشرة، مشاركة الرابط
السيناريو 3: تحديث صفحة النافذة المنبثقة
- النافذة المنبثقة في تدفق المحتوى → الضغط على F5 للتحديث
- قواعد الاعتراض لا تعمل(ليس من تنقل تدفق المحتوى)
- عرض الصفحة الكاملة /photos/1
(2) الاعتراض متعدد المستويات
▶ مثال: توجيه الاعتراض متعدد المستويات
المخرجات:
يتم تصيير الصفحة كما هو موصوف أعلاه، مع تحديث واجهة المستخدم بناءً على السلوك الموصوف.
// ============================================
// مثال على توجيه الاعتراض متعدد المستويات
// هيكل الدليل:
// 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>
);
}
المخرجات:
يقوم CategoryPhotoModal بتصيير واجهة المستخدم الخاصة به.
المخرجات:
من /categories/nature، النقر على الصورة:
1. عرض صورة أكبر في نافذة منبثقة(شريط العنوان /photos/1)
2. النقر على إغلاق أو الخلفية → router.back() → العودة إلى /categories/nature
3. الوصول المباشر إلى /photos/1 → عرض الصفحة الكاملة(لا نوافذ منبثقة)
4. النافذة المنبثقة على صفحة الفئة تعمل بشكل صحيح
5. نموذج النافذة المنبثقة + المسار المتوازي المركب
(1) تصميم البنية
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 |
قابل للمشاركة | لا نوافذ منبثقة | يرى المستلمون الصفحة الكاملة |
▶ مثال: النافذة المنبثقة الكاملة
المخرجات:
رسم توضيحي: إجراءات المستخدم؛ في تدفق المحتوى؟؛ فتحة @modal تطابق واعتراض المسارات؛ وصول مباشر عرض الصفحة الكاملة؛ عرض النافذة المنبثقة تحديث شريط العنوان؛ خروج المستخدم.
// ============================================
// نافذة منبثقة كاملة + 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>
);
}
المخرجات:
يُصيّر: مكوّن تنقل باستخدام useRouter للتوجيه البرمجي.
المخرجات:
تدفق المستخدم:
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) عرض عروض مختلفة بناءً على الدور
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
▶ مثال: لوحة تحكم مدفوعة بالدور
المخرجات:
رسم توضيحي للوصول القائم على الدور: admin (كامل)، editor (قراءة/كتابة)، viewer (قراءة فقط).
// ============================================
// التصيير الشرطي: عرض لوحة تحكم مختلفة حسب الدور
// ============================================
// 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>
);
}
المخرجات:
يُصيّر واجهة مستخدم مكوّن ▶ مثال: لوحة تحكم مدفوعة بالدور كما هو موصوف في القسم.
// 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>
);
}
المخرجات:
المسؤول(role = "admin"):
┌────────────────────────────┬──────────────────────┐
│ │ عناصر تحكم المسؤول │
│ لوحة التحكم │ ┌────────────────┐ │
│ مرحبًا بعودتك، أليس! │ │ CPU: 45% ✅ │ │
│ │ │ الذاكرة: 72% ⚠️ │ │
│ ملخص المشروع │ │ المستخدمون: 1,234│ │
│ 12 مشروعًا نشطًا │ └────────────────┘ │
│ 48 مهمة معلقة │ الموافقات المعلقة │
│ 8 أعضاء فريق │ 12 │
│ │ [عرض جميع الإعدادات] │
└────────────────────────────┴──────────────────────┘
المحرر(role = "editor"):
┌────────────────────────────┬──────────────────────┐
│ │ أدوات المحرر │
│ لوحة التحكم │ عدد المسودات: 8 │
│ مرحبًا بعودتك، بوب! │ التعديلات الأخيرة: │
│ │ • تحديث الصفحة الرئيسية│
│ مشاريعي │ • إصلاح خطأ إملائي │
│ 5 مشاريع نشطة │ • إضافة مقالة مدونة │
│ │ [إنشاء مقال جديد] │
└────────────────────────────┴──────────────────────┘
المشاهد(role = "viewer"):
┌────────────────────────────┬──────────────────────┐
│ │ نظرة عامة │
│ لوحة التحكم │ وصول للقراءة فقط │
│ مرحبًا، تشارلي! │ روابط سريعة: │
│ │ • التوثيق │
│ نشاط الفريق │ • التقارير │
│ 10 أعضاء فريق متصلون │ • مركز المساعدة │
│ │ │
└────────────────────────────┴──────────────────────┘
7. مثال كامل: تدفق المحتوى + النافذة المنبثقة + لوحة التحكم الشرطية
// ============================================
// مثال شامل: نظام توجيه متكامل لمنصات المحتوى
// يغطي 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">
← العودة إلى تدفق المحتوى
</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>
);
}
المخرجات المتوقعة:
1. زيارة / → شبكة 12 صورة
2. النقر على الصورة → عرض صورة أكبر في نافذة منبثقة(URL: /photos/5)
3. النقر على الخلفية → إغلاق النافذة المنبثقة، العودة إلى تدفق المحتوى
4. تحديث → عرض الصفحة الكاملة /photos/5
5. مشاركة /photos/5 → يرى الأصدقاء الصفحة الكاملة
6. زيارة /dashboard → عرض لوحة التحكم + لوحة المسؤول
7. من لوحة التحكم، النقر على الصورة → نفس تجربة النافذة المنبثقة (مسارات الاعتراض)
❓ أسئلة شائعة
export default function Default() { return null; }.(..) لمسارات الاعتراض؟(..) بناءً على هيكل نظام الملفات الفعلي، لكن مجموعات المسارات (group) لا تستهلك مستويات. على سبيل المثال، app/(feed)/photos/(.)[id]/page.tsx يعترض app/(feed)/photos/[id]/page.tsx لأن (feed) لا يتم احتسابه.if (role === 'admin')) ممكن أيضًا، إلا أن Parallel Routes تحافظ على استقلالية ملف عرض كل دور، مع سلامة الأنواع، وسهولة أكبر في الاختبار والفصل.📖 ملخص
- Parallel Routes: تنفيذ مناطق تصيير مستقلة متعددة لعنوان URL واحد باستخدام اصطلاح تسمية
@slot - يجب أن يحتوي كل
@slotعلىdefault.tsx؛ يُعرض عند عدم العثور على مسار مطابق - Intercepting Routes: استخدام
(.)و(..)و(..)(..)و(...)لمطابقة المسارات على مستويات مختلفة - تركيبة "النافذة المنبثقة + مسار الاعتراض" تنفذ "نوافذ منبثقة مدفوعة بعنوان URL"، مع موازنة تجربة المستخدم وSEO
- اعتراض المسار يعمل فقط عند تنقل العميل؛ الوصول المباشر أو تحديث الصفحة يعرض الصفحة الكاملة
(...)يطابق مسارات مستوى الجذر،(.)يطابق مسارات نفس المستوى،(..)يطابق مسارات المستوى الأصل- يتم تنفيذ التصيير الشرطي باستخدام فتحات Parallel Routes وفحوصات الدور، مع عرض كل دور في ملف منفصل
- مجموعات المسارات
(group)لا تستهلك مستويات حساب الاعتراض
📝 تمارين
-
تمرين أساسي (⭐): أنشئ فتحة
@modalفي مشروعك ونفّذ الوظيفة الأساسية "النقر على الزر → عرض مقتطف نصي في نافذة منبثقة" (استخدمdefault.tsxلإرجاعnull). -
تمرين متقدم (⭐⭐): أنشئ نظام تصفح صور: صفحة تدفق المحتوى (
/) تعرض شبكة من الصور؛ النقر على صورة يفتح نافذة منبثقة ((.)photos/[id])؛ والوصول المباشر إلى/photos/1يعرض الصفحة الكاملة. تحقق من سلوك زر الرجوع ووظيفة التحديث. -
تحدٍّ (⭐⭐⭐): نفّذ نظام لوحة تحكم مدفوع بالدور: أنشئ ثلاث فتحات -
@adminو@editorو@viewer- بحيث يعرض كل دور محتوى لوحة تحكم مختلفًا (يرى المسؤولون مراقبة النظام، ويرى المحررون إحصائيات المسودات، ويرى المشاهدون تنبيهات القراءة فقط). تحقق من التصيير الشرطي عن طريق تبديل متغيرrole.