关于 nextjs-app-router-fundamentals
nextjs-app-router-fundamentals 面向使用 App Router 构建或迁移 Next.js 13+ 应用的开发者。它讲解路由约定、布局、元数据、数据获取等模式,并提供避免使用 `any` 的 TypeScript 指引。
主要功能
- 01App Router 文件与路由约定
- 02Pages Router 迁移指南
- 03布局与嵌套路由
- 04静态和动态元数据
- 05服务端与客户端组件模式
- 06数据获取与静态路由生成
适用场景
- 01启动 Next.js 13+ App Router 项目
- 02从 Pages Router 迁移路由
- 03实现嵌套和动态路由
- 04构建元数据与静态页面
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ wsimmonds/ claude-nextjs-skills --skill nextjs-app-router-fundamentals安装后在 Claude Code 中输入/nextjs-app-router-fundamentals确认已加载。
详细介绍
Next.js App Router 基础
概述
全面介绍 Next.js App Router(Next.js 13+),涵盖从 Pages Router 迁移、基于文件的路由约定、布局、元数据处理以及现代 Next.js 模式。
TypeScript:绝不使用 any 类型
重要规则: 此代码库启用了 @typescript-eslint/no-explicit-any。使用 any 会导致构建失败。
❌ 错误:
function handleSubmit(e: any) { ... }
const data: any[] = [];
✅ 正确:
function handleSubmit(e: React.FormEvent<HTMLFormElement>) { ... }
const data: string[] = [];
常见的 Next.js 类型模式
// Page props
function Page({ params }: { params: { slug: string } }) { ... }
function Page({ searchParams }: { searchParams: { [key: string]: string | string[] | undefined } }) { ... }
// Form events
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { ... }
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { ... }
// Server actions
async function myAction(formData: FormData) { ... }
何时使用此技能
在以下情况下使用此技能:
- 从 Pages Router(
pages/目录)迁移到 App Router(app/目录) - 从头开始创建 Next.js 13+ 应用
- 使用布局、模板和嵌套路由
- 实现元数据和 SEO 优化
- 使用 App Router 路由约定构建应用
- 处理路由组、并行路由或拦截路由基础知识
核心概念
App Router 与 Pages Router
Pages Router(旧版 - Next.js 12 及更早版本):
pages/
├── index.tsx # Route: /
├── about.tsx # Route: /about
├── _app.tsx # Custom App component
├── _document.tsx # Custom Document component
└── api/ # API routes
└── hello.ts # API endpoint: /api/hello
App Router(现代 - Next.js 13+):
app/
├── layout.tsx # Root layout (required)
├── page.tsx # Route: /
├── about/ # Route: /about
│ └── page.tsx
├── blog/
│ ├── layout.tsx # Nested layout
│ └── [slug]/
│ └── page.tsx # Dynamic route: /blog/:slug
└── api/ # Route handlers
└── hello/
└── route.ts # API endpoint: /api/hello
文件约定
App Router 中的特殊文件:
layout.tsx- 某个片段及其子级共用的 UI(保留状态,不会重新渲染)page.tsx- 路由的专属 UI,使该路由可公开访问loading.tsx- 使用 React Suspense 的加载 UIerror.tsx- 使用错误边界的错误 UInot-found.tsx- 404 UItemplate.tsx- 类似于布局,但在导航时会重新渲染route.ts- API 端点(路由处理程序)
共置:
- 可以在
app/中共置组件、测试和其他文件 - 只有
page.tsx和route.ts文件会创建公开路由 - 其他文件(组件、工具、测试)不能作为路由访问
迁移指南:从 Pages Router 迁移到 App Router
步骤 1:了解当前结构
检查现有的 Pages Router 设置:
- 查看
pages/目录结构 - 找出
_app.tsx——用于处理全局状态、布局和提供程序 - 找出
_document.tsx——用于自定义 HTML 结构 - 记录元数据的用法(
next/head、<Head>组件) - 列出所有路由和动态片段
步骤 2:创建根布局
创建 app/layout.tsx——所有 App Router 应用都必须包含此文件:
// app/layout.tsx
export const metadata = {
title: 'My App',
description: 'App description',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
迁移说明:
- 将
_document.tsx中的 HTML 结构移至layout.tsx - 将
_app.tsx中的全局提供程序和包装器移至layout.tsx - 将
<Head>元数据转换为metadata导出 - 根布局必须包含
<html>和<body>标签
步骤 3:将页面迁移为路由
简单页面迁移:
// Before: pages/index.tsx
import Head from 'next/head';
export default function Home() {
return (
<>
<Head>
<title>Home Page</title>
</Head>
<main>
<h1>Welcome</h1>
</main>
</>
);
}
// After: app/page.tsx
export default function Home() {
return (
<main>
<h1>Welcome</h1>
</main>
);
}
// Metadata moved to layout.tsx or exported here
export const metadata = {
title: 'Home Page',
};
嵌套路由迁移:
// Before: pages/blog/[slug].tsx
export default function BlogPost() { ... }
// After: app/blog/[slug]/page.tsx
export default function BlogPost() { ... }
步骤 4:更新导航
将锚点标签替换为 Next.js Link:
// Before (incorrect in App Router)
<a href="/about">About</a>
// After (correct)
import Link from 'next/link';
<Link href="/about">About</Link>
步骤 5:清理 Pages 目录
迁移完成后:
- 从
pages/目录中删除所有页面文件 - 如果尚未迁移 API 路由,则保留
pages/api/ - 删除
_app.tsx和_document.tsx(其功能已移至布局) - 可以选择删除空的
pages/目录
元数据处理
静态元数据
// app/page.tsx or app/layout.tsx
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'My Page',
description: 'Page description',
keywords: ['nextjs', 'react'],
openGraph: {
title: 'My Page',
description: 'Page description',
images: ['/og-image.jpg'],
},
};
动态元数据
// app/blog/[slug]/page.tsx
export async function generateMetadata({
params
}: {
params: { slug: string }
}): Promise<Metadata> {
const post = await getPost(params.slug);
return {
title: post.title,
description: post.excerpt,
};
}
布局与嵌套
创建嵌套布局
// app/layout.tsx - Root layout
export default function RootLayout({ children }) {
return (
<html>
<body>
<Header />
{children}
<Footer />
</body>
</html>
);
}
// app/blog/layout.tsx - Blog layout
export default function BlogLayout({ children }) {
return (
<div>
<BlogSidebar />
<main>{children}</main>
</div>
);
}
布局行为:
- 布局会在导航过程中保留状态
- 路由更改时布局不会重新渲染
- 父布局会包裹子布局
- 根布局为必需项,会包裹整个应用
路由模式
动态路由
// app/blog/[slug]/page.tsx
export default function BlogPost({
params
}: {
params: { slug: string }
}) {
return <article>Post: {params.slug}</article>;
}
全匹配路由
// app/shop/[...slug]/page.tsx - Matches /shop/a, /shop/a/b, etc.
export default function Shop({
params
}: {
params: { slug: string[] }
}) {
return <div>Path: {params.slug.join('/')}</div>;
}
可选全匹配路由
// app/shop/[[...slug]]/page.tsx - Matches /shop AND /shop/a, /shop/a/b
路由组
对路由进行分组,但不影响 URL:
app/
├── (marketing)/
│ ├── about/
│ │ └── page.tsx # /about
│ └── contact/
│ └── page.tsx # /contact
└── (shop)/
└── products/
└── page.tsx # /products
常见迁移问题
问题 1:忘记根布局的 HTML 标签
错误:
export default function RootLayout({ children }) {
return <div>{children}</div>; // Missing <html> and <body>
}
正确:
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
问题 2:在 App Router 中使用 next/head
错误:
import Head from 'next/head';
export default function Page() {
return (
<>
<Head><title>Title</title></Head>
<main>Content</main>
</>
);
}
正确:
export const metadata = { title: 'Title' };
export default function Page() {
return <main>Content</main>;
}
问题 3:未删除 Pages 目录
迁移路由后,删除旧的 pages/ 目录文件,以免造成混淆。如果存在冲突的路由,构建会失败。
陷阱 4:缺少 page.tsx 文件
没有 page.tsx 文件就无法访问路由。仅有布局不会创建路由。
app/
├── blog/
│ ├── layout.tsx # NOT a route
│ └── page.tsx # This makes /blog accessible
陷阱 5:链接用法错误
错误:
<a href="/about">About</a> // Works but causes full page reload
正确:
import Link from 'next/link';
<Link href="/about">About</Link> // Client-side navigation
服务端组件与客户端组件
默认:服务端组件
app/ 中的所有组件默认都是服务端组件:
// app/page.tsx - Server Component (default)
export default async function Page() {
const data = await fetch('https://api.example.com/data');
const json = await data.json();
return <div>{json.title}</div>;
}
优点:
- 可直接使用 async/await
- 可直接访问数据库/API
- 客户端 JavaScript 为零
- 自动代码拆分
客户端组件
需要以下功能时,使用 'use client' 指令:
- 交互元素(onClick、onChange 等)
- React Hooks(useState、useEffect、useContext 等)
- 浏览器 API(window、localStorage 等)
- 事件监听器
// app/components/Counter.tsx
'use client';
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
数据获取模式
服务端组件数据获取
// app/posts/page.tsx
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 } // Revalidate every hour
});
return res.json();
}
export default async function PostsPage() {
const posts = await getPosts();
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
并行数据获取
export default async function Page() {
// Fetch in parallel
const [posts, users] = await Promise.all([
fetch('https://api.example.com/posts').then(r => r.json()),
fetch('https://api.example.com/users').then(r => r.json()),
]);
return (/* render */);
}
使用 generateStaticParams 生成静态站点
概述
generateStaticParams 是 App Router 中与 Pages Router 的 getStaticPaths 等效的功能。它会在构建时为动态路由生成静态页面。
基本用法
// app/blog/[id]/page.tsx
export async function generateStaticParams() {
// Return array of params to pre-render
return [
{ id: '1' },
{ id: '2' },
{ id: '3' },
];
}
export default function BlogPost({
params
}: {
params: { id: string }
}) {
return <article>Blog post {params.id}</article>;
}
要点:
- 返回一个对象数组,其中包含路由参数键
- 每个对象代表一个要在构建时预渲染的页面
- 函数必须导出,且名称为
generateStaticParams - 仅适用于服务端组件(不能使用
'use client'指令) - 替代 Pages Router 的
getStaticPaths
获取静态参数的数据
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
return posts.map((post: { slug: string }) => ({
slug: post.slug,
}));
}
export default async function BlogPost({
params
}: {
params: { slug: string }
}) {
const post = await fetch(`https://api.example.com/posts/${params.slug}`).then(r => r.json());
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}
多个动态片段
// app/products/[category]/[id]/page.tsx
export async function generateStaticParams() {
const categories = await getCategories();
const params = [];
for (const category of categories) {
const products = await getProducts(category.slug);
for (const product of products) {
params.push({
category: category.slug,
id: product.id,
});
}
}
return params;
}
export default function ProductPage({
params
}: {
params: { category: string; id: string }
}) {
return <div>Category: {params.category}, Product: {params.id}</div>;
}
动态行为配置
// app/blog/[id]/page.tsx
export async function generateStaticParams() {
return [{ id: '1' }, { id: '2' }];
}
// Control behavior for non-pre-rendered paths
export const dynamicParams = true; // default - allows runtime generation
// export const dynamicParams = false; // returns 404 for non-pre-rendered paths
export default function BlogPost({
params
}: {
params: { id: string }
}) {
return <article>Post {params.id}</article>;
}
选项:
dynamicParams = true(默认):未预渲染的路径会按需生成dynamicParams = false:未预渲染的路径会返回 404
常见模式
模式 1:基于 ID 的简单路由
export async function generateStaticParams() {
return [
{ id: '1' },
{ id: '2' },
{ id: '3' },
];
}
模式 2:从 API 获取
export async function generateStaticParams() {
const items = await fetch('https://api.example.com/items').then(r => r.json());
return items.map(item => ({ id: item.id }));
}
模式 3:数据库查询
export async function generateStaticParams() {
const posts = await db.post.findMany();
return posts.map(post => ({ slug: post.slug }));
}
从 Pages Router 迁移
之前(Pages Router):
// pages/blog/[id].tsx
export async function getStaticPaths() {
return {
paths: [
{ params: { id: '1' } },
{ params: { id: '2' } },
],
fallback: false,
};
}
export async function getStaticProps({ params }) {
return { props: { id: params.id } };
}
之后(App Router):
// app/blog/[id]/page.tsx
export async function generateStaticParams() {
return [
{ id: '1' },
{ id: '2' },
];
}
export const dynamicParams = false; // equivalent to fallback: false
export default function BlogPost({ params }: { params: { id: string } }) {
return <div>Post {params.id}</div>;
}
应避免的常见错误
❌ 错误:使用 'use client'
'use client'; // ERROR! generateStaticParams only works in Server Components
export async function generateStaticParams() {
return [{ id: '1' }];
}
❌ 错误:使用 Pages Router 模式
export async function getStaticPaths() { // Wrong API!
return { paths: [...], fallback: false };
}
❌ 错误:缺少 export 关键字
async function generateStaticParams() { // Must be exported!
return [{ id: '1' }];
}
✅ 正确:简洁的服务端组件
// app/blog/[id]/page.tsx
// No 'use client' directive
export async function generateStaticParams() {
return [{ id: '1' }, { id: '2' }];
}
export default function Page({ params }: { params: { id: string } }) {
return <div>Post {params.id}</div>;
}
关键实现说明:
当被要求“编写”或“实现” generateStaticParams 时:
- 务必使用 Edit 或 Write 工具修改实际文件
- 务必将函数添加到现有的 page.tsx 文件
- 务必删除所有有关 generateStaticParams 的 TODO 注释
- 不要只在 Markdown 中输出代码——应实际实现
- 不要在不写入文件的情况下展示代码
测试与验证
迁移到 App Router 或使用它构建时,请验证:
-
结构:
- 存在
app/目录 - 存在包含
<html>和<body>的根layout.tsx - 每个路由都有一个
page.tsx文件
- 存在
-
元数据:
- App Router 中没有
next/head导入 - 从页面或布局导出元数据
- 元数据使用
Metadata类型正确声明类型
- App Router 中没有
-
导航:
- 使用来自
next/link的Link组件 - 内部导航不使用普通的
<a>标签
- 使用来自
-
清理:
pages/目录中没有残留的页面文件- 已移除
_app.tsx和_document.tsx - 已移除旧的元数据模式
快速参考
文件结构映射
| Pages Router | App Router | 用途 |
|---|---|---|
pages/index.tsx | app/page.tsx | 首页路由 |
pages/about.tsx | app/about/page.tsx | 关于页面路由 |
pages/[id].tsx | app/[id]/page.tsx | 动态路由 |
pages/_app.tsx | app/layout.tsx | 全局布局 |
pages/_document.tsx | app/layout.tsx | HTML 结构 |
pages/api/hello.ts | app/api/hello/route.ts | API 路由 |
常用命令
# Create new Next.js app with App Router
npx create-next-app@latest my-app
# Run development server
npm run dev
# Build for production
npm run build
# Start production server
npm start
其他资源
有关更高级的路由模式(并行路由、拦截路由、路由处理器),请参阅 nextjs-advanced-routing 技能。
有关服务端与客户端组件的最佳实践和反模式,请参阅 nextjs-server-client-components 和 nextjs-anti-patterns 技能。