Next.js App Router
React 全栈开发的最佳实践框架
1. App Router 架构
1.1 目录结构
app/
├── layout.tsx # 根布局
├── page.tsx # 首页 /
├── loading.tsx # 加载状态
├── error.tsx # 错误处理
├── not-found.tsx # 404 页面
│
├── about/
│ └── page.tsx # /about
│
├── blog/
│ ├── layout.tsx # 博客布局
│ ├── page.tsx # /blog
│ └── [slug]/
│ └── page.tsx # /blog/:slug
│
├── (marketing)/ # 路由组(不影响 URL)
│ ├── pricing/
│ │ └── page.tsx # /pricing
│ └── features/
│ └── page.tsx # /features
│
└── api/
└── users/
└── route.ts # API 路由 /api/users1.2 特殊文件约定
| 文件 | 作用 |
|---|---|
page.tsx | 页面组件,使路由可访问 |
layout.tsx | 共享布局,保持状态 |
loading.tsx | 加载 UI(Suspense boundary) |
error.tsx | 错误 UI(Error boundary) |
not-found.tsx | 404 页面 |
route.ts | API 路由处理 |
template.tsx | 类似 layout,但不保持状态 |
1.3 Layout 嵌套
tsx
// app/layout.tsx - 根布局
export default function RootLayout({ children }) {
return (
<html>
<body>
<Header />
{children}
<Footer />
</body>
</html>
);
}
// app/blog/layout.tsx - 嵌套布局
export default function BlogLayout({ children }) {
return (
<div className="blog-container">
<Sidebar />
<main>{children}</main>
</div>
);
}
// /blog 页面结构:
// RootLayout
// └── BlogLayout
// └── BlogPage2. 数据获取
2.1 Server Components 数据获取
tsx
// app/posts/page.tsx - Server Component(默认)
async function PostsPage() {
// 直接在组件中 await
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
export default PostsPage;2.2 并行数据获取
tsx
async function Dashboard() {
// 并行请求,不阻塞
const [user, posts, notifications] = await Promise.all([
getUser(),
getPosts(),
getNotifications()
]);
return (
<div>
<UserProfile user={user} />
<PostList posts={posts} />
<NotificationList notifications={notifications} />
</div>
);
}2.3 流式渲染
tsx
import { Suspense } from 'react';
async function SlowComponent() {
const data = await fetchSlowData(); // 需要 3 秒
return <div>{data}</div>;
}
export default function Page() {
return (
<div>
<h1>Dashboard</h1>
<FastComponent /> {/* 立即显示 */}
<Suspense fallback={<Skeleton />}>
<SlowComponent /> {/* 流式传输 */}
</Suspense>
</div>
);
}3. 缓存策略
3.1 fetch 缓存
tsx
// 默认缓存(推荐用于静态数据)
const data = await fetch('https://api.example.com/data');
// 不缓存(动态数据)
const data = await fetch('https://api.example.com/data', {
cache: 'no-store'
});
// 定时重验证
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 } // 1小时后重新验证
});
// 标签重验证
const data = await fetch('https://api.example.com/data', {
next: { tags: ['posts'] } // 打标签
});3.2 路由段缓存配置
tsx
// 页面级别的缓存配置
export const revalidate = 3600; // 每小时重验证
export const dynamic = 'force-dynamic'; // 总是动态渲染
export const fetchCache = 'force-no-store'; // 禁用 fetch 缓存3.3 重验证
tsx
import { revalidatePath, revalidateTag } from 'next/cache';
// Server Action 中重验证
async function createPost(formData: FormData) {
'use server';
await db.posts.create({ ... });
// 方式一:重验证路径
revalidatePath('/posts');
// 方式二:重验证标签
revalidateTag('posts');
}4. 路由与导航
4.1 Link 组件
tsx
import Link from 'next/link';
function Navigation() {
return (
<nav>
<Link href="/">Home</Link>
<Link href="/about">About</Link>
<Link href="/blog/hello-world">Blog Post</Link>
{/* 预加载 */}
<Link href="/dashboard" prefetch={true}>Dashboard</Link>
{/* 禁用预加载 */}
<Link href="/large-page" prefetch={false}>Large Page</Link>
</nav>
);
}4.2 useRouter
tsx
'use client';
import { useRouter } from 'next/navigation';
function NavigateButton() {
const router = useRouter();
return (
<button onClick={() => {
router.push('/dashboard'); // 导航
router.replace('/login'); // 替换当前历史
router.refresh(); // 刷新当前路由
router.back(); // 后退
router.forward(); // 前进
}}>
Navigate
</button>
);
}4.3 动态路由
tsx
// app/blog/[slug]/page.tsx
interface Props {
params: { slug: string };
searchParams: { [key: string]: string | string[] | undefined };
}
export default function BlogPost({ params, searchParams }: Props) {
return <h1>Post: {params.slug}</h1>;
}
// 生成静态参数
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map(post => ({ slug: post.slug }));
}4.4 路由组
app/
├── (marketing)/ # 营销相关页面共享布局
│ ├── layout.tsx
│ ├── about/
│ └── pricing/
│
├── (shop)/ # 商店相关页面共享布局
│ ├── layout.tsx
│ ├── products/
│ └── cart/
│
└── layout.tsx # 根布局
// URL 不包含路由组名称
// /about, /pricing, /products, /cart5. Loading 和 Error 处理
5.1 Loading UI
tsx
// app/dashboard/loading.tsx
export default function Loading() {
return (
<div className="loading-container">
<Spinner />
<p>Loading dashboard...</p>
</div>
);
}5.2 Error Boundary
tsx
// app/dashboard/error.tsx
'use client';
interface ErrorProps {
error: Error & { digest?: string };
reset: () => void;
}
export default function Error({ error, reset }: ErrorProps) {
return (
<div className="error-container">
<h2>Something went wrong!</h2>
<p>{error.message}</p>
<button onClick={reset}>Try again</button>
</div>
);
}5.3 Not Found
tsx
// app/not-found.tsx
export default function NotFound() {
return (
<div>
<h2>Page Not Found</h2>
<p>Could not find the requested resource.</p>
<Link href="/">Return Home</Link>
</div>
);
}
// 在组件中触发
import { notFound } from 'next/navigation';
async function UserPage({ params }) {
const user = await getUser(params.id);
if (!user) {
notFound(); // 显示 not-found.tsx
}
return <UserProfile user={user} />;
}6. Middleware
6.1 基本用法
tsx
// middleware.ts (项目根目录)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// 检查认证
const token = request.cookies.get('token');
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
// 添加请求头
const response = NextResponse.next();
response.headers.set('x-custom-header', 'value');
return response;
}
// 匹配的路由
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*'],
};6.2 常见用途
tsx
// 1. 认证检查
if (!isAuthenticated) {
return NextResponse.redirect(new URL('/login', request.url));
}
// 2. 地区重定向
const country = request.geo?.country || 'US';
if (country === 'CN') {
return NextResponse.redirect(new URL('/cn', request.url));
}
// 3. A/B 测试
const bucket = Math.random() < 0.5 ? 'control' : 'experiment';
const response = NextResponse.next();
response.cookies.set('ab-bucket', bucket);
// 4. 请求改写
return NextResponse.rewrite(new URL('/proxy-page', request.url));7. API Routes
7.1 Route Handlers
tsx
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
const users = await db.users.findAll();
return NextResponse.json(users);
}
export async function POST(request: NextRequest) {
const body = await request.json();
const user = await db.users.create(body);
return NextResponse.json(user, { status: 201 });
}7.2 动态路由参数
tsx
// app/api/users/[id]/route.ts
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const user = await db.users.findById(params.id);
if (!user) {
return NextResponse.json(
{ error: 'User not found' },
{ status: 404 }
);
}
return NextResponse.json(user);
}8. 面试高频问题
Q1: App Router 和 Pages Router 的区别?
| 特性 | Pages Router | App Router |
|---|---|---|
| 目录 | /pages | /app |
| 组件类型 | 默认 Client | 默认 Server |
| 布局 | 手动实现 | layout.tsx |
| 数据获取 | getServerSideProps | 直接 async/await |
| Loading | 手动处理 | loading.tsx |
| Error | 手动处理 | error.tsx |
Q2: Server Components 在 Next.js 中的优势?
- 自动代码分割:Client Components 自动懒加载
- 直接数据获取:无需 API 层
- 减少 Bundle:Server Components 不发送 JS
- 流式渲染:配合 Suspense 逐步显示
Q3: Next.js 的缓存策略有哪些?
- Request Memoization:同一渲染中相同请求自动去重
- Data Cache:fetch 结果缓存
- Full Route Cache:静态渲染的页面缓存
- Router Cache:客户端路由缓存
Q4: 什么时候用 Server Actions vs API Routes?
Server Actions:
- 表单提交
- 简单的数据变更
- 需要与 React 组件紧密集成
API Routes:
- 需要被外部调用
- Webhook 处理
- RESTful API
Q5: 如何在 Next.js 中处理认证?
- Middleware:统一检查,重定向未认证用户
- Server Components:直接读取 cookies,获取 session
- Client Components:使用 useSession 等 Hook
- API Routes:验证请求头中的 token