2. App Router vs Pages Router
🗺️ App Router vs Pages Router
Заголовок раздела «🗺️ App Router vs Pages Router»Это, пожалуй, самый важный выбор при работе с Next.js сегодня. Два поколения системы маршрутизации, два разных подхода к архитектуре. Давай разберём всё по-честному! 🎯
Next.js 1-12 Next.js 13+──────────────────── ────────────────────────────pages/ app/ index.tsx → page.tsx about.tsx → about/page.tsx blog/ blog/ [slug].tsx → [slug]/page.tsx _app.tsx → layout.tsx _document.tsx → layout.tsx (root) api/ → api/ (route.ts)📚 Pages Router: Старый добрый способ
Заголовок раздела «📚 Pages Router: Старый добрый способ»Pages Router появился в Next.js 1.0 и работал вплоть до Next.js 12. Это проверенная система, которую знают миллионы разработчиков:
// pages/index.tsx — главная страницаexport default function HomePage({ posts }) { return ( <div> <h1>Мой блог</h1> {posts.map(post => <PostCard key={post.id} post={post} />)} </div> );}
// getServerSideProps — данные при каждом запросе (SSR)export async function getServerSideProps() { const posts = await fetch('https://api.example.com/posts').then(r => r.json()); return { props: { posts } };}// pages/blog/[slug].tsx — динамический роутexport default function BlogPost({ post }) { return <article>{post.content}</article>;}
// getStaticProps — данные при сборке (SSG)export async function getStaticProps({ params }) { const post = await getPost(params.slug); return { props: { post }, revalidate: 60 }; // ISR!}
// getStaticPaths — какие slug генерировать при сборкеexport async function getStaticPaths() { const posts = await getAllPosts(); return { paths: posts.map(p => ({ params: { slug: p.slug } })), fallback: 'blocking' };}// pages/_app.tsx — глобальная обёрткаimport type { AppProps } from 'next/app';import '../styles/globals.css';
export default function MyApp({ Component, pageProps }: AppProps) { return ( <AuthProvider> <Layout> <Component {...pageProps} /> </Layout> </AuthProvider> );}🆕 App Router: Новая эра (Next.js 13+)
Заголовок раздела «🆕 App Router: Новая эра (Next.js 13+)»App Router построен на React Server Components и меняет всё:
// app/page.tsx — главная страница// Server Component по умолчанию — нет 'use client'!export default async function HomePage() { // Данные прямо в компоненте, без getServerSideProps! const posts = await fetch('https://api.example.com/posts').then(r => r.json());
return ( <div> <h1>Мой блог</h1> {posts.map(post => <PostCard key={post.id} post={post} />)} </div> );}// app/blog/[slug]/page.tsx — динамический роутexport default async function BlogPost({ params}: { params: Promise<{ slug: string }> // Next.js 15: params async!}) { const { slug } = await params; const post = await getPost(slug);
return <article>{post.content}</article>;}
// Больше нет getStaticPaths — используем generateStaticParamsexport async function generateStaticParams() { const posts = await getAllPosts(); return posts.map(post => ({ slug: post.slug }));}
// ISR через экспорт переменнойexport const revalidate = 60;// app/layout.tsx — root layout (заменяет _app.tsx и _document.tsx)import type { Metadata } from 'next';
export const metadata: Metadata = { title: 'Мой блог', description: 'Описание сайта',};
export default function RootLayout({ children,}: { children: React.ReactNode;}) { return ( <html lang="ru"> <body> <AuthProvider> <Header /> <main>{children}</main> <Footer /> </AuthProvider> </body> </html> );}📂 Файловые конвенции App Router
Заголовок раздела «📂 Файловые конвенции App Router»В App Router каждый файл имеет специальное назначение:
app/├── layout.tsx # Обёртка — остаётся при навигации├── template.tsx # Обёртка — пересоздаётся при навигации├── page.tsx # Страница — отображается по URL├── loading.tsx # Skeleton/spinner во время загрузки├── error.tsx # UI ошибки (Error Boundary)├── not-found.tsx # UI для 404├── global-error.tsx # Ошибки в root layout└── route.ts # API endpoint (Route Handler)Подробнее о каждом:
// app/dashboard/layout.tsx — layout для /dashboard и всех дочерних роутовexport default function DashboardLayout({ children}: { children: React.ReactNode}) { return ( <div className="dashboard"> <DashboardSidebar /> <main>{children}</main> </div> );}// app/dashboard/loading.tsx — автоматически показывается пока page.tsx грузитсяexport default function DashboardLoading() { return ( <div className="skeleton"> <div className="skeleton-title" /> <div className="skeleton-line" /> <div className="skeleton-line" /> </div> );}// Next.js автоматически оборачивает page.tsx в <Suspense fallback={<DashboardLoading />}>// app/dashboard/error.tsx — обработчик ошибок'use client'; // Error boundaries должны быть Client Components!
export default function DashboardError({ error, reset,}: { error: Error & { digest?: string }; reset: () => void;}) { return ( <div> <h2>Что-то пошло не так!</h2> <p>{error.message}</p> <button onClick={reset}>Попробовать снова</button> </div> );}// app/blog/[slug]/not-found.tsx — показывается при вызове notFound()import Link from 'next/link';
export default function PostNotFound() { return ( <div> <h1>Пост не найден 😔</h1> <p>Этот пост был удалён или никогда не существовал.</p> <Link href="/blog">← Вернуться в блог</Link> </div> );}
// app/blog/[slug]/page.tsximport { notFound } from 'next/navigation';
export default async function BlogPost({ params }) { const { slug } = await params; const post = await getPost(slug);
if (!post) notFound(); // Вызовет not-found.tsx
return <article>{post.content}</article>;}⚖️ Сравнительная таблица
Заголовок раздела «⚖️ Сравнительная таблица»| Фича | Pages Router | App Router |
|---|---|---|
| Папка | pages/ | app/ |
| Данные SSR | getServerSideProps | async компонент + fetch |
| Данные SSG | getStaticProps | async компонент + generateStaticParams |
| ISR | revalidate в getStaticProps | export const revalidate = N |
| Layout | _app.tsx (один на всё) | layout.tsx (вложенные!) |
| Loading UI | Вручную с useState | loading.tsx автоматически |
| Error UI | Вручную | error.tsx автоматически |
| Server Components | ❌ | ✅ По умолчанию |
| Server Actions | ❌ | ✅ |
| Стриминг | ❌ | ✅ |
| Metadata API | Вручную через Head | export const metadata |
| Параллельные роуты | ❌ | ✅ |
| Перехватывающие роуты | ❌ | ✅ |
🔄 Сосуществование: Pages + App вместе
Заголовок раздела «🔄 Сосуществование: Pages + App вместе»Хорошая новость: можно использовать оба роутера одновременно! Это полезно для постепенной миграции:
my-app/├── app/ # Новые страницы — App Router│ ├── layout.tsx│ ├── about/│ │ └── page.tsx # /about → App Router│ └── dashboard/│ └── page.tsx # /dashboard → App Router│├── pages/ # Старые страницы — Pages Router│ ├── _app.tsx│ ├── index.tsx # / → Pages Router│ └── blog/│ └── [slug].tsx # /blog/:slug → Pages RouterПравила сосуществования:
- App Router имеет приоритет над Pages Router для одинаковых роутов
- Нельзя иметь
app/about/page.tsxиpages/about.tsxодновременно _app.tsxи_document.tsxв Pages Router НЕ влияют на App Router
🚀 Когда использовать что?
Заголовок раздела «🚀 Когда использовать что?»App Router (рекомендуется для новых проектов):
// ✅ Используй App Router если:// 1. Новый проект// 2. Нужны Server Components для производительности// 3. Хочешь Server Actions вместо API routes// 4. Нужны вложенные layouts// 5. Нужен streaming/Suspense
// Пример: полноценный Server Component// app/products/page.tsxasync function ProductsPage() { const products = await db.product.findMany(); // Прямо в компоненте! return <ProductGrid products={products} />;}Pages Router (если…):
// ⚠️ Оставайся на Pages Router если:// 1. Большой легаси-проект — миграция дорого стоит// 2. Используешь библиотеки несовместимые с RSC// 3. Команда не готова изучать новую парадигму// 4. Нужна стабильность: Pages Router более зрелый
// pages/products.tsxexport default function ProductsPage({ products }) { return <ProductGrid products={products} />;}
export async function getServerSideProps() { const products = await db.product.findMany(); return { props: { products } };}📐 Вложенные layouts: суперсила App Router
Заголовок раздела «📐 Вложенные layouts: суперсила App Router»Это одна из лучших фич App Router! Каждый сегмент может иметь свой layout:
// app/layout.tsx — root layout (всё приложение)export default function RootLayout({ children }) { return ( <html lang="ru"> <body> <GlobalNav /> {children} <Footer /> </body> </html> );}
// app/dashboard/layout.tsx — layout для /dashboard/*export default function DashboardLayout({ children }) { return ( <div className="dashboard-container"> <DashboardSidebar /> <div className="dashboard-content"> {children} </div> </div> );}
// app/dashboard/settings/layout.tsx — layout для /dashboard/settings/*export default function SettingsLayout({ children }) { return ( <div> <SettingsTabs /> {children} </div> );}
// app/dashboard/settings/profile/page.tsx// Получит: RootLayout > DashboardLayout > SettingsLayout > ProfilePageРезультирующее дерево компонентов:
<RootLayout> ← app/layout.tsx <DashboardLayout> ← app/dashboard/layout.tsx <SettingsLayout> ← app/dashboard/settings/layout.tsx <ProfilePage> ← app/dashboard/settings/profile/page.tsx </SettingsLayout> </DashboardLayout></RootLayout>🎯 Резюме урока
Заголовок раздела «🎯 Резюме урока»App Router — будущее Next.js. Если начинаешь новый проект — используй его.
Pages Router — всё ещё поддерживается и отлично работает для существующих проектов.
Ключевые файловые конвенции App Router:
page.tsx— страницаlayout.tsx— обёртка (персистентная)loading.tsx— UI загрузкиerror.tsx— UI ошибкиnot-found.tsx— UI 404route.ts— API endpoint