前端
2025年9月1日
16 分钟阅读
Next.js App Router 路由系统深度解析
Next.js App Router 路由系统深度解析
Next.js 的 App Router 完全重构了之前 Pages Router 的路由系统,基于 React Server Components 和文件系统路由。本文将从基础概念到高级特性,全面解析 App Router 的工作原理和最佳实践。
App Router 基础:文件系统路由
路由约定
App Router 使用 app 目录下的文件夹结构来定义路由:
app/
page.tsx → / (根路由)
about/
page.tsx → /about
blog/
page.tsx → /blog
[slug]/
page.tsx → /blog/:slug (动态路由)
每个路由目录必须包含一个 page.tsx 文件来定义该路由的 UI。
路由文件类型
App Router 支持多种特殊文件,每种都有特定用途:
// app/dashboard/
page.tsx; // 页面组件(必需)
layout.tsx; // 布局组件
loading.tsx; // 加载状态
error.tsx; // 错误边界
not - found.tsx; // 404 页面
template.tsx; // 模板组件(每次导航都重新挂载)Layout vs Template:
// layout.tsx - 导航时保持状态
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div>
<Sidebar /> {/* 导航时不会重新渲染 */}
{children}
</div>
);
}
// template.tsx - 导航时重新挂载
export default function DashboardTemplate({
children,
}: {
children: React.ReactNode;
}) {
return (
<div>
<Header /> {/* 每次导航都重新渲染 */}
{children}
</div>
);
}动态路由
单段动态路由
使用 [slug] 语法创建动态路由:
// app/blog/[slug]/page.tsx
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await getPost(slug);
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
);
}重要:在 Next.js 15+ 中,params 是 Promise,需要 await。
多段动态路由
使用 [...slug] 创建捕获所有路由:
// app/docs/[...slug]/page.tsx
export default async function DocsPage({
params,
}: {
params: Promise<{ slug: string[] }>;
}) {
const { slug } = await params;
// slug = ['api', 'routes'] 对应 /docs/api/routes
return <div>Documentation for {slug.join("/")}</div>;
}可选捕获路由
使用 [[...slug]] 创建可选捕获:
// app/shop/[[...slug]]/page.tsx
export default async function ShopPage({
params,
}: {
params: Promise<{ slug?: string[] }>;
}) {
const { slug } = await params;
if (!slug) {
// 显示所有产品
return <ProductList />;
}
// 显示特定类别
return <CategoryPage category={slug[0]} />;
}路由组(Route Groups)
路由组使用 (folderName) 语法,不会影响 URL 路径:
app/
(marketing)/
about/
page.tsx → /about
contact/
page.tsx → /contact
(dashboard)/
dashboard/
page.tsx → /dashboard
settings/
page.tsx → /settings
使用场景:
- 组织路由:按功能分组
- 条件布局:为不同组应用不同布局
// app/(marketing)/layout.tsx
export default function MarketingLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div>
<MarketingHeader />
{children}
<MarketingFooter />
</div>
);
}
// app/(dashboard)/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div>
<DashboardSidebar />
{children}
</div>
);
}并行路由(Parallel Routes)
使用 @folder 语法创建并行路由:
app/
dashboard/
@analytics/
page.tsx // 并行槽位
@team/
page.tsx // 并行槽位
layout.tsx
page.tsx
布局文件:
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
analytics,
team,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
team: React.ReactNode;
}) {
return (
<div className="grid grid-cols-3">
<div>{analytics}</div>
<div>{children}</div>
<div>{team}</div>
</div>
);
}默认文件:使用 default.tsx 处理未匹配的槽位:
// app/dashboard/@analytics/default.tsx
export default function DefaultAnalytics() {
return <div>Analytics placeholder</div>;
}拦截路由(Intercepting Routes)
使用 (.) 语法在同一路由级别拦截,(..) 向上拦截:
app/
feed/
page.tsx
@modal/
(.)photo/
[id]/
page.tsx // 拦截 /photo/[id]
photo/
[id]/
page.tsx // 原始路由
实现模态框:
// app/@modal/(.)photo/[id]/page.tsx
import Modal from "@/components/Modal";
import PhotoView from "@/app/photo/[id]/page";
export default function PhotoModal({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
return (
<Modal>
<PhotoView params={Promise.resolve({ id })} />
</Modal>
);
}
// app/photo/[id]/page.tsx
export default async function PhotoView({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const photo = await getPhoto(id);
return (
<div>
<img src={photo.url} alt={photo.title} />
</div>
);
}路由处理:Search Params
读取查询参数
// app/search/page.tsx
export default async function SearchPage({
searchParams,
}: {
searchParams: Promise<{ q?: string; page?: string }>;
}) {
const { q, page = "1" } = await searchParams;
const results = await searchArticles(q || "", parseInt(page));
return (
<div>
<h1>Search: {q}</h1>
<SearchResults results={results} />
</div>
);
}更新查询参数
在客户端组件中使用 useRouter 和 useSearchParams:
"use client";
import { useRouter, useSearchParams } from "next/navigation";
export function SearchFilters() {
const router = useRouter();
const searchParams = useSearchParams();
function updateFilter(key: string, value: string) {
const params = new URLSearchParams(searchParams);
params.set(key, value);
router.push(`?${params.toString()}`);
}
return (
<select onChange={(e) => updateFilter("category", e.target.value)}>
<option value="all">All</option>
<option value="tech">Tech</option>
</select>
);
}路由跳转与预取
使用 Link 组件
import Link from "next/link";
export function Navigation() {
return (
<nav>
{/* 客户端路由跳转,默认预取 */}
<Link href="/about">About</Link>
{/* 禁用预取 */}
<Link href="/dashboard" prefetch={false}>
Dashboard
</Link>
{/* 替换当前历史记录 */}
<Link href="/login" replace>
Login
</Link>
{/* 滚动到顶部 */}
<Link href="/top" scroll={true}>
Top
</Link>
</nav>
);
}编程式导航
"use client";
import { useRouter } from "next/navigation";
export function NavigationButton() {
const router = useRouter();
function handleClick() {
// 跳转到新路由
router.push("/dashboard");
// 替换当前路由
router.replace("/login");
// 返回上一页
router.back();
// 前进一页
router.forward();
// 刷新当前路由
router.refresh();
}
return <button onClick={handleClick}>Navigate</button>;
}预取策略
Next.js 在以下情况自动预取:
- Link 组件:当链接进入视口时
- 路由器预取:
router.prefetch('/route') - 静态生成:构建时预取所有静态路由
"use client";
import { useRouter } from "next/navigation";
export function PrefetchButton() {
const router = useRouter();
function handleMouseEnter() {
// 鼠标悬停时预取
router.prefetch("/dashboard");
}
return <button onMouseEnter={handleMouseEnter}>Hover to prefetch</button>;
}路由元数据
静态元数据
// app/about/page.tsx
export const metadata = {
title: "About Us",
description: "Learn more about our company",
};
export default function AboutPage() {
return <div>About Us</div>;
}动态元数据
// app/blog/[slug]/page.tsx
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await getPost(slug);
return {
title: post.title,
description: post.excerpt,
openGraph: {
images: [post.ogImage],
},
};
}路由段配置
// app/api/route.ts
export const dynamic = "force-dynamic"; // 强制动态渲染
export const revalidate = 3600; // 重新验证时间
export const dynamicParams = true; // 允许动态参数
export const runtime = "nodejs"; // 或 'edge'
export const preferredRegion = "us-east-1";实际应用:构建复杂路由结构
场景:电商网站路由
app/
(shop)/
layout.tsx // 商店布局
products/
page.tsx // 产品列表
[id]/
page.tsx // 产品详情
@modal/
(.)reviews/
page.tsx // 拦截评论路由(模态框)
(account)/
layout.tsx // 账户布局(需要认证)
orders/
page.tsx
[id]/
page.tsx
api/
checkout/
route.ts // API 路由
实现认证保护路由
// app/(account)/layout.tsx
import { redirect } from "next/navigation";
import { getSession } from "@/lib/auth";
export default async function AccountLayout({
children,
}: {
children: React.ReactNode;
}) {
const session = await getSession();
if (!session) {
redirect("/login");
}
return (
<div>
<AccountSidebar />
{children}
</div>
);
}最佳实践
- 合理使用路由组:组织相关路由,应用共享布局
- 避免过深嵌套:路由嵌套不要超过 3-4 层
- 使用动态路由处理相似页面:而不是创建多个静态路由
- 利用并行路由:实现复杂的仪表板布局
- 使用拦截路由:创建流畅的模态框体验
- 优化预取策略:对重要路由启用预取,对低优先级路由禁用
- 合理设置路由段配置:根据数据特性选择动态/静态渲染
理解 App Router 的路由系统是掌握 Next.js 13+ 的关键。通过合理运用这些特性,可以构建出既灵活又高性能的 Web 应用。