以下是Next.js 中 App Router 和 Pages Router 的区别,并提供相应的代码示例方便大家理解记忆。
App Router 是 Next.js 演进过程中的一个重要里程碑。Pages Router 是 Next.js 长期以来的核心路由机制,稳定且功能强大。而从 Next.js 13 开始引入的 App Router ,则被视为下一代路由方案,它基于 React Server Components 构建,带来了许多革新性的特性。
1、核心区别概览
特性 | App Router (Next.js 13+) | Pages Router (传统方式) |
---|---|---|
核心架构 | 基于 React Server Components (RSC) | 基于客户端组件 (Client Components) |
目录结构 | app/ |
pages/ |
文件约定 | page.tsx , layout.tsx , loading.tsx , error.tsx , template.tsx , route.ts |
_app.tsx , _document.tsx , 文件名即路由 (e.g., about.tsx ) |
路由定义 | 文件夹定义路由,page.tsx 作为 UI |
文件定义路由 (e.g., pages/about.tsx -> /about ) |
布局机制 | 内置且嵌套的 layout.tsx 文件,状态在导航时保留 |
需要手动创建共享组件或使用 _app.tsx ,状态可能丢失 |
数据获取 | 在 Server Components 中直接使用 async/await 和 fetch |
getServerSideProps , getStaticProps , getInitialProps |
渲染模式 | 默认是 Server Components (服务端渲染),可选择性使用 "use client" |
需明确指定 SSR, SSG, CSR, ISR |
API 路由 | route.ts 文件 (支持所有 HTTP 方法) |
pages/api/ 目录下的文件 (一个文件一个端点) |
加载状态 | 内置 loading.tsx ,自动应用 React Suspense |
需要手动实现加载状态 UI |
错误处理 | 内置 error.tsx ,自动应用 React Error Boundary |
需要自定义错误边界或使用 _error.tsx |
SEO 和元数据 | generateMetadata 函数或静态 metadata 对象 |
使用 <Head> 组件 (next/head ) |
异步性 | 组件级别可以是 async |
页面级别的数据获取函数是 async |
2、详细对比与代码示例
2.1、目录结构和路由定义
Pages Router: 文件即路由。
pages/index.tsx
->/
pages/about.tsx
->/about
pages/blog/[slug].tsx
->/blog/:slug
App Router: 文件夹定义路由,page.tsx
文件用于呈现该路由的 UI。
app/page.tsx
->/
app/about/page.tsx
->/about
app/blog/[slug]/page.tsx
->/blog/:slug
2.2、布局 (Layouts)
Pages Router:
布局通常在 pages/_app.tsx
中实现,或者在每个页面中手动导入一个共享的 Layout 组件。在页面切换时,整个页面会重新渲染,Layout 组件的状态可能会丢失(除非在 _app.tsx
中巧妙处理)。
pages/_app.tsx
示例:
tsx
import Layout from '../components/Layout';
import type { AppProps } from 'next/app';
function MyApp({ Component, pageProps }: AppProps) {
return (
<Layout>
<Component {...pageProps} />
</Layout>
);
}
export default MyApp;
components/Layout.tsx
:
tsx
import { ReactNode } from 'react';
export default function Layout({ children }: { children: ReactNode }) {
return (
<div>
<nav>
{/* Navigation bar */}
</nav>
<main>{children}</main>
</div>
);
}
App Router:
引入了内置的 layout.tsx
文件。布局是分层和嵌套的,当用户在属于同一个布局的路由之间导航时,布局本身不会重新渲染,其状态(如输入框内容、滚动位置)得以保留。
app/layout.tsx
(根布局):
tsx
import { ReactNode } from 'react';
// 这是必须的根布局,会应用到所有路由
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<body>
<header>My Website Header</header>
{children}
<footer>My Website Footer</footer>
</body>
</html>
);
}
app/dashboard/layout.tsx
(嵌套布局):
tsx
import { ReactNode } from 'react';
export default function DashboardLayout({ children }: { children: ReactNode }) {
return (
<section>
<nav>
{/* Dashboard specific navigation */}
</nav>
{children}
</section>
);
}
2.3、数据获取
Pages Router:
pages/posts/[id].tsx
示例 (SSR):
tsx
import { GetServerSideProps } from 'next';
type Post = {
id: string;
title: string;
body: string;
};
export const getServerSideProps: GetServerSideProps = async (context) => {
const { id } = context.params as { id: string };
const res = await fetch(`https://api.example.com/posts/${id}`);
const post: Post = await res.json();
return {
props: { post },
};
};
export default function PostPage({ post }: { post: Post }) {
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
</article>
);
}
App Router:
app/posts/[id]/page.tsx
示例:
tsx
type Post = {
id: string;
title: string;
body: string;
};
async function getPost(id: string): Promise<Post> {
const res = await fetch(`https://api.example.com/posts/${id}`, { cache: 'no-store' });
if (!res.ok) {
throw new Error('Failed to fetch data');
}
return res.json();
}
// 页面组件本身可以是 async 函数
export default async function PostPage({ params }: { params: { id: string } }) {
const post = await getPost(params.id);
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
</article>
);
}
2.4、服务端组件 vs 客户端组件
app/components/Counter.tsx
(客户端组件):
tsx
"use client";
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
You clicked {count} times
</button>
);
}
app/dashboard/page.tsx
(服务端组件,使用客户端组件):
tsx
import Counter from '../components/Counter';
// 这是一个服务端组件 (默认)
async function getSomeData(): Promise<{ message: string }> {
return { message: "Hello from the server!" };
}
export default async function DashboardPage() {
const data = await getSomeData();
return (
<div>
<h1>Dashboard</h1>
<p>Server-side data: {data.message}</p>
<Counter />
</div>
);
}
2.5、API 路由 (API Routes)
pages/api/hello.ts
:
tsx
import type { NextApiRequest, NextApiResponse } from 'next';
export default function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === 'GET') {
res.status(200).json({ name: 'John Doe' });
} else {
res.status(405).end(); // Method Not Allowed
}
}
app/api/hello/route.ts
:
tsx
import { NextResponse } from 'next/server';
export async function GET() {
return NextResponse.json({ name: 'John Doe' });
}
export async function POST(request: Request) {
const body = await request.json();
return NextResponse.json({ message: 'Data received', body });
}
2.6、加载与错误处理
app/dashboard/loading.tsx
:
tsx
export default function Loading() {
return <p>Loading dashboard data...</p>;
}
app/dashboard/error.tsx
:
tsx
"use client";
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
return (
<div>
<h2>Something went wrong in the dashboard!</h2>
<p>{error.message}</p>
<button onClick={() => reset()}>Try again</button>
</div>
);
}
3、选择哪个 Router?
- 新项目: 强烈推荐使用 App Router。它是 Next.js 的未来,提供了更优的性能(更小的客户端包体积)、更好的开发体验(组件级数据获取、内置布局和加载/错误状态)和更灵活的架构。
- 现有项目: 如果你的项目已经使用 Pages Router 并且稳定运行,可以不必急于迁移。Next.js 官方支持在同一个项目中同时使用两种 Router,你可以逐步将新的功能或页面用 App Router 来构建。
App Router 的学习曲线相对陡峭一些,因为它引入了 React Server Components 这个新概念。但一旦掌握,它所带来的优势是巨大的。
4、写在最后
如果有遗漏和其他看法欢迎各位大佬补充说明!
好了,今天就分享到这了。Bye~~