路由系统基础

深入理解 Next.js 的文件系统路由、动态路由及路由组等核心概念。

📚 学习目标

学完这篇文章后,你将能够:

  • 理解 Next.js App Router 的路由原理
  • 掌握静态路由、动态路由、路由组的使用
  • 了解核心文件约定(page、layout、loading、error、not-found)
  • 学会在项目中实现不同的路由功能
  • 掌握路由的最佳实践

前置知识

在开始学习之前,建议先阅读:

你需要了解:

  • URL 的基本概念
  • 文件和目录的基本操作

1️⃣ Next.js App Router 概述

1.1 什么是 App Router?

Next.js 13+ 引入了全新的 App Router,使用 app/ 目录替代了之前的 pages/ 目录。

核心特点

  • 📁 文件系统路由(文件夹和文件自动生成路由)
  • 🎯 布局系统(Layouts)
  • ⚡ 流式渲染(Streaming)
  • 🔄 Server Components(默认)
  • 🔌 并行路由和拦截(高级功能)

1.2 App Router vs Pages Router

特性App Router (app/)Pages Router (pages/)
文件约定文件夹 + 文件文件
布局系统支持(Layouts)不支持
默认组件Server ComponentClient Component
数据获取async/awaitgetStaticProps/getServerSideProps
发布版本Next.js 13+早期版本

本项目使用 App Router:所有路由都在 app/ 目录下。


2️⃣ 文件系统路由基础

2.1 路由规则

Next.js 使用文件夹结构来定义路由:

文件/文件夹生成的 URL
app/page.tsx/
app/about/page.tsx/about
app/about/team/page.tsx/about/team
app/artifact/[id]/page.tsx/artifact/123 (动态)

2.2 核心文件约定

Next.js 使用特殊的文件名来定义不同的功能:

文件名作用
page.tsx页面文件(必需)
layout.tsx布局文件(共享 UI)
loading.tsx加载状态
error.tsx错误处理
not-found.tsx404 页面
route.tsAPI 路由

3️⃣ 静态路由

3.1 基本静态路由

静态路由是最简单的路由形式,路径固定不变。

文件结构

app/
├── page.tsx              → /
├── login/
│   └── page.tsx          → /login
└── deepresearch/
    └── page.tsx          → /deepresearch

示例 1:首页(app/page.tsx

查看本项目的首页:app/page.tsx

'use client';

import { useRef, useMemo, useState, useEffect } from 'react';
import { ProtectedRoute } from './components/ProtectedRoute';
import SessionSidebar from './components/SessionSidebar';
import { ChatHeader } from './components/ChatHeader';
import { MessageList } from './components/MessageList';
import { ChatInput, type ChatInputHandle } from './components/ChatInput';

/**
 * 聊天页面主组件
 */
export default function ChatPage() {
  // 组件逻辑...
  return (
    <div className="flex h-screen">
      <SessionSidebar />
      <main className="flex-1">
        <ChatHeader />
        <MessageList />
        <ChatInput />
      </main>
    </div>
  );
}

说明

  • 这是项目的根页面,访问 / 时渲染
  • 使用 'use client' 标记为 Client Component
  • 整合了多个子组件

示例 2:登录页(app/login/page.tsx

'use client';

import { useRouter } from 'next/navigation';

export default function LoginPage() {
  const router = useRouter();

  const handleLogin = () => {
    // 登录逻辑...
    router.push('/'); // 跳转到首页
  };

  return (
    <div>
      <h1>登录</h1>
      <button onClick={handleLogin}>登录</button>
    </div>
  );
}

3.2 嵌套静态路由

路由可以嵌套多层:

app/
├── page.tsx              → /
├── about/
│   ├── page.tsx          → /about
│   └── team/
│       └── page.tsx      → /about/team

4️⃣ 动态路由

4.1 什么是动态路由?

动态路由允许你创建带有参数的路由,例如 /artifact/123/user/profile 等。

语法:使用方括号 [param] 标记动态参数。

4.2 基本动态路由

文件结构

app/
└── artifact/
    └── [id]/
        └── page.tsx      → /artifact/123

示例:Canvas 工件详情页(app/artifact/[id]/page.tsx

'use client';

import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';

export default function ArtifactPage() {
  // 获取路由参数
  const params = useParams();
  const router = useRouter();
  const artifactId = params.id as string;

  const [artifact, setArtifact] = useState(null);

  useEffect(() => {
    // 根据 ID 加载工件
    fetch(`/api/artifacts/${artifactId}`)
      .then(res => res.json())
      .then(data => setArtifact(data))
      .catch(() => router.push('/artifact/not-found'));
  }, [artifactId, router]);

  if (!artifact) {
    return <div>加载中...</div>;
  }

  return (
    <div>
      <h1>工件 #{artifactId}</h1>
      <pre>{artifact.content}</pre>
    </div>
  );
}

说明

  • useParams() Hook 获取路由参数
  • params.id 对应 [id] 动态参数
  • 可以根据参数加载数据

4.3 动态路由在 API 中的应用

API 路由也支持动态参数

app/api/
└── artifacts/
    └── [id]/
        └── route.ts      → GET /api/artifacts/123

查看本项目的实现:app/api/artifacts/[id]/route.ts

import { NextRequest, NextResponse } from 'next/server';
import { withAuth } from '@/app/middleware/auth';
import { artifactService } from '@/app/services';

/**
 * GET /api/artifacts/[id]
 * 获取指定 ID 的工件
 */
export const GET = withAuth(async (request: NextRequest, auth, { params }) => {
  try {
    const id = params.id;
    const artifact = await artifactService.getArtifactById(auth.client, id);

    if (!artifact) {
      return NextResponse.json({ error: '工件不存在' }, { status: 404 });
    }

    return NextResponse.json({ artifact });
  } catch (e) {
    return NextResponse.json(
      { error: '获取工件失败', detail: String(e) },
      { status: 500 },
    );
  }
});

/**
 * DELETE /api/artifacts/[id]
 * 删除指定 ID 的工件
 */
export const DELETE = withAuth(
  async (request: NextRequest, auth, { params }) => {
    try {
      const id = params.id;
      await artifactService.deleteArtifact(auth.client, id);
      return NextResponse.json({ success: true });
    } catch (e) {
      return NextResponse.json(
        { error: '删除工件失败', detail: String(e) },
        { status: 500 },
      );
    }
  },
);

说明

  • API Route 的 params 参数包含动态路由参数
  • params.id 对应 [id] 动态段

5️⃣ Layout(布局)

5.1 什么是 Layout?

Layout 允许你在多个页面之间共享 UI,例如导航栏、侧边栏等。

特点

  • 布局会在路由切换时保持状态
  • 支持嵌套布局
  • 不会重新渲染(性能优化)

5.2 本项目的全局布局

查看实现:app/layout.tsx

import type { Metadata } from "next";
import "./globals.css";
import { AuthProvider } from "./contexts/AuthContext";

export const metadata: Metadata = {
  title: "LangGraph Chat App",
  description: "Chat application powered by LangGraph",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="zh-CN">
      <body className="bg-[#050509] text-slate-200 antialiased selection:bg-blue-500/30 overflow-hidden">
        <AuthProvider>
          {children}
        </AuthProvider>
      </body>
    </html>
  );
}

代码解析

  • metadata 导出:设置页面的 SEO 信息
  • AuthProvider:包裹所有子组件,提供全局认证状态
  • {children}:占位符,所有子页面内容会渲染在这里

5.3 嵌套布局

你可以在子路由中定义自己的布局:

app/
├── layout.tsx              # 根布局
└── dashboard/
    ├── layout.tsx          # Dashboard 子布局
    ├── page.tsx            # /dashboard
    └── settings/
        └── page.tsx        # /dashboard/settings
// app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div>
      <nav>Dashboard 导航栏</nav>
      {children}
    </div>
  );
}

6️⃣ 特殊路由文件

6.1 loading.tsx - 加载状态

定义路由加载时的 UI。

// app/chat/loading.tsx
export default function Loading() {
  return <div>加载中...</div>;
}

本项目未使用:因为使用了客户端加载状态。

6.2 error.tsx - 错误处理

定义路由出错的 UI。

'use client';

export default function Error({
  error,
  reset,
}: {
  error: Error;
  reset: () => void;
}) {
  return (
    <div>
      <h2>出错了!</h2>
      <p>{error.message}</p>
      <button onClick={reset}>重试</button>
    </div>
  );
}

6.3 not-found.tsx - 404 页面

定义资源未找到时的 UI。

查看本项目的实现app/artifact/[id]/ArtifactNotFound.tsx

import Link from 'next/link';
import { ArrowLeft, FileQuestion } from 'lucide-react';

export default function ArtifactNotFound() {
  return (
    <div className="flex flex-col items-center justify-center h-screen">
      <FileQuestion className="w-24 h-24 text-gray-500 mb-4" />
      <h1 className="text-2xl font-bold mb-2">工件不存在</h1>
      <p className="text-gray-400 mb-4">
        该工件可能已被删除或您没有访问权限
      </p>
      <Link href="/" className="flex items-center text-blue-400 hover:underline">
        <ArrowLeft className="w-4 h-4 mr-2" />
        返回首页
      </Link>
    </div>
  );
}

使用示例

// app/artifact/[id]/page.tsx
'use client';

import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import ArtifactNotFound from './ArtifactNotFound';

export default function ArtifactPage() {
  const params = useParams();
  const [artifact, setArtifact] = useState(null);
  const [notFound, setNotFound] = useState(false);

  useEffect(() => {
    fetch(`/api/artifacts/${params.id}`)
      .then(res => {
        if (!res.ok) {
          setNotFound(true);
          return null;
        }
        return res.json();
      })
      .then(data => {
        if (data) setArtifact(data.artifact);
      });
  }, [params.id]);

  if (notFound) {
    return <ArtifactNotFound />;
  }

  if (!artifact) {
    return <div>加载中...</div>;
  }

  return <div>{artifact.content}</div>;
}

7️⃣ 路由组(Route Groups)

7.1 什么是路由组?

路由组使用圆括号 (group) 命名,用于组织代码而不影响 URL 路径。

语法

app/
├── (marketing)/
│   ├── about/
│   │   └── page.tsx      → /about
│   └── contact/
│       └── page.tsx      → /contact
└── (app)/
    └── dashboard/
        └── page.tsx      → /dashboard

说明

  • (marketing)(app) 不会出现在 URL 中
  • 用于组织相关页面

7.2 本项目的应用

本项目使用路由组来组织不同类型的页面:

app/
├── (app)/
│   ├── page.tsx          → /
│   ├── login/
│   │   └── page.tsx      → /login
│   └── deepresearch/
│       └── page.tsx      → /deepresearch
└── (artifact)/
    └── artifact/
        └── [id]/
            └── page.tsx  → /artifact/123

8️⃣ 路由导航

使用 Link 组件进行客户端导航(推荐):

import Link from 'next/link';

export default function Navigation() {
  return (
    <nav>
      <Link href="/">首页</Link>
      <Link href="/about">关于</Link>
      <Link href="/artifact/123">工件详情</Link>
    </nav>
  );
}

优点

  • 客户端导航(无页面刷新)
  • 自动预加载(鼠标悬停时)
  • 更好的性能

8.2 useRouter Hook

使用 useRouter Hook 进行编程式导航:

'use client';

import { useRouter } from 'next/navigation';

export default function LoginButton() {
  const router = useRouter();

  const handleLogin = () => {
    // 登录成功后跳转
    router.push('/');

    // 替换当前页面(不添加历史记录)
    router.replace('/dashboard');

    // 返回上一页
    router.back();
  };

  return <button onClick={handleLogin}>登录</button>;
}

方法说明

  • router.push(path) - 导航到新页面
  • router.replace(path) - 替换当前页面
  • router.back() - 返回上一页
  • router.forward() - 前进

8.3 本项目中的应用

查看登录页:app/login/page.tsx

'use client';

import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useAuth } from '@/app/contexts/AuthContext';

export default function LoginPage() {
  const router = useRouter();
  const { user } = useAuth();

  // 如果已登录,跳转到首页
  useEffect(() => {
    if (user) {
      router.push('/');
    }
  }, [user, router]);

  // ... 登录逻辑
}

9️⃣ 路由参数与查询参数

9.1 获取路由参数

使用 useParams Hook 获取动态路由参数:

'use client';

import { useParams } from 'next/navigation';

export default function UserPage() {
  const params = useParams();
  const userId = params.id; // 对应 [id]

  return <div>用户 ID: {userId}</div>;
}

9.2 获取查询参数

使用 useSearchParams Hook 获取 URL 查询参数:

'use client';

import { useSearchParams } from 'next/navigation';

export default function SearchPage() {
  const searchParams = useSearchParams();
  const query = searchParams.get('q'); // ?q=hello
  const page = searchParams.get('page'); // ?page=1

  return (
    <div>
      <p>搜索词: {query}</p>
      <p>页码: {page}</p>
    </div>
  );
}

9.3 本项目中的应用

查看会话列表 API:app/api/chat/sessions/route.ts

export const GET = withAuth(async (request: NextRequest, auth) => {
  const { searchParams } = new URL(request.url);
  const type = searchParams.get('type') as SessionType | undefined;

  // 根据 type 参数过滤会话
  const sessions = await sessionService.getAllSessions(auth.client, type);
  return NextResponse.json({ sessions });
});

使用示例

  • GET /api/chat/sessions - 获取所有会话
  • GET /api/chat/sessions?type=chat - 只获取聊天会话
  • GET /api/chat/sessions?type=deepresearch - 只获取深度研究会话

🔟 实战案例:完整路由实现

10.1 项目路由总览

10.2 核心路由代码

聊天主页(app/page.tsx

'use client';

export default function ChatPage() {
  return (
    <div className="flex h-screen">
      <SessionSidebar />
      <main className="flex-1">
        <ChatHeader />
        <MessageList />
        <ChatInput />
      </main>
    </div>
  );
}

会话管理 API(app/api/chat/sessions/route.ts

import { NextRequest, NextResponse } from 'next/server';
import { withAuth } from '@/app/middleware/auth';
import { sessionService } from '@/app/services';

// GET /api/chat/sessions - 获取会话列表
export const GET = withAuth(async (request: NextRequest, auth) => {
  const sessions = await sessionService.getAllSessions(auth.client);
  return NextResponse.json({ sessions });
});

// POST /api/chat/sessions - 创建新会话
export const POST = withAuth(async (request: NextRequest, auth) => {
  const { name } = await request.json();
  const result = await sessionService.createSession(
    { name },
    auth.user.id,
    auth.client,
  );
  return NextResponse.json(result);
});

// DELETE /api/chat/sessions - 删除会话
export const DELETE = withAuth(async (request: NextRequest, auth) => {
  const { id } = await request.json();
  await sessionService.deleteSession({ id });
  return NextResponse.json({ success: true });
});

// PATCH /api/chat/sessions - 更新会话名称
export const PATCH = withAuth(async (request: NextRequest, auth) => {
  const { id, name } = await request.json();
  await sessionService.updateSessionName({ id, name });
  return NextResponse.json({ success: true });
});

💡 练习题

  1. 选择题:以下文件会生成什么 URL?

    app/
    ├── page.tsx
    ├── product/
    │   └── [id]/
    │       └── page.tsx
    └── api/
        └── users/
            └── route.ts
    • A. /, /product/:id, /api/users
    • B. /, /product/[id], /api/users
    • C. /home, /product/:id, /api/users
    • D. /, /product/:id, /api/users/route
  2. 代码题:创建一个动态路由 /blog/[slug],显示博客文章详情。

  3. 分析题:查看本项目的 app/artifact/[id]/page.tsx,说明它如何获取和使用路由参数。

  4. 实践题:创建一个新的页面 /settings,并添加导航链接。


📚 参考资源

官方文档

本项目相关文件


✅ 总结

路由规则

  • 文件系统路由:文件夹和文件自动生成 URL
  • page.tsx → 页面
  • layout.tsx → 布局
  • route.ts → API

路由类型

  • 静态路由:app/page.tsx/
  • 动态路由:app/[id]/page.tsx/123
  • 路由组:app/(group)/page.tsx/

导航方式

  • Link 组件:客户端导航(推荐)
  • useRouter Hook:编程式导航

本项目核心路由

  • / - 聊天主页
  • /login - 登录页
  • /deepresearch - 深度研究页
  • /artifact/[id] - 工件详情页
  • /api/chat/* - 聊天相关 API
  • /api/artifacts/* - 工件管理 API

下一步:阅读下一篇文章《API Routes 完全指南》,学习如何创建和使用 Next.js API。

登录以继续阅读

解锁完整文档、代码示例及更多高级功能。

立即登录

On this page