Skip to content

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/users

1.2 特殊文件约定

文件作用
page.tsx页面组件,使路由可访问
layout.tsx共享布局,保持状态
loading.tsx加载 UI(Suspense boundary)
error.tsx错误 UI(Error boundary)
not-found.tsx404 页面
route.tsAPI 路由处理
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
//         └── BlogPage

2. 数据获取

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. 路由与导航

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, /cart

5. 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 RouterApp Router
目录/pages/app
组件类型默认 Client默认 Server
布局手动实现layout.tsx
数据获取getServerSideProps直接 async/await
Loading手动处理loading.tsx
Error手动处理error.tsx

Q2: Server Components 在 Next.js 中的优势?

  1. 自动代码分割:Client Components 自动懒加载
  2. 直接数据获取:无需 API 层
  3. 减少 Bundle:Server Components 不发送 JS
  4. 流式渲染:配合 Suspense 逐步显示

Q3: Next.js 的缓存策略有哪些?

  1. Request Memoization:同一渲染中相同请求自动去重
  2. Data Cache:fetch 结果缓存
  3. Full Route Cache:静态渲染的页面缓存
  4. Router Cache:客户端路由缓存

Q4: 什么时候用 Server Actions vs API Routes?

Server Actions

  • 表单提交
  • 简单的数据变更
  • 需要与 React 组件紧密集成

API Routes

  • 需要被外部调用
  • Webhook 处理
  • RESTful API

Q5: 如何在 Next.js 中处理认证?

  1. Middleware:统一检查,重定向未认证用户
  2. Server Components:直接读取 cookies,获取 session
  3. Client Components:使用 useSession 等 Hook
  4. API Routes:验证请求头中的 token

前端面试知识库