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

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

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

5 دقيقة قراءة

أنفع حقيقة عن هذا الترحيل أنك لست مضطرًا للاختيار. فـ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). الشيفرة المنسوخة من درس عمره سنتان تفكّكهما مباشرة وتفشل على نحو يبدو كأنه مشكلة بيانات.

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

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

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

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

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

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

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

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

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