المسارات المتوازية (Parallel Routes)

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

على سبيل المثال، بالنظر إلى لوحة تحكم، يمكنك استخدام المسارات المتوازية لعرض صفحات الفريق والتحليلات في نفس الوقت:

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

الاصطلاح

الفتحات (Slots)

يتم إنشاء المسارات المتوازية باستخدام فتحات مسماة. يتم تعريف الفتحات باستخدام اصطلاح @folder. على سبيل المثال، هيكل الملفات التالي يعرّف فتحتين: @analytics و@team:

هيكل نظام الملفات للمسارات المتوازية

يتم تمرير الفتحات كخصائص (props) إلى تخطيط الأب المشترك. بالنسبة للمثال أعلاه، المكون في app/layout.js يقبل الآن خصائص فتحات @analytics و@team، ويمكنه عرضها بالتوازي مع خاصية children:

export default function Layout({
  children,
  team,
  analytics,
}: {
  children: React.ReactNode
  analytics: React.ReactNode
  team: React.ReactNode
}) {
  return (
    <>
      {children}
      {team}
      {analytics}
    </>
  )
}
export default function Layout({ children, team, analytics }) {
  return (
    <>
      {children}
      {team}
      {analytics}
    </>
  )
}

ومع ذلك، الفتحات ليست أجزاء من المسار ولا تؤثر على بنية URL. على سبيل المثال، لـ /@analytics/views، سيكون URL هو /views لأن @analytics هي فتحة. يتم دمج الفتحات مع مكون الصفحة العادي لتشكيل الصفحة النهائية المرتبطة بجزء المسار. بسبب هذا، لا يمكنك أن يكون لديك فتحات ثابتة وديناميكية منفصلة في نفس مستوى جزء المسار. إذا كانت إحدى الفتحات ديناميكية، يجب أن تكون جميع الفتحات في ذلك المستوى ديناميكية.

معلومة مفيدة:

  • خاصية children هي فتحة ضمنية لا تحتاج إلى تعيين لمجلد. هذا يعني أن app/page.js تعادل app/@children/page.js.

default.js

يمكنك تعريف ملف default.js ليتم عرضه كحل بديل للفتحات غير المتطابقة أثناء التحميل الأولي أو إعادة تحميل الصفحة بالكامل.

خذ بعين الاعتبار هيكل المجلدات التالي. فتحة @team تحتوي على صفحة /settings، لكن @analytics لا تحتوي عليها.

مسارات متوازية مع مسارات غير متطابقة

عند التنقل إلى /settings، ستقوم فتحة @team بعرض صفحة /settings مع الحفاظ على الصفحة النشطة حاليًا لفتحة @analytics.

عند التحديث، سيقوم Next.js بعرض default.js لـ @analytics. إذا لم يكن default.js موجودًا، سيتم عرض 404 بدلاً من ذلك.

بالإضافة إلى ذلك، بما أن children هي فتحة ضمنية، تحتاج أيضًا إلى إنشاء ملف default.js لعرض حل بديل لـ children عندما لا يتمكن Next.js من استعادة الحالة النشطة للصفحة الأم.

السلوك

بشكل افتراضي، يحتفظ Next.js بتتبع الحالة النشطة (أو الصفحة الفرعية) لكل فتحة. ومع ذلك، المحتوى المعروض داخل الفتحة سيعتمد على نوع التنقل:

  • التنقل اللين: أثناء التنقل من جانب العميل، سيقوم Next.js بتنفيذ عرض جزئي، تغيير الصفحة الفرعية داخل الفتحة، مع الحفاظ على الصفحات الفرعية النشطة للفتحات الأخرى، حتى لو لم تتطابق مع URL الحالي.
  • التنقل القاسي: بعد تحميل الصفحة بالكامل (تحديث المتصفح)، لا يمكن لـ Next.js تحديد الحالة النشطة للفتحات التي لا تتطابق مع URL الحالي. بدلاً من ذلك، سيقوم بعرض ملف default.js للفتحات غير المتطابقة، أو 404 إذا لم يكن default.js موجودًا.

معلومة مفيدة:

  • الـ 404 للمسارات غير المتطابقة يساعد على ضمان أنك لا تعرض عن طريق الخطأ مسارًا متوازيًا على صفحة لم يكن مقصودًا لها.

أمثلة

مع useSelectedLayoutSegment(s)

كل من useSelectedLayoutSegment وuseSelectedLayoutSegments يقبلان معلمة parallelRoutesKey، والتي تسمح لك بقراءة جزء المسار النشط داخل فتحة.

'use client'

import { useSelectedLayoutSegment } from 'next/navigation'

export default function Layout({ auth }: { auth: React.ReactNode }) {
  const loginSegment = useSelectedLayoutSegment('auth')
  // ...
}
'use client'

import { useSelectedLayoutSegment } from 'next/navigation'

export default function Layout({ auth }) {
  const loginSegment = useSelectedLayoutSegment('auth')
  // ...
}

عندما يقوم المستخدم بالتنقل إلى app/@auth/login (أو /login في شريط العناوين)، سيكون loginSegment مساويًا للسلسلة "login".

مسارات مشروطة

يمكنك استخدام المسارات المتوازية لعرض مسارات بشكل مشروط بناءً على شروط معينة، مثل دور المستخدم. على سبيل المثال، لعرض صفحة لوحة تحكم مختلفة لدور /admin أو /user:

رسم توضيحي للمسارات المشروطة
import { checkUserRole } from '@/lib/auth'

export default function Layout({
  user,
  admin,
}: {
  user: React.ReactNode
  admin: React.ReactNode
}) {
  const role = checkUserRole()
  return role === 'admin' ? admin : user
}
import { checkUserRole } from '@/lib/auth'

export default function Layout({ user, admin }) {
  const role = checkUserRole()
  return role === 'admin' ? admin : user
}

مجموعات التبويب

يمكنك إضافة layout داخل فتحة للسماح للمستخدمين بالتنقل داخل الفتحة بشكل مستقل. هذا مفيد لإنشاء تبويبات.

على سبيل المثال، فتحة @analytics تحتوي على صفحتين فرعيتين: /page-views و/visitors.

فتحة التحليلات مع صفحتين فرعيتين وتخطيط

داخل @analytics، قم بإنشاء ملف layout لمشاركة التبويبات بين الصفحتين:

import Link from 'next/link'

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <>
      <nav>
        <Link href="/page-views">مشاهدات الصفحة</Link>
        <Link href="/visitors">الزوار</Link>
      </nav>
      <div>{children}</div>
    </>
  )
}
import Link from 'next/link'

export default function Layout({ children }) {
  return (
    <>
      <nav>
        <Link href="/page-views">مشاهدات الصفحة</Link>
        <Link href="/visitors">الزوار</Link>
      </nav>
      <div>{children}</div>
    </>
  )
}

النوافذ المنبثقة (Modals)

يمكن استخدام المسارات المتوازية مع مسارات الاعتراض (Intercepting Routes) لإنشاء نوافذ منبثقة تدعم الربط العميق. هذا يسمح لك بحل التحديات الشائعة عند بناء النوافذ المنبثقة، مثل:

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

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

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

لتنفيذ هذا النمط، ابدأ بإنشاء مسار /login الذي يعرض الصفحة الرئيسية لتسجيل الدخول.

رسم توضيحي لصفحة تسجيل الدخول مع النوافذ المنبثقة
import { Login } from '@/app/ui/login'

export default function Page() {
  return <Login />
}
import { Login } from '@/app/ui/login'

export default function Page() {
  return <Login />
}

ثم، داخل فتحة @auth، أضف ملف default.js الذي يُرجع null. هذا يضمن عدم عرض النافذة المنبثقة عندما لا تكون نشطة.

export default function Default() {
  return null
}
export default function Default() {
  return null
}

داخل فتحة @auth الخاصة بك، اعترض مسار /login عن طريق تحديث مجلد /(.)login. استورد مكون <Modal> وأطفاله إلى ملف /(.)login/page.tsx:

import { Modal } from '@/app/ui/modal'
import { Login } from '@/app/ui/login'

export default function Page() {
  return (
    <Modal>
      <Login />
    </Modal>
  )
}
import { Modal } from '@/app/ui/modal'
import { Login } from '@/app/ui/login'

export default function Page() {
  return (
    <Modal>
      <Login />
    </Modal>
  )
}

معلومة مفيدة:

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

الآن، يمكنك الاستفادة من موجه Next.js لفتح وإغلاق النافذة المنبثقة. هذا يضمن تحديث URL بشكل صحيح عند فتح النافذة المنبثقة، وعند التنقل للخلف والأمام.

لفتح النافذة المنبثقة، قم بتمرير فتحة @auth كخاصية إلى تخطيط الأب وعرضها مع خاصية children.

import Link from 'next/link'

export default function Layout({
  auth,
  children,
}: {
  auth: React.ReactNode
  children: React.ReactNode
}) {
  return (
    <>
      <nav>
        <Link href="/login">فتح النافذة المنبثقة</Link>
      </nav>
      <div>{auth}</div>
      <div>{children}</div>
    </>
  )
}
import Link from 'next/link'

export default function Layout({ auth, children }) {
  return (
    <>
      <nav>
        <Link href="/login">فتح النافذة المنبثقة</Link>
      </nav>
      <div>{auth}</div>
      <div>{children}</div>
    </>
  )
}

عندما ينقر المستخدم على <Link>، ستفتح النافذة المنبثقة بدلاً من التنقل إلى صفحة /login. ومع ذلك، عند التحديث أو التحميل الأولي، التنقل إلى /login سيأخذ المستخدم إلى صفحة تسجيل الدخول الرئيسية.

إغلاق النافذة المنبثقة

يمكنك إغلاق النافذة المنبثقة عن طريق استدعاء router.back() أو باستخدام مكون Link.

'use client'

import { useRouter } from 'next/navigation'

export function Modal({ children }: { children: React.ReactNode }) {
  const router = useRouter()

  return (
    <>
      <button
        onClick={() => {
          router.back()
        }}
      >
        إغلاق النافذة المنبثقة
      </button>
      <div>{children}</div>
    </>
  )
}
'use client'

import { useRouter } from 'next/navigation'

export function Modal({ children }) {
  const router = useRouter()

  return (
    <>
      <button
        onClick={() => {
          router.back()
        }}
      >
        إغلاق النافذة المنبثقة
      </button>
      <div>{children}</div>
    </>
  )
}

عند استخدام مكون Link للتنقل بعيدًا عن صفحة لا يجب أن تعرض فتحة @auth بعد الآن، نحتاج إلى التأكد من أن المسار المتوازي يتطابق مع مكون يُرجع null. على سبيل المثال، عند التنقل للخلف إلى الصفحة الرئيسية، ننشئ مكون @auth/page.tsx:

import Link from 'next/link'

export function Modal({ children }: { children: React.ReactNode }) {
  return (
    <>
      <Link href="/">إغلاق النافذة المنبثقة</Link>
      <div>{children}</div>
    </>
  )
}
import Link from 'next/link'

export function Modal({ children }) {
  return (
    <>
      <Link href="/">إغلاق النافذة المنبثقة</Link>
      <div>{children}</div>
    </>
  )
}
export default function Page() {
  return null
}
export default function Page() {
  return null
}

أو إذا كنت تتنقل إلى أي صفحة أخرى (مثل /foo، /foo/bar، إلخ)، يمكنك استخدام فتحة catch-all:

export default function CatchAll() {
  return null
}
export default function CatchAll() {
  return null
}

معلومة مفيدة:

  • نستخدم مسار catch-all في فتحة @auth الخاصة بنا لإغلاق النافذة المنبثقة بسبب كيفية تصرف المسارات المتوازية(#behavior). بما أن عمليات التنقل من جانب العميل إلى مسار لم يعد يتطابق مع الفتحة ستبقى مرئية، نحتاج إلى مطابقة الفتحة مع مسار يُرجع null لإغلاق النافذة المنبثقة.
  • يمكن أن تتضمن الأمثلة الأخرى فتح نافذة منبثقة للصور في معرض مع وجود صفحة مخصصة /photo/[id]، أو فتح عربة تسوق في نافذة منبثقة جانبية.
  • عرض مثال للنوافذ المنبثقة مع مسارات الاعتراض والمسارات المتوازية.

واجهات تحميل ومعالجة الأخطاء

يمكن بث المسارات المتوازية بشكل مستقل، مما يسمح لك بتحديد حالات خطأ وتحميل مستقلة لكل مسار:

تمكن المسارات المتوازية من حالات الخطأ والتحميل المخصصة

راجع وثائق واجهة التحميل ومعالجة الأخطاء لمزيد من المعلومات.