01 App Router 简介
自 Next.js 13 起,框架引入 App Router:在 app/ 目录下用文件与文件夹描述 URL,并默认基于 React Server Components 组织页面与数据获取逻辑。
与 Pages Router(pages/)相比
- 路由入口由
page.tsx/layout.tsx等约定文件驱动,而不是单一的pages/*.tsx。 - 布局可嵌套、共享 UI 与加载状态;数据获取更自然地贴近组件树(含服务端组件)。
- 本章只聚焦 App Router;旧项目若仍用 Pages Router,可查阅官方迁移指南逐步切换。
要点
Next.js 15 默认以 App Router 为主流;新项目请优先使用 app/ 目录,与官方文档和生态示例保持一致。
02 文件系统路由基础
在 App Router 中,page.tsx(或 page.js)声明该路径下的页面 UI。目录层级即 URL 层级(在 app 根之下)。
常见映射(以 src/app 为例):
src/app/page.tsx→ 站点根路径/src/app/about/page.tsx→/aboutsrc/app/blog/page.tsx→/blog
最小可用的首页示例:
// src/app/page.tsx
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "首页",
description: "站点首页",
};
export default function HomePage() {
return (
<main className="min-h-screen p-8">
<h1 className="text-3xl font-bold">欢迎来到首页</h1>
<p className="mt-4 text-gray-600">这是由 app/page.tsx 渲染的路由 /</p>
</main>
);
}
要点
只有包含 page.tsx(或 page.js)的目录才会成为可访问的「路由段」;仅有文件夹而没有 page 时,该路径通常无法直接打开(可用于分组、布局或私有模块)。
03 布局 (Layout)
layout.tsx 包裹同级及以下路由,用于放置导航栏、侧边栏、主题容器等。在子路由之间切换时,布局默认保持挂载状态,因此适合放不需要随页面重置的全局 UI 或上下文(注意与「模板」的区别见下)。
根布局(必须包含 <html> 与 <body>):
// src/app/layout.tsx
import type { Metadata } from "next";
import "./globals.css";
export const metadata: Metadata = {
title: { default: "我的站点", template: "%s | 我的站点" },
description: "根布局提供全站 HTML 外壳",
};
export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="zh-CN">
<body className="antialiased">{children}</body>
</html>
);
}
嵌套布局示例(例如控制台子树共享侧栏):
// src/app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: { children: React.ReactNode }) {
return (
<div className="flex min-h-screen">
<aside className="w-56 border-r p-4">侧栏</aside>
<section className="flex-1 p-6">{children}</section>
</div>
);
}
// src/app/dashboard/page.tsx → URL: /dashboard
export default function DashboardHome() {
return <h1>控制台首页</h1>;
}
template.tsx 与 layout.tsx
两者都可包装子路由,但 template 在每次导航时都会重新挂载(内部 state 会重置、useEffect 会再跑),适合需要进场动画或强制刷新子树的场景;layout 在导航时保持挂载,适合持久 shell。
04 动态路由
文件夹名使用方括号表示动态段。Next.js 15 中,在服务端页面/布局里,params(以及 searchParams)为 Promise,需在 async 组件内 await。
单段动态 [slug]
// src/app/posts/[slug]/page.tsx
type Props = { params: Promise<{ slug: string }> };
export default async function PostPage({ params }: Props) {
const { slug } = await params;
return (
<article>
<h1>文章:{slug}</h1>
</article>
);
}
Catch-all [...slug]
// src/app/docs/[...slug]/page.tsx
type Props = { params: Promise<{ slug: string[] }> };
export default async function DocsPage({ params }: Props) {
const { slug } = await params; // 例如 /docs/a/b → ['a','b']
return <pre>{JSON.stringify(slug, null, 2)}</pre>;
}
Optional catch-all [[...slug]]
// src/app/wiki/[[...slug]]/page.tsx
type Props = { params: Promise<{ slug?: string[] }> };
export default async function WikiPage({ params }: Props) {
const { slug } = await params;
const segments = slug ?? [];
return <p>段落:{segments.join(" / ") || "(根)"}</p>;
}
三种模式对比
| 模式 | 目录示例 | URL 示例 | params(await 后) |
|---|---|---|---|
[slug] |
app/posts/[slug] |
/posts/hello |
{ slug: 'hello' } |
[...slug] |
app/docs/[...slug] |
/docs/api/auth |
{ slug: ['api','auth'] } |
[[...slug]] |
app/wiki/[[...slug]] |
/wiki 或 /wiki/a/b |
{ slug?: string[] },根路径可能为 undefined |
05 路由组与特殊文件
括号文件夹名 (segment) 为路由组:仅用于组织代码与共享布局,不会出现在 URL 中。例如 app/(marketing)/page.tsx 仍对应 /。
src/app/
(marketing)/
layout.tsx # 营销区布局
page.tsx # /
(app)/
dashboard/
page.tsx # /dashboard
loading.tsx
在对应路由段显示加载 UI,并由框架用 Suspense 包裹;适合列表、慢数据占位。
// src/app/dashboard/loading.tsx
export default function Loading() {
return <p className="animate-pulse">加载中…</p>;
}
error.tsx
错误边界,必须是客户端组件,接收 error 与 reset。
// src/app/dashboard/error.tsx
"use client";
export default function DashboardError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div>
<h2>出错了</h2>
<button type="button" onClick={() => reset()}>
重试
</button>
</div>
);
}
not-found.tsx
当调用 notFound() 或没有匹配路由时展示。
// src/app/not-found.tsx
import Link from "next/link";
export default function NotFound() {
return (
<div>
<h1>404</h1>
<p>页面不存在</p>
<Link href="/">返回首页</Link>
</div>
);
}
06 导航
App Router 中客户端导航请使用 next/link 与 next/navigation(不要使用旧版 next/router 中的 useRouter 写 App Router 页面逻辑)。
Link:声明式导航 + 预取
import Link from "next/link";
export default function Nav() {
return (
<nav className="flex gap-4">
<Link href="/about" prefetch>
关于
</Link>
<Link href="/posts/first" replace>
文章(替换历史记录)
</Link>
</nav>
);
}
useRouter:编程式导航(客户端组件)
"use client";
import { useRouter } from "next/navigation";
export function GoDashboardButton() {
const router = useRouter();
return (
<button type="button" onClick={() => router.push("/dashboard")}>
进入控制台
</button>
);
}
redirect:服务端 / Server Action 中重定向
import { redirect } from "next/navigation";
export default async function AdminPage() {
const role = "guest"; // 实际应从会话或数据库读取
if (role !== "admin") {
redirect("/login");
}
return <h1>管理后台</h1>;
}
usePathname 与 useSearchParams(客户端)
"use client";
import { usePathname, useSearchParams } from "next/navigation";
export function DebugRoute() {
const pathname = usePathname();
const searchParams = useSearchParams();
const tab = searchParams.get("tab");
return (
<p>
当前路径:{pathname};查询参数 tab:{tab ?? "无"}
</p>
);
}
提示:含 useSearchParams 的客户端子树在静态渲染场景下可能需要外层 <Suspense> 包裹以避免构建警告,详见官方文档「静态渲染与动态 API」。
07 本章要点
路由入口
用 page.tsx 定义可访问路径;目录结构映射 URL。
布局持久化
layout.tsx 嵌套包裹子路由;template.tsx 每次导航重新挂载。
动态段
[]、[...]、[[...]] 覆盖单段、多级与可选多级;Next.js 15 服务端中 await params。
体验与边界
loading / error / not-found 分工明确;路由组 () 组织代码不改 URL。
导航 API
Link、useRouter、redirect、usePathname、useSearchParams 覆盖声明式、命令式与地址解析。