توسعه فرانت‌اند

Next.js App Router: راهنمای مهاجرت

مهاجرت تدریجی از Pages Router، مرز Server و Client، قرارداد داده و cache و نمونه کامل هم‌زیستی دو Router با Layout و خطا.

1404/10/29 4 دقیقه مطالعه 2,843 بازدید

مهاجرت به App Router را از یک مسیر محدود شروع کنید که رفتار و وابستگی‌هایش روشن‌اند. جابه‌جا کردن تمام فایل‌های pages به app، قرارداد داده و state را خودکار اصلاح نمی‌کند. پیش از تغییر، معلوم کنید هر صفحه داده را کجا می‌گیرد، چه بخش‌هایی تعاملی‌اند و URL و Metadata آن چیست. همین فهرست، مبنای تصمیم درباره مرز Server و Client خواهد بود.

Pages Router و App Router می‌توانند در دوره مهاجرت کنار هم باشند. راهنمای رسمی مهاجرت Next.js مسیر تدریجی را توضیح می‌دهد. انتخاب مهاجرت باید به نیاز پروژه و هزینه نگهداری مربوط باشد؛ جمله عمومی «همیشه ارزشش را دارد» برای پروژه‌ای با وابستگی ناسازگار یا برنامه تحویل محدود کافی نیست. یک مسیر با قرارداد مشخص، ریسک و نتیجه را قابل بررسی می‌کند.

قبل از انتقال، نقشه مسیرها را تهیه کنید

URLهای فعلی، مسیرهای پویا، Queryها و redirectها را ثبت کنید. کاربر نباید با تغییر ساختار فایل، لینک قبلی را از دست بدهد. یک URL را هم‌زمان در دو Router تعریف نکنید. در نمونه آموزشی این مقاله، مسیر اصلی در app و مسیر legacy در pages است. این دو عمداً مقصد متفاوت دارند و نشان‌دهنده مرحله‌ای از مهاجرت‌اند، نه دو پیاده‌سازی هم‌زمان برای یک مسیر.

وابستگی به _app و _document را بررسی کنید. Provider، CSS عمومی و اطلاعات سند ممکن است آنجا باشند. انتقال هر کدام نیاز به محل مناسب در ساختار جدید دارد. Root Layout سند html و body را معرفی می‌کند، اما جایگزین بی‌تفاوت تمام منطق فایل‌های قبلی نیست. یک Provider کلاینتی باید در مرز مناسب قرار بگیرد تا کل برنامه بی‌دلیل Client نشود.

برای هر route، رفتار تازه بودن داده و اختیار کاربر را هم بنویسید. صفحه عمومی ثابت با داشبورد شخصی یک قرارداد cache ندارد. مهاجرت Router را با تغییر هم‌زمان همه قواعد داده مخلوط نکنید مگر نیاز مشخصی وجود دارد. اگر هر دو تغییر لازم‌اند، تصمیم‌ها را جدا مستند کنید تا علت اختلاف رفتار قابل تشخیص بماند.

Server Component و Client Component را بر اساس نیاز جدا کنید

راهنمای Server and Client Components نقش دو محیط را توضیح می‌دهد. در App Router، page و layout معمولاً Server Component هستند. رویدادهای تعاملی، state محلی و دسترسی به API مرورگر به مرز Client نیاز دارند. use client در ابتدای ماژول این مرز را معرفی می‌کند؛ لازم نیست روی هر فایل بدون بررسی اضافه شود.

یک بخش تعاملی کوچک را می‌توان در component جدا گذاشت و از صفحه سروری استفاده کرد. داده عبوری باید با قرارداد serialization سازگار باشد. ارسال function معمولی سرور به عنوان onClick به کلاینت، همین قرارداد را رعایت نمی‌کند. در نمونه، شمارنده state محلی دارد و داده حساس یا function سروری به آن داده نمی‌شود.

Client Component به معنی اجرا شدن تمام کد فقط پس از باز شدن مرورگر نیست. رندر اولیه و hydration قواعد خود را دارند. دسترسی به window یا localStorage در محل نامناسب همچنان می‌تواند مسئله ایجاد کند. مرز use client را با مجوز دسترسی بی‌قید به API مرورگر در هر مرحله یکی نگیرید؛ زمان اجرای کد باید روشن باشد.

دریافت داده و APIهای درخواست را بازبینی کنید

الگوهای getServerSideProps و getStaticProps در pages را به همان شکل داخل app کپی نکنید. دریافت داده در component سروری و قراردادهای فعلی cache باید با نیاز route تعریف شوند. نمونه این مقاله داده ثابت دارد تا انتقال ساختار با وابستگی شبکه مخلوط نشود. برای API واقعی، Schema پاسخ، خطا و رفتار تازه‌سازی لازم است.

در مستندات جاری، params و searchParams در page قرارداد Promise دارند. نمونه‌های قدیمی که آن‌ها را همواره شیء هم‌زمان فرض می‌کنند، باید با نسخه هدف تطبیق داده شوند. این تغییر را در مسیرهای پویا و Metadata بررسی کنید. اصلاح TypeScript بدون اصلاح روش دسترسی، قرارداد runtime را کامل نمی‌کند.

قواعد cache در نسخه‌های Next.js تغییر کرده‌اند. از حفظ، همه fetchها را cached یا همه صفحات را dynamic معرفی نکنید. مدل انتخاب‌شده، از جمله فعال بودن Cache Components، اهمیت دارد. مقاله PPR و Cache Components ادامه مرتبط است. داده شخصی نباید به دلیل یک پیش‌فرض قدیمی در cache مشترک قرار بگیرد.

نمونه کوچک با دو Router و یک component تعاملی

برای ساخت پایه، دستور npx create-next-app@16.4.0 router-demo را اجرا کنید؛ این نسخه در مستندات رسمی بررسی‌شده این راهنما نسخه جاری است. انتخاب‌های TypeScript، ESLint، App Router و پوشه src را فعال و Tailwind را غیرفعال کنید. نام پروژه router-demo است. وابستگی‌ها و تنظیم‌های پایه را scaffold تولید می‌کند؛ Lockfile را نگه دارید تا نسخه نصب‌شده قابل پیگیری باشد.

راهنمای نصب Next.js پیش‌نیازهای Node و روش اجرای پروژه را معرفی می‌کند. در پوشه پروژه، npm run dev اجرا و localhost:3000 را باز کنید. فایل‌های زیر را در مسیرهای گفته‌شده قرار دهید و فایل‌های page، layout و globals تولیدشده را با این نسخه جایگزین کنید. این مثال سرویس بیرونی، پایگاه داده، تصویر یا کلید API ندارد.

ساختار app و صفحه اصلی

فایل layout را در src/app/layout.tsx، صفحه را در src/app/page.tsx و CSS را در src/app/globals.css بگذارید. زبان و جهت سند در Layout تعریف شده‌اند. صفحه اصلی یک Server Component با محتوای ثابت و لینک به مسیر قدیمی است. Metadata پایه عنوان و توضیح واقعی همین نمونه را معرفی می‌کند و دارایی خیالی ندارد.

import type { Metadata } from 'next';
import type { ReactNode } from 'react';
import './globals.css';
export const metadata: Metadata = { title:'نمونه مهاجرت Router', description:'دو مسیر مستقل در App Router و Pages Router.' };
export default function RootLayout({ children }: { children:ReactNode }) {
  return <html lang="fa" dir="rtl"><body>{children}</body></html>;
}
import Link from 'next/link';
import Counter from './Counter';
export default function Home() {
  return <main><h1>صفحه App Router</h1><p>این محتوا ثابت و سروری است.</p><Counter /><p><Link href="/legacy">رفتن به مسیر Pages Router</Link></p></main>;
}
* { box-sizing:border-box; }
body { margin:0; background:#f4f6f9; color:#17212b; font:1rem/1.9 system-ui,sans-serif; }
main { max-inline-size:42rem; margin-inline:auto; padding:2rem 1rem; }
button { font:inherit; padding:.4rem .8rem; color:#fff; background:#164ca4; border:0; }
a { color:#164ca4; }
:focus-visible { outline:3px solid #164ca4; outline-offset:4px; }

مرز تعاملی و مسیر قدیمی

Counter را در src/app/Counter.tsx و صفحه قدیمی را در src/pages/legacy.tsx قرار دهید. صفحه legacy از API Router جدید استفاده نمی‌کند. پیمایش بین دو Router را مانند پیمایش درون یک Layout مشترک فرض نکنید؛ در دوره هم‌زیستی، مرز Router می‌تواند رفتار بارگیری و حفظ state متفاوتی داشته باشد. state شمارنده نمونه تضمین حفظ در تمام جابه‌جایی‌ها ندارد.

'use client';
import { useState } from 'react';
export default function Counter() {
  const [count,setCount] = useState(0);
  return <section aria-label="شمارنده محلی"><p role="status">مقدار: {count}</p><button type="button" onClick={() => setCount(value => value + 1)}>افزایش شمارنده</button></section>;
}
import Link from 'next/link';
import Head from 'next/head';
export default function Legacy() {
  return <><Head><title>مسیر قدیمی نمونه</title></Head><main dir="rtl"><h1>صفحه Pages Router</h1><p>این مسیر هنوز در pages قرار دارد.</p><Link href="/">بازگشت به App Router</Link></main></>;
}

برای CSS مسیر قدیمی، src/pages/_app.tsx زیر را هم اضافه کنید. Root Layout پوشه app قرارداد صفحه‌های pages را اجرا نمی‌کند؛ وارد کردن CSS در این دو ورودی، استفاده همان فایل مشترک را در هر Router روشن می‌کند.

import type { AppProps } from 'next/app';
import '../app/globals.css';
export default function App({ Component,pageProps }:AppProps) {
  return <Component {...pageProps} />;
}

Loading و Error مرزهای مشخص دارند

loading.tsx برای وضعیت انتظار segment و error.tsx برای خطاهای مرتبط قرارداد دارند. وجود فایل loading تضمین نمی‌کند در صفحه‌ای با محتوای آماده و ثابت دیده شود. Error Boundary نیز همه خطاهای ممکن، از جمله هر رویداد کلاینتی یا خطای Layout هم‌سطح، را یکسان پوشش نمی‌دهد. محدوده فایل‌ها را از راهنمای رسمی همان convention بخوانید.

برای نمونه، فایل‌های زیر را در src/app/loading.tsx و src/app/error.tsx بگذارید. صفحه ثابت عمداً خطای ساختگی ایجاد نمی‌کند و ادعای مشاهده این stateها نداریم. پیام خطا جزئیات داخلی را نمایش نمی‌دهد و reset مسیر تلاش دوباره را ارائه می‌کند. در پروژه واقعی، ثبت خطا و وضعیت داده باید جدا به همین قرارداد متصل شوند.

export default function Loading() {
  return <main><p role="status">محتوای این مسیر در حال آماده شدن است.</p></main>;
}
'use client';
export default function ErrorPage({ reset }: { error:Error & { digest?:string }; reset:() => void }) {
  return <main><h1>نمایش مسیر ناموفق بود</h1><p>می‌توانید دوباره تلاش کنید.</p><button type="button" onClick={reset}>تلاش دوباره</button></main>;
}

برای توضیح تجربه انتظار و پیام، Loading States و Error Handling ادامه‌های مرتبط‌اند. رندر شدن fallback به معنی موفقیت دوباره درخواست نیست؛ reset باید با وضعیت واقعی عملیات هماهنگ باشد، به‌خصوص اگر قبلاً اثر سروری رخ داده باشد.

Metadata و وابستگی‌ها را پس از انتقال حفظ کنید

عنوان و توضیح قبلی را با نیاز صفحه در Metadata API منتقل کنید. چند منبع تکراری برای title نسازید. CSS عمومی، فونت و scriptهای خارجی نیز باید در محل مناسب ساختار جدید باشند. کتابخانه‌ای که به Router قدیمی یا lifecycle خاص وابسته است ممکن است به adapter یا مرز Client نیاز داشته باشد. Codemod کمک مکانیکی است و تصمیم معماری را صادر نمی‌کند.

برای تحویل هر route، مسیر، مرز Client، داده، cache و state خطا را ثبت کنید. مهاجرت تدریجی زمانی قابل نگهداری است که مشخص باشد کدام بخش منتقل شده و چه وابستگی باقی مانده است. حذف فایل قدیمی فقط پس از انتقال واقعی قرارداد همان route معنا دارد. هدف، رفتار روشن در ساختار جدید است، نه تغییر نام پوشه بدون فهم داده و state.

برای یک route کاری، سناریوی ورود مستقیم و بازگشت از جزئیات را هم در تعریف پذیرش بیاورید. کاربر ممکن است از لینک ذخیره‌شده وارد شود و navigation قبلی را طی نکرده باشد. مقدار Query، فیلتر و محل بازگشت باید مطابق قرارداد قبلی یا تغییر مستند حفظ شوند. اگر تصمیم تازه‌ای درباره حفظ state دارید، آن را صریح به تیم محصول معرفی کنید؛ اختلاف میان دو Router نباید به عنوان رفتار طبیعی و بی‌نیاز از توضیح رها شود.

نمونه این مقاله authentication و mutation ندارد. قبل از انتقال صفحه‌ای با عملیات نوشتن، محل بررسی اختیار و نتیجه معتبر را مشخص کنید. فایل page جدید به تنهایی مجوز داده یا ایمنی عمل را فراهم نمی‌کند. همین محدودیت را در مستند مسیر ثبت کنید تا نمونه کوچک به اشتباه به عنوان معماری کامل حساب کاربر کپی نشود.

واکنش به این مقاله

اشتراک‌گذاری این مقاله:
دیدگاه‌ها 0
۰ / ۲۰۰۰

هنوز دیدگاهی ثبت نشده

اولین نفری باشید که نظر می‌دهد!