返回博客列表
前端
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

使用场景

  1. 组织路由:按功能分组
  2. 条件布局:为不同组应用不同布局
// 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>
  );
}

更新查询参数

在客户端组件中使用 useRouteruseSearchParams

"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 在以下情况自动预取:

  1. Link 组件:当链接进入视口时
  2. 路由器预取router.prefetch('/route')
  3. 静态生成:构建时预取所有静态路由
"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>
  );
}

最佳实践

  1. 合理使用路由组:组织相关路由,应用共享布局
  2. 避免过深嵌套:路由嵌套不要超过 3-4 层
  3. 使用动态路由处理相似页面:而不是创建多个静态路由
  4. 利用并行路由:实现复杂的仪表板布局
  5. 使用拦截路由:创建流畅的模态框体验
  6. 优化预取策略:对重要路由启用预取,对低优先级路由禁用
  7. 合理设置路由段配置:根据数据特性选择动态/静态渲染

理解 App Router 的路由系统是掌握 Next.js 13+ 的关键。通过合理运用这些特性,可以构建出既灵活又高性能的 Web 应用。