تخطّى إلى المحتوى الرئيسي
المسارات المتوازية والمسارات الاعتراضية في Next.js App Router: ملاحظات ميدانية حول نمط النافذة المنبثقة للصور

المسارات المتوازية والمسارات الاعتراضية في Next.js App Router: ملاحظات ميدانية حول نمط النافذة المنبثقة للصور

August 21, 2026
هندسة الواجهات
5 دقيقة قراءة

الخلاصة السريعة

في Devya، نستخدم المسارات المتوازية (Parallel Routes) — وهي مجلدات في Next.js App Router تبدأ بالرمز @، مثل @modal — لعرض أكثر من صفحة واحدة داخل نفس التخطيط (layout) في آن واحد. يمرّر Next.js محتوى كل فتحة (slot) إلى أقرب layout.tsx كخاصية (prop) مسمّاة، إلى جانب فتحة children الضمنية، ولا تضيف مجلدات الفتحات أي جزء إلى عنوان URL.

نجمع بين المسارات المتوازية والمسارات الاعتراضية (Intercepting Routes) — قواعد التسمية (.) و(..) و(..)(..) و(...) — لاستبدال مكوّن مختلف داخل فتحة معينة عند التنقل من جانب العميل (client-side)، دون تغيير عنوان URL إطلاقًا.

ملف default.tsx هو ما يعرضه Next.js لفتحة معينة عندما لا يطابق عنوان URL الحالي أي شيء بداخلها. بدونه، يؤدي التنقل الكامل (hard navigation) إلى مسار لا يملأ كل الفتحات إلى خطأ 404 — وهو خطأ شحناه مرة ولم نكتشفه إلا عند التحديث (refresh).

تحديث الصفحة يدويًا أو مشاركة رابط مثل /photo/123 لا يُفترض أن يُظهر النافذة المنبثقة (modal). المفترض أن يعرض page.tsx العادي لهذا المسار كصفحة كاملة ومستقلة. هذه هي كامل فكرة النمط بالنسبة لنا، وليست شيئًا نحاول الالتفاف عليه.

كل فتحة هي شجرة فرعية مستقلة بملفات loading.tsx وerror.tsx خاصة بها، ما يتيح لنا جعل النافذة المنبثقة تُعلّق (suspend) وتُبث (stream) بشكل مستقل عن الصفحة خلفها.

ما المشكلة التي تحلها المسارات المتوازية لفريقنا؟

يتيح المسار المتوازي لتخطيطنا (layout) عرض أكثر من صفحة في آن واحد، كل واحدة معنونة بفتحة مسمّاة بدلًا من جزء في عنوان URL. لجأنا إلى هذا النمط في مشروع لأحد العملاء يحتوي على شبكة صور: كان النقر على مصغّرة يجب أن يفتح نافذة معاينة دون مغادرة الشبكة، وكانت هذه النافذة تحتاج أيضًا إلى عنوان URL قابل للمشاركة. النافذة المعتمدة على متغيّر منطقي (boolean) تمنحك النصف الأول مجانًا، ولا يمكنها منحك النصف الثاني إطلاقًا، لأنه لا يوجد عنوان URL لحالة تعيش داخل useState.

قاعدة تسمية المجلد هي اسم يبدأ بالرمز @. يمرّر Next.js محتوى كل فتحة مطابقة إلى أقرب layout.tsx كخاصية باسم المجلد. أعدنا استخدام الآلية نفسها خارج سياق النوافذ المنبثقة أيضًا — لوحة تحكم بفتحتي @team و@analytics تتنقلان بشكل مستقل، بحيث لا يؤثر تبديل التبويب في إحداهما على الأخرى.

كيف تحدد قاعدة (.) ما الذي تعترضه روابط فريقنا؟

تطابق قواعد النقاط مسارًا مستهدفًا بالنسبة لموضع الملف المعترِض في نظام الملفات، وليس بالنسبة لعنوان URL الحالي. (.) تطابق مسارًا في نفس مستوى المجلد، و(..) تطابق مستوى واحدًا أعلى، و(..)(..) تطابق مستويين أعلى، و(...) تطابق من جذر مجلد app.

عند وجود مسار مثل app/@modal/(.)photo/[id]/page.tsx، فإن التنقل من جانب العميل إلى /photo/[id] المُطلق من مسار في نفس مستوى @modal يُعرض داخل فتحة النافذة المنبثقة بدلًا من استبدال الشجرة بأكملها. أما التحميل الكامل لنفس العنوان — تحديث، أو لصق رابط، أو إحالة خارجية — فيحلّ app/photo/[id]/page.tsx العادي بدلًا من ذلك. الاعتراض لا يحدث إلا في انتقال من جانب العميل.

لماذا يهم ملف default.tsx، وماذا يتعطل بدونه؟

تجاهلناه مرة واحدة، وبدا كل شيء سليمًا في بيئة التطوير حتى قام أحد أعضاء الفريق بتحديث مسار والنافذة المنبثقة مفتوحة، فواجه خطأ 404. الفتحة التي لا تطابق فيها جزء من عنوان URL الحالي تحتاج إلى شيء تعرضه عند التحميل الكامل للصفحة، إذ لا توجد حالة عميل سابقة يمكن الرجوع إليها. ملف default.tsx الذي يُعيد null هو ذلك الحل الاحتياطي.

التنقل من جانب العميل أكثر تسامحًا: بدون default.tsx، يستمر Next.js في عرض آخر ما أظهرته الفتحة بدلًا من إلغاء تركيبها. هذا مفيد في حالة تبويبات لوحة التحكم، لكنه يعني أيضًا أن غياب default.tsx يجتاز كل اختبار نقر يدوي ولا يفشل إلا عند إعادة التحميل الكامل — وهو بالضبط المسار الذي يسلكه رابط مُشارَك.

لماذا يعرض التحديث الصفحة الكاملة بدلًا من النافذة المنبثقة؟

لأن هذا هو التصميم المقصود، وليس عيبًا. app/photo/[id]/page.tsx صفحة كاملة ومستقلة — بنفس المحتوى، دون أي إطار للنافذة المنبثقة، وتُعرض دون تشغيل أي سطر من جافاسكربت من جانب العميل أولًا. هذا ما يجعل الرابط قابلًا للمشاركة والفهرسة فعليًا: أي زاحف بحث أو بوت معاينة روابط لا يُشغّل الموجّه من جانب العميل يحصل على الصفحة الحقيقية، لا على غلاف فارغ ينتظر جافاسكربت ليفتح نافذة.

كيف نغلق النافذة المنبثقة دون أن نفقد قابلية مشاركة رابطها؟

نافذتنا المنبثقة هي Client Component تستدعي router.back() من next/navigation، وهو ما يعكس التنقل الناعم الذي فتحها ويترك الفتحة تعود إلى default.tsx. الفجوة التي وجدناها في الإنتاج: زائر يفتح /photo/123 مباشرة في تبويب جديد لا يملك سجل تصفح (history) للرجوع إليه، فإما لا يفعل router.back() شيئًا أو يخرج من التطبيق. أصبحنا نعرض دائمًا رابط إغلاق صريح بعنوان href حقيقي يعود إلى المعرض، حتى لا يعتمد الإغلاق على وجود سجل تصفح.

نافذة بحالة عميل أم نافذة بمسار اعتراضي — كيف نقرر؟

النافذة المعتمدة على متغيّر منطقي تكلّف استدعاء useState واحدًا، ولا تملك عنوان URL، ولا تنجو من التحديث، وتحتاج جافاسكربت من جانب العميل لعرض أي شيء. النافذة ذات المسار الاعتراضي تكلّف فتحة، ومجلدًا اعتراضيًا، ومسارًا احتياطيًا، وملف default.tsx — وفي المقابل تحصل على عنوان URL حقيقي، وتحديث يعمل، ومحتوى يُعرض دون جافاسكربت عند التنقل المباشر. ما زلنا نستخدم المتغيّر المنطقي لواجهات مؤقتة فعليًا مثل رسالة تأكيد. ونلجأ إلى النسخة الموجّهة عندما يستحق المحتوى خلف النافذة عنوانه الخاص.

الأسئلة الشائعة

هل تظهر مجلدات فتحات المسارات المتوازية مثل @modal في عنوان URL؟ لا. الرمز @ يُميّز المجلد كفتحة وليس كجزء من المسار، لذا لا يظهر أبدًا في عنوان URL، ويحدد فقط أي خاصية في layout تستقبل محتواه.

ماذا يحدث إذا انتقل شخص مباشرة إلى /photo/123 بدلًا من النقر عبر قائمة الصور؟ يعرض Next.js app/photo/[id]/page.tsx كصفحة كاملة عادية. الاعتراض لا يحدث إلا في انتقال من جانب العميل من مسار مطابق.

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

لماذا لا يُغلق router.back() النافذة المنبثقة لزائر فتح الرابط مباشرة؟ لا يوجد سجل تصفح للرجوع إليه عندما يكون المسار أول شيء يُحمَّل في تبويب. نقرن معالج الإغلاق برابط احتياطي صريح.

هل يمكن دمج (..)(..) مع فتحة معرّفة على عدة مستويات أعلى في الشجرة؟ نعم — عدد النقاط يصف فقط عدد مستويات المجلدات أعلى موضع الملف المعترِض نفسه، بمعزل عن مكان وجود مجلد الفتحة @slot ذاته.