← 返回文章列表

Next.js App Router 全面指南:从 Pages 迁移到 App

全面介绍 Next.js 13+ App Router 的核心概念,包括 Server Components、Streaming、缓存策略等,以及从 Pages Router 迁移的注意事项。

2025年3月15日·4 分钟阅读
字号

Next.js App Router 是 React 生态的一次重要演进。它基于 React Server Components(RSC)构建,带来了更强大的数据获取模式和更细粒度的缓存控制。

核心概念:Server Components vs Client Components

默认情况下,App Router 中的所有组件都是 Server Components

// app/blog/page.tsx — Server Component(默认)
// 可以直接 await,无需 useEffect + useState
export default async function BlogPage() {
  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>
  );
}

需要使用浏览器 API、state 或事件处理时,添加 "use client" 指令:

"use client";
// 现在这是 Client Component
import { useState } from "react";
 
export default function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

文件约定(File Conventions)

App Router 用特定文件名约定来定义路由行为:

app/
  layout.tsx      # 共享布局,不会在导航时重新渲染
  page.tsx        # 路由页面,公开可访问
  loading.tsx     # 加载 UI(React Suspense 边界)
  error.tsx       # 错误 UI(React Error Boundary)
  not-found.tsx   # 404 页面
  template.tsx    # 类似 layout,但每次导航都重新挂载

数据获取最佳实践

并行获取

// ❌ 串行:总时间 = 请求1 + 请求2
async function Page() {
  const user = await getUser();
  const posts = await getPosts(user.id);
  // ...
}
 
// ✅ 并行:总时间 = max(请求1, 请求2)
async function Page() {
  const [user, posts] = await Promise.all([getUser(), getAllPosts()]);
  // ...
}

流式渲染(Streaming)

使用 Suspense 逐步呈现内容,不阻塞整个页面:

import { Suspense } from "react";
 
export default function Page() {
  return (
    <div>
      <h1>博客</h1>
      {/* 立即显示,不等待 */}
      <Suspense fallback={<PostSkeleton />}>
        <PostList /> {/* 这个可以慢慢加载 */}
      </Suspense>
    </div>
  );
}

缓存策略

这是 App Router 最复杂也最强大的部分:

// 默认缓存(构建时生成静态内容)
const data = await fetch('https://api.example.com/posts');
 
// 不缓存(每次请求都重新获取)
const data = await fetch('https://api.example.com/posts', {
  cache: 'no-store'
});
 
// 定时重新验证(每小时刷新一次)
const data = await fetch('https://api.example.com/posts', {
  next: { revalidate: 3600 }
});
 
// 按需重新验证
import { revalidatePath, revalidateTag } from 'next/cache';
 
export async function action() {
  // 触发后清除缓存
  revalidatePath('/blog');
  revalidateTag('posts');
}

Route Handlers(API Routes 替代)

// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server';
 
export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams;
  const tag = searchParams.get('tag');
 
  const posts = await getPostsByTag(tag);
  return NextResponse.json(posts);
}
 
export async function POST(request: NextRequest) {
  const body = await request.json();
  const post = await createPost(body);
  return NextResponse.json(post, { status: 201 });
}

Metadata API

静态和动态 SEO 配置变得非常简洁:

// 静态 metadata
export const metadata = {
  title: '我的博客',
  description: '技术文章分享',
  openGraph: {
    title: '我的博客',
    images: ['/og-image.png'],
  },
};
 
// 动态 metadata
export async function generateMetadata({ params }) {
  const post = await getPost(params.slug);
  return {
    title: post.title,
    description: post.description,
  };
}

从 Pages Router 迁移注意事项

Pages RouterApp Router
getServerSideProps直接在 async 组件中 await
getStaticPropsfetch + cache: 'force-cache'
getStaticPathsgenerateStaticParams
useRouter().pathnameusePathname()
_app.tsxapp/layout.tsx
API RoutesRoute Handlers

App Router 是 Next.js 的未来方向,虽然迁移有一定成本,但它带来的数据获取模式和性能提升是值得的。

分享

// RELATED_POSTS