الترحيل من Pages Router إلى App Router دون تجميد التطوير
الموجّهان يعملان جنبًا إلى جنب في التطبيق نفسه، فالترحيل يجري مسارًا مسارًا بينما يواصل فريقك الإطلاق. هذا هو الترتيب الذي ينجح، والمواضع الثلاثة التي يتعثّر فيها عادةً.
أنفع حقيقة عن هذا الترحيل أنك لست مضطرًا للاختيار. فـpages/ وapp/ يعملان
في التطبيق نفسه، وفي العملية نفسها، على النشر نفسه. يُطابَق الطلب مع app/
أولًا، ويسقط إلى pages/ إن لم يجد شيئًا.
معنى ذلك أن تجميد الميزات ليس شرطًا للترحيل، بل عَرَضٌ لتخطيطه بوصفه تغييرًا واحدًا كبيرًا بدل أربعين تغييرًا صغيرًا.
الترتيب الذي ينجح
رحّل من الأوراق إلى الداخل، لا من الجذر إلى الخارج.
- مسار قليل الزيارات وقليل الخطر أولًا. شيء مثل
/about. لا لأنه مهم، بل لأنه يجبرك على بناء التخطيط ومساعد البيانات الوصفية وأعراف جلب البيانات التي ستستعملها في كل مكان آخر، على صفحة لن يلاحظ أحد تعطّلها ساعة. - بقيّة المسارات التسويقية الثابتة. تتحوّل شبه آليًا، وفيها أكبر مكسب في الأداء.
- المسارات الديناميكية للقراءة فقط. صفحات المنتجات والمقالات والقوائم.
هنا تحوّل
getStaticPropsوgetServerSidePropsإلى مكوّناتasync، وهذا هو الجزء الأكبر من العمل الآلي. - المسارات المُوثَّقة والتفاعلية. لوحات التحكّم والإعدادات والدفع. هذه تحمل الخطر الحقيقي لأنها تحمل التعديلات.
- التخطيط الجذري أخيرًا. نقل
_app.tsxو_document.tsxمبكرًا يدفع كل مسار إلى الشجرة الجديدة قبل أن يكون أيٌّ منها جاهزًا.
الفرق التي تعكس هذا الترتيب - فتبدأ بالهيكل لأنه يبدو كالأساس - هي التي تنتهي إلى التجميد.
ما يصير إليه كل واجهة من Pages
| Pages Router | App Router |
|---|---|
getStaticProps | مكوّن async + fetch مع revalidate أو وسم |
getServerSideProps | مكوّن async + cache: 'no-store' |
getStaticPaths | generateStaticParams |
_app.tsx | app/layout.tsx |
_document.tsx | app/layout.tsx (وسما html وbody) |
next/head | تصدير metadata أو generateMetadata |
useRouter().query | خاصّيتا params وsearchParams |
| مسارات API | Route 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 لا أحد يريد لمسها يغطّي ما يجب فعله أولًا.
