تخطَّ إلى المحتوى

الترحيل من Pages Router إلى App Router دون تجميد التطوير

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

7 دقيقة قراءة

أنفع حقيقة عن هذا الترحيل أنك لست مضطرًا للاختيار. فـpages/ وapp/ يعملان في التطبيق نفسه، وفي العملية نفسها، على النشر نفسه. يُطابَق الطلب مع app/ أولًا، ويسقط إلى pages/ إن لم يجد شيئًا.

معنى ذلك أن تجميد الميزات ليس شرطًا للترحيل، بل عَرَضٌ لتخطيطه بوصفه تغييرًا واحدًا كبيرًا بدل أربعين تغييرًا صغيرًا.

الترتيب الذي ينجح

رحّل من الأوراق إلى الداخل، لا من الجذر إلى الخارج.

  1. مسار قليل الزيارات وقليل الخطر أولًا. شيء مثل /about. لا لأنه مهم، بل لأنه يجبرك على بناء التخطيط ومساعد البيانات الوصفية وأعراف جلب البيانات التي ستستعملها في كل مكان آخر، على صفحة لن يلاحظ أحد تعطّلها ساعة.
  2. بقيّة المسارات التسويقية الثابتة. تتحوّل شبه آليًا، وفيها أكبر مكسب في الأداء.
  3. المسارات الديناميكية للقراءة فقط. صفحات المنتجات والمقالات والقوائم. هنا تحوّل getStaticProps وgetServerSideProps إلى مكوّنات async، وهذا هو الجزء الأكبر من العمل الآلي.
  4. المسارات المُوثَّقة والتفاعلية. لوحات التحكّم والإعدادات والدفع. هذه تحمل الخطر الحقيقي لأنها تحمل التعديلات.
  5. التخطيط الجذري أخيرًا. نقل _app.tsx و_document.tsx مبكرًا يدفع كل مسار إلى الشجرة الجديدة قبل أن يكون أيٌّ منها جاهزًا.

الفرق التي تعكس هذا الترتيب - فتبدأ بالهيكل لأنه يبدو كالأساس - هي التي تنتهي إلى التجميد.

ما يصير إليه كل واجهة من Pages

Pages RouterApp Router
getStaticPropsمكوّن async + fetch مع revalidate أو وسم
getServerSidePropsمكوّن async + cache: 'no-store'
getStaticPathsgenerateStaticParams
_app.tsxapp/layout.tsx
_document.tsxapp/layout.tsx (وسما html وbody)
next/headتصدير metadata أو generateMetadata
useRouter().queryخاصّيتا params وsearchParams
مسارات APIRoute Handlers، أو Server Actions للتعديلات

المسار عادةً يقصُر. هذا:

// pages/products/[slug].tsx
export async function getStaticProps({ params }) {
  const product = await getProduct(params.slug);
  if (!product) return { notFound: true };
  return { props: { product }, revalidate: 3600 };
}
 
export async function getStaticPaths() {
  const products = await getProducts();
  return {
    paths: products.map((p) => ({ params: { slug: p.slug } })),
    fallback: 'blocking',
  };
}
 
export default function ProductPage({ product }) {
  return <Product data={product} />;
}

يصير هذا:

// app/products/[slug]/page.tsx
export async function generateStaticParams() {
  const products = await getProducts();
  return products.map((p) => ({ slug: p.slug }));
}
 
export default async function ProductPage({ params }) {
  const { slug } = await params;
  const product = await getProduct(slug);
  if (!product) notFound();
 
  return <Product data={product} />;
}

انتبه إلى await params. في إصدارات Next.js الحالية، params وsearchParams وعود (promises). الشيفرة المنسوخة من درس عمره سنتان تفكّكهما مباشرة وتفشل على نحو يبدو كأنه مشكلة بيانات.

ما يتشاركه الموجّهان وما لا يتشاركانه

التعايش الذي يجعل الترحيل التدريجي ممكنًا ليس كاملًا، وحيث لا يكون كاملًا يتعثّر الترحيل.

يتشاركان الطلب. نشرة واحدة، ونطاق واحد، وnext.config واحد، ويُحلّ المسار إلى الموجّه الذي يملكه - مع أسبقيّة app/ حين يُعرّف الاثنان المسار نفسه. وهذه التفصيلة الأخيرة هي الآليّة التي تقوم عليها الاستراتيجية كلّها: تُضيفون نسخة App Router فتفوز، ويبقى الملف القديم تراجعًا جاهزًا، على بُعد حذفٍ واحد من التنظيف وعلى بُعد ردّ واحد من العودة للحياة.

ولا يتشاركان التخطيط. فـpages/_app.tsx يلفّ مسارات Pages وحدها، وapp/layout.tsx يلفّ مسارات App وحدها، فيصير كل ما هو عامّ - التحليلات، ومزوّد السمة، والترويسة - موجودًا مرّتين طوال مدّة الترحيل. كرّروه عن قصد واحذفوا نسخةً في النهاية. أما محاولة تفادي التكرار باستيراد أحدهما داخل الآخر فهي الطريق إلى مزوّد عميل يلفّ شجرة الخادم.

ولا يتشاركان التنقّل من جهة العميل. الرابط من مسار Pages إلى مسار App تحميل مستند كامل لا انتقال عميل. وفي ترحيل طويل يظهر ذلك صفحةً تبدو أبطأ من طرفيها معًا، ويزول حين يستقرّ الطرفان في الجهة نفسها.

المواضع الثلاثة التي يتعثّر فيها

وسم الهيكل بـuse client كي يُترجَم الترحيل. شيءٌ في التخطيط القديم يستعمل موفّر سياق، فيشتكي البناء، فيضع أحدهم 'use client' في أعلى التخطيط الجذري. صار التطبيق يُترجَم، وصار كل مسار يرسل الشجرة كاملة إلى المتصفّح، وذهبت الفائدة الأساسية من الترحيل بينما الكلفة تُدفَع كاملة. ادفع الموفّرين نزولًا إلى مكوّن عميل يغلّف children، وأبقِ التخطيط على الخادم. هذا هو انضباط الحدود نفسه في أين يكلّفك use client فعلًا.

نقل طبقة البيانات كما هي. كانت getServerSideProps تعمل مرّة لكل مسار، فبنت معظم الشيفرات دالّة واحدة تجلب كل شيء لكل صفحة. في App Router يستطيع كل مكوّن أن يجلب ما يحتاجه، وتُوحَّد الطلبات المتطابقة داخل عرض واحد تلقائيًا. الإبقاء على الدالّة الجبّارة يعمل، لكنه يُبقي المسار كلّه ينتظر أبطأ استدعاء: فلا شيء يتدفّق، ولا تكسب من Suspense شيئًا.

ترك next/head في مكانه. في app/ هو معطّل بصمت. لا خطأ ولا تحذير - فقط مسار بلا عنوان ولا وصف، لا يلاحظه أحد حتى يصل تقرير سيو بعد شهر. ابحث عنه بـgrep قبل الإطلاق، وحوّل كل موضع إلى تصدير metadata.

تأجيل فحص المصادقة إلى النهاية. المصادقة المكتوبة لـgetServerSideProps تُعيد تحويلًا من دالّة تملك المسار كلّه. وفي App Router يكون القرار المكافئ مكانه عادةً تخطيطًا أو حدّ المسار نفسه، والشكل مختلف بما يكفي لتؤجّله الفرق - فتكون المسارات خلفه آخر ما ينتقل، ويتوقّف الترحيل قبل خطوة من نهايته على الأسطح الأهمّ تحديدًا. انقلوا مسارًا مُوثَّق الدخول مبكّرًا وعن قصد، لتستقرّ الأنماط والمخاطر منخفضة.

إثبات النجاح، مسارًا مسارًا

قبل نقل أي مسار، سجّل ما يفعله الآن: LCP وINP من بيانات الميدان، والـ HTML المعروض، والعنوان والرابط المعياري، وJSON-LD، وحجم First Load JS. وبعد النقل، قارن. الترحيل بلا «قبل» هو إعادة كتابة بخطوات إضافية.

المسارات التي نقلناها بهذه الطريقة تفقد عادةً بين 30% و50% من جافاسكريبت الخاصة بها ولا تكسب خطرًا، لأنه لم يكن في أي لحظة أكثر من مسار واحد في حالة مجهولة.

متى يجوز التوقّف

الترحيل الذي يبلغ تسعين بالمئة ثم يقف عندها نتيجة شائعة ومُبخَّسة، وليست فشلًا بالضرورة. موجّهان في تطبيق واحد إعدادٌ مدعوم لا حالة عطل، والعشرة بالمئة الباقية عادةً حفنة مسارات لها سبب حقيقي في كونها عسيرة: اعتماد على خادم مخصّص، أو مكتبة بلا حكاية لمكوّنات الخادم، أو صفحة ستُستبدل الربع القادم على أي حال.

واختبار قبول التوقّف هو: هل تكلّفكم مسارات Pages المتبقّية شيئًا مستمرًّا؟ إن كانت معزولة وقليلة الزيارات ولا يحرّرها أحد، فالإتمام ترتيبٌ لا قيمة. أما إن كانت تحتجز التخطيط المشترك - بحيث لا تستطيع جهة App Router إزالة المزوّد المكرّر - فهي تكلّفكم على كل مسار، وذلك هو سبب الإتمام.

وما لا يجوز أبدًا هو التوقّف دون تدوينه. فالترحيل بلا حالة نهائية مكتوبة يصير، مع أول تغيّر في الأشخاص، قاعدة شيفرة لا يعرف أحد فيها أيّ العُرفين ساري المفعول، فيُنسخ الاثنان معًا إلى الأمام.

وإن كانت الشيفرة التي ترحّلها أيضًا شيفرة لا أحد يريد لمسها، فتلك مشكلة ثانية فوق هذه - تسلّم شيفرة Next.js لا أحد يريد لمسها يغطّي ما يجب فعله أولًا.

أسئلة ذات صلة

هل يعمل الموجّهان معًا؟
نعم، وهذا هو المقصود. يتعايشان في تطبيق واحد، فيجري الترحيل مسارًا بمسار ولا يتوقف شيء أثناءه.
أي مسار ننقل أولًا؟
مسار حقيقي غير حرج. صفحة تسويقية تعلّم الفريق النموذج، وصفحة الدفع تعلّمه إياه تحت الضغط.
ما بديل getServerSideProps؟
لا بديل، لأن المكوّن نفسه يصير موضع جلب البيانات. وهذا أبطأ التغييرات ترسخًا، وأجدرها بأن يُنفَّذ بفهم لا آليًا.
هل يجب نقل كل شيء؟
في النهاية نعم، إن أردت نموذجًا ذهنيًا واحدًا في الشيفرة. لكن ترحيلًا متعثرًا ترك المسارات الصعبة خلفه يظل أفضل من إعادة كتابة لم تصدر قط.

← العودة إلى كل المقالات

اتصل بنا+1 848 272 7583واتساب+90 850 308 5436البريدinfo@codefacture.comصفحة التواصل