Skip to content

Next.js App Router 实战指南:从基础到进阶的完整路由体系

Next.js 13.4 起,App Router 成为默认路由方案,提供了更强大的路由功能和更灵活的代码组织方式。本文将带你从零掌握 App Router 的核心概念、进阶模式和实战技巧。

目录

  1. App Router 基础:文件系统路由
  2. 路由导航:四种方式实现页面跳转
  3. 进阶路由模式:动态路由与路由组织
  4. 路由处理程序:构建 API 接口
  5. 中间件:全局请求拦截与控制
  6. 实战案例:构建完整的后台管理系统

1. App Router 基础:文件系统路由

1.1 从 Pages Router 到 App Router

Next.js 提供了两套路由方案:

  • Pages Router:旧的 pages 目录方案
  • App Router:新的 app 目录方案(推荐)

App Router 的优势

  • 功能更强、性能更好
  • 代码组织更灵活
  • 支持服务端组件、嵌套布局、加载状态、错误处理等

1.2 核心文件约定

App Router 通过特殊文件名定义不同功能:

src/
└── app/
    ├── page.js           # 页面(必需)
    ├── layout.js         # 布局(可选)
    ├── template.js       # 模板(可选)
    ├── loading.js        # 加载状态(可选)
    ├── error.js          # 错误处理(可选)
    ├── not-found.js      # 404 页面(可选)
    └── dashboard/
        └── page.js       # 嵌套路由

1.2.1 页面(page.js)

页面是路由的基本单元,映射到 URL:

javascript
// app/page.js → /
// app/dashboard/page.js → /dashboard
// app/dashboard/settings/page.js → /dashboard/settings

export default function Page() {
  return <h1>Hello, Next.js!</h1>
}

1.2.2 布局(layout.js)

布局是多个页面共享的 UI,导航时会保留状态:

javascript
// app/dashboard/layout.js
export default function DashboardLayout({ children }) {
  return (
    <section>
      <nav>Dashboard Navigation</nav>
      {children}
    </section>
  )
}

特性

  • 支持嵌套布局
  • 导航时不会重新渲染
  • 保持可交互性和状态

1.2.3 模板(template.js)

模板与布局类似,但不会维持状态:

javascript
// app/dashboard/template.js
export default function Template({ children }) {
  return <div>{children}</div>
}

使用场景

  • 依赖 useEffectuseState 的功能
  • 每次路由切换需要重新执行的场景

布局 vs 模板的区别

特性LayoutTemplate
状态保持✅ 是❌ 否
组件重新挂载❌ 否✅ 是
适用场景侧边导航、头部登录表单、统计页面

1.2.4 加载状态(loading.js)

基于 React Suspense 实现加载界面:

javascript
// app/dashboard/loading.js
export default function DashboardLoading() {
  return <>Loading dashboard...</>
}

// app/dashboard/page.js
async function getData() {
  await new Promise((resolve) => setTimeout(resolve, 3000))
  return { message: 'Hello, Dashboard!' }
}

export default async function DashboardPage() {
  const { message } = await getData()
  return <h1>{message}</h1>
}

触发条件

  • page.js 导出 async 函数
  • 使用 React 的 use 函数

1.2.5 错误处理(error.js)

基于 React Error Boundary 捕获错误:

javascript
'use client' // 必须是客户端组件
// app/dashboard/error.js
import { useEffect } from 'react'

export default function Error({ error, reset }) {
  useEffect(() => {
    console.error(error)
  }, [error])

  return (
    <div>
      <h2>Something went wrong!</h2>
      <button onClick={() => reset()}>Try again</button>
    </div>
  )
}

注意事项

  • error.js 不能捕获同级 layout.jstemplate.js 中的错误
  • 根布局错误使用 global-error.js

1.2.6 404 页面(not-found.js)

自定义 404 页面:

javascript
// app/not-found.js
import Link from 'next/link'

export default function NotFound() {
  return (
    <div>
      <h2>Not Found</h2>
      <p>Could not find requested resource</p>
      <Link href="/">Return Home</Link>
    </div>
  )
}

手动触发

javascript
import { notFound } from 'next/navigation'

export default async function Page({ params }) {
  const user = await fetchUser(params.id)
  if (!user) {
    notFound() // 触发最近的 not-found.js
  }
}

1.3 文件层级关系

Layout
├── Template
│   ├── Loading
│   ├── Error
│   └── Page

2. 路由导航:四种方式实现页面跳转

Next.js 内置组件,支持预获取和客户端路由:

javascript
import Link from 'next/link'

export default function Page() {
  return <Link href="/dashboard">Dashboard</Link>
}

动态渲染

javascript
export default function PostList({ posts }) {
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>
          <Link href={`/blog/${post.slug}`}>{post.title}</Link>
        </li>
      ))}
    </ul>
  )
}

激活状态判断

javascript
'use client'
import { usePathname } from 'next/navigation'
import Link from 'next/link'

export function Navigation({ navLinks }) {
  const pathname = usePathname()

  return (
    <>
      {navLinks.map((link) => {
        const isActive = pathname === link.href
        return (
          <Link
            className={isActive ? 'text-blue' : 'text-black'}
            href={link.href}
            key={link.name}
          >
            {link.name}
          </Link>
        )
      })}
    </>
  )
}

禁用滚动

javascript
<Link href="/dashboard" scroll={false}>
  Dashboard
</Link>

2.2 使用 useRouter Hook

客户端组件中使用:

javascript
'use client'
import { useRouter } from 'next/navigation'

export default function Page() {
  const router = useRouter()

  return (
    <button type="button" onClick={() => router.push('/dashboard')}>
      Dashboard
    </button>
  )
}

2.3 使用 redirect 函数

服务端组件中使用:

javascript
import { redirect } from 'next/navigation'

async function fetchTeam(id) {
  const res = await fetch('https://...')
  if (!res.ok) return undefined
  return res.json()
}

export default async function Profile({ params }) {
  const team = await fetchTeam(params.id)
  if (!team) {
    redirect('/login')
  }
  // ...
}

2.4 使用浏览器原生 History API

javascript
'use client'
import { useSearchParams } from 'next/navigation'

export default function SortProducts() {
  const searchParams = useSearchParams()

  function updateSorting(sortOrder) {
    const params = new URLSearchParams(searchParams.toString())
    params.set('sort', sortOrder)
    window.history.pushState(null, '', `?${params.toString()}`)
  }

  return (
    <>
      <button onClick={() => updateSorting('asc')}>Sort Ascending</button>
      <button onClick={() => updateSorting('desc')}>Sort Descending</button>
    </>
  )
}

replaceState 示例

javascript
'use client'
import { usePathname } from 'next/navigation'

export default function LocaleSwitcher() {
  const pathname = usePathname()

  function switchLocale(locale) {
    const newPath = `/${locale}${pathname}`
    window.history.replaceState(null, '', newPath)
  }

  return (
    <>
      <button onClick={() => switchLocale('en')}>English</button>
      <button onClick={() => switchLocale('fr')}>French</button>
    </>
  )
}

3. 进阶路由模式:动态路由与路由组织

3.1 动态路由

处理动态 URL 参数。

3.1.1 单个参数 [folderName]

javascript
// app/blog/[slug]/page.js
export default function Page({ params }) {
  return <div>My Post: {params.slug}</div>
}

访问 /blog/hello-worldparams = { slug: 'hello-world' }

3.1.2 捕获所有片段 [...folderName]

javascript
// app/shop/[...slug]/page.js
export default function Page({ params }) {
  return <div>My Shop: {JSON.stringify(params)}</div>
}

访问 /shop/clothes/topsparams = { slug: ['clothes', 'tops'] }

3.1.3 可选捕获所有 [[...folderName]]

javascript
// app/shop/[[...slug]]/page.js
export default function Page({ params }) {
  return <div>My Shop: {JSON.stringify(params)}</div>
}

访问 /shopparams = {} 访问 /shop/aparams = { slug: ['a'] }

3.2 路由组(Route Groups)

组织代码而不影响 URL。

3.2.1 按逻辑分组

app/
├── (marketing)/
│   ├── about/
│   │   └── page.js    → /about
│   └── blog/
│       └── page.js    → /blog
└── (shop)/
    ├── account/
    │   └── page.js    → /account
    └── cart/
        └── page.js    → /cart

3.2.2 创建不同布局

app/
├── (shop)/
│   ├── layout.js
│   ├── account/page.js
│   └── cart/page.js
├── checkout/
│   └── page.js
└── layout.js

/account/cart 使用 (shop)/layout.js/checkout 使用根 layout.js

3.2.3 创建多个根布局

app/
├── (shop)/
│   ├── layout.js      # 包含 <html> 和 <body>
│   └── page.js        → /
└── (marketing)/
    ├── layout.js      # 包含 <html> 和 <body>
    └── blog/page.js   → /blog

注意事项

  • 需要删除 app/layout.js
  • 每个路由组的 layout.js 都需要 <html><body>
  • 跨根布局导航会导致页面完全重新加载

3.3 平行路由(Parallel Routes)

在同一布局中同时或条件渲染多个页面。

3.3.1 基本用法

文件夹以 @ 开头命名:

app/
├── @team/
│   └── page.js
├── @analytics/
│   └── page.js
└── layout.js
javascript
// app/layout.js
export default function Layout({ children, team, analytics }) {
  return (
    <>
      {children}
      {team}
      {analytics}
    </>
  )
}

3.3.2 条件渲染

javascript
// app/layout.js
export default function Layout({ children, modal }) {
  const isLoggedIn = checkAuth()

  return (
    <>
      {children}
      {isLoggedIn ? <Dashboard /> : <Login />}
    </>
  )
}

3.3.3 独立路由处理

每个插槽可以有自己的加载状态和错误状态:

app/
├── @analytics/
│   ├── page.js
│   ├── loading.js
│   └── error.js
└── layout.js

3.3.4 子导航

app/
├── @analytics/
│   ├── page.js        → /
│   ├── page-views/
│   │   └── page.js    → /page-views
│   └── visitors/
│       └── page.js    → /visitors
└── layout.js

3.3.5 default.js

处理硬导航时插槽不匹配的情况:

javascript
// app/@team/default.js
export default function Default() {
  return <div>Team Default Content</div>
}

优势

  1. 代码更易于管理,适合团队协作
  2. 每个插槽独立的加载状态和错误处理
  3. 每个插槽可以有自己的导航和状态管理

3.4 拦截路由(Intercepting Routes)

在当前路由拦截其他路由地址。

3.4.1 实现方式

文件夹命名规则:

  • (.) - 匹配同一层级
  • (..) - 匹配上一层级
  • (..)(..) - 匹配上上层级
  • (...) - 匹配根目录

3.4.2 实战案例:图片详情 Modal

app/
├── @modal/
│   ├── default.js
│   └── (.)photo/
│       └── [id]/
│           └── page.js
├── photo/
│   └── [id]/
│       └── page.js
└── page.js
javascript
// app/page.js
import Link from "next/link"
import { photos } from "./data"

export default function Home() {
  return (
    <main>
      {photos.map(({ id, src }) => (
        <Link key={id} href={`/photo/${id}`}>
          <img src={src} alt={id} />
        </Link>
      ))}
    </main>
  )
}

// app/photo/[id]/page.js(独立访问)
export default function PhotoPage({ params: { id } }) {
  const photo = photos.find((p) => p.id === id)
  return <img src={photo.src} />
}

// app/@modal/(.)photo/[id]/page.js(拦截路由)
export default function PhotoModal({ params: { id } }) {
  const photo = photos.find((p) => p.id === id)
  return (
    <div className="modal">
      <img src={photo.src} />
    </div>
  )
}

效果

  • 从首页点击图片 → 以 Modal 展示
  • 直接访问 /photo/123 → 独立页面展示

4. 路由处理程序:构建 API 接口

4.1 定义路由处理程序

创建 route.js 文件(注意不是 router.js):

javascript
// app/api/posts/route.js
import { NextResponse } from 'next/server'

export async function GET() {
  const res = await fetch('https://jsonplaceholder.typicode.com/posts')
  const data = await res.json()
  return NextResponse.json({ data })
}

注意route.js 不能与同级的 page.js 同时存在。

4.2 支持的 HTTP 方法

javascript
export async function GET(request) {}
export async function HEAD(request) {}
export async function POST(request) {}
export async function PUT(request) {}
export async function DELETE(request) {}
export async function PATCH(request) {}
// OPTIONS 会自动实现
export async function OPTIONS(request) {}

4.3 获取参数

javascript
export async function GET(request, { params }) {
  // 获取 URL 参数
  const searchParams = request.nextUrl.searchParams
  const query = searchParams.get('query')

  // 获取动态路由参数
  const id = params.id
}

动态路由参数示例

路由文件URLparams
app/dashboard/[team]/route.js/dashboard/1{ team: '1' }
app/shop/[tag]/[item]/route.js/shop/1/2{ tag: '1', item: '2' }
app/blog/[...slug]/route.js/blog/1/2{ slug: ['1', '2'] }

4.4 缓存行为

4.4.1 默认缓存

GET 请求默认会被缓存:

javascript
// app/api/time/route.js
export async function GET() {
  return Response.json({ data: new Date().toLocaleTimeString() })
}

退出缓存的条件

  1. 使用 Request 对象
  2. 添加其他 HTTP 方法(如 POST)
  3. 使用动态函数(cookies、headers)
  4. 设置 export const dynamic = 'force-dynamic'

4.4.2 重新验证

设置缓存时效:

javascript
// 方式 1:路由段配置项
export const revalidate = 10

export async function GET() {
  return Response.json({ data: new Date().toLocaleTimeString() })
}

// 方式 2:fetch 选项
export async function GET() {
  const res = await fetch('https://api.example.com/data', {
    next: { revalidate: 5 }
  })
  return Response.json(await res.json())
}

4.5 常见操作

4.5.1 获取请求体

javascript
export async function POST(request) {
  const body = await request.json()
  // 或
  const formData = await request.formData()
  return NextResponse.json(body)
}
javascript
import { cookies } from 'next/headers'

export async function GET(request) {
  const cookieStore = cookies()
  const token = cookieStore.get('token')

  return new Response('Hello', {
    headers: { 'Set-Cookie': `token=${token}` }
  })
}

4.5.3 处理 Headers

javascript
import { headers } from 'next/headers'

export async function GET(request) {
  const headersList = headers()
  const referer = headersList.get('referer')

  return new Response('Hello', {
    headers: { referer: referer }
  })
}

4.5.4 设置 CORS

javascript
export async function GET(request) {
  return new Response('Hello', {
    status: 200,
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
      'Access-Control-Allow-Headers': 'Content-Type, Authorization',
    }
  })
}

4.5.5 流式响应

javascript
async function* makeIterator() {
  yield '<p>One</p>'
  await new Promise(resolve => setTimeout(resolve, 200))
  yield '<p>Two</p>'
  await new Promise(resolve => setTimeout(resolve, 200))
  yield '<p>Three</p>'
}

export async function GET() {
  const iterator = makeIterator()
  const stream = new ReadableStream({
    async pull(controller) {
      const { value, done } = await iterator.next()
      if (done) {
        controller.close()
      } else {
        controller.enqueue(value)
      }
    },
  })

  return new Response(stream)
}

5. 中间件:全局请求拦截与控制

5.1 定义中间件

在项目根目录创建 middleware.js

javascript
// middleware.js
import { NextResponse } from 'next/server'

export function middleware(request) {
  return NextResponse.redirect(new URL('/home', request.url))
}

export const config = {
  matcher: '/about/:path*',
}

5.2 设置匹配路径

5.2.1 使用 matcher 配置

javascript
export const config = {
  matcher: ['/about/:path*', '/dashboard/:path*'],
}

高级匹配

javascript
export const config = {
  matcher: [
    {
      source: '/api/*',
      has: [
        { type: 'header', key: 'Authorization', value: 'Bearer Token' },
        { type: 'query', key: 'userId', value: '123' },
      ],
      missing: [{ type: 'cookie', key: 'session', value: 'active' }],
    },
  ],
}

5.2.2 使用条件语句

javascript
export function middleware(request) {
  if (request.nextUrl.pathname.startsWith('/about')) {
    return NextResponse.rewrite(new URL('/about-2', request.url))
  }

  if (request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.rewrite(new URL('/dashboard/user', request.url))
  }
}

5.3 中间件操作

javascript
export function middleware(request) {
  // 读取 cookie
  const cookie = request.cookies.get('nextjs')

  // 设置 cookie
  const response = NextResponse.next()
  response.cookies.set('vercel', 'fast')
  return response
}

5.3.2 处理 Headers

javascript
export function middleware(request) {
  const requestHeaders = new Headers(request.headers)
  requestHeaders.set('x-custom-header', 'value')

  const response = NextResponse.next({
    request: { headers: requestHeaders },
  })

  response.headers.set('x-response-header', 'value')
  return response
}

5.3.3 CORS 配置

javascript
const allowedOrigins = ['https://acme.com', 'https://my-app.org']

export function middleware(request) {
  const origin = request.headers.get('origin') ?? ''
  const isAllowedOrigin = allowedOrigins.includes(origin)

  const isPreflight = request.method === 'OPTIONS'

  if (isPreflight) {
    return NextResponse.json({}, {
      headers: {
        ...(isAllowedOrigin && { 'Access-Control-Allow-Origin': origin }),
        'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
        'Access-Control-Allow-Headers': 'Content-Type, Authorization',
      }
    })
  }

  const response = NextResponse.next()
  if (isAllowedOrigin) {
    response.headers.set('Access-Control-Allow-Origin', origin)
  }
  return response
}

export const config = { matcher: '/api/:path*' }

5.4 执行顺序

  1. headers(next.config.js)
  2. redirects(next.config.js)
  3. 中间件
  4. beforeFiles(next.config.js)
  5. 基于文件系统的路由
  6. afterFiles(next.config.js)
  7. 动态路由
  8. fallback(next.config.js)

5.5 中间件代码组织

5.5.1 简单方式

javascript
async function authMiddleware(request) {
  // 鉴权逻辑
  return NextResponse.next()
}

async function loggingMiddleware(request) {
  // 日志逻辑
  return NextResponse.next()
}

export async function middleware(request) {
  await authMiddleware(request)
  await loggingMiddleware(request)
}

5.5.2 高阶函数方式

javascript
function withAuth(middleware) {
  return async (request) => {
    // 鉴权逻辑
    return middleware(request)
  }
}

function withLogging(middleware) {
  return async (request) => {
    // 日志逻辑
    return middleware(request)
  }
}

export default withLogging(withAuth(() => NextResponse.next()))

5.5.3 链式调用(推荐)

javascript
function chain(functions, index = 0) {
  const current = functions[index]
  if (current) {
    const next = chain(functions, index + 1)
    return current(next)
  }
  return () => NextResponse.next()
}

export default chain([withAuth, withLogging])

5.6 运行时注意事项

Middleware 只支持 Edge Runtime,不支持 Node.js Runtime。

避免使用 Node.js API,优先使用 Web API。


6. 实战案例:构建完整的后台管理系统

6.1 项目结构

src/
├── app/
│   ├── layout.js                    # 根布局
│   ├── page.js                      # 首页
│   ├── (auth)/
│   │   ├── layout.js                # 认证布局
│   │   ├── login/
│   │   │   └── page.js
│   │   └── register/
│   │       └── page.js
│   ├── (dashboard)/
│   │   ├── layout.js                # 后台布局
│   │   ├── page.js                  # 后台首页
│   │   ├── @sidebar/
│   │   │   ├── page.js
│   │   │   └── navigation/
│   │   │       └── page.js
│   │   ├── @content/
│   │   │   ├── page.js
│   │   │   ├── users/
│   │   │   │   ├── page.js
│   │   │   │   └── [id]/
│   │   │   │       └── page.js
│   │   │   └── settings/
│   │   │       └── page.js
│   │   └── default.js
│   └── api/
│       ├── auth/
│       │   └── route.js
│       └── users/
│           ├── route.js
│           └── [id]/
│               └── route.js
├── middleware.js
└── lib/
    ├── auth.js
    └── utils.js

6.2 实现步骤

6.2.1 创建根布局

javascript
// app/layout.js
import './globals.css'
import { Inter } from 'next/font/google'

const inter = Inter({ subsets: ['latin'] })

export const metadata = {
  title: 'Admin Dashboard',
  description: 'Next.js Admin Dashboard',
}

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body className={inter.className}>{children}</body>
    </html>
  )
}

6.2.2 创建中间件(鉴权)

javascript
// middleware.js
import { NextResponse } from 'next/server'
import { verifyToken } from './lib/auth'

export function middleware(request) {
  const token = request.cookies.get('token')
  const isAuthenticated = verifyToken(token?.value)

  // 公开路由
  const publicPaths = ['/login', '/register']
  const isPublicPath = publicPaths.some(path =>
    request.nextUrl.pathname.startsWith(path)
  )

  // 如果未登录且访问受保护路由,重定向到登录页
  if (!isAuthenticated && !isPublicPath) {
    return NextResponse.redirect(new URL('/login', request.url))
  }

  // 如果已登录且访问公开路由,重定向到后台
  if (isAuthenticated && isPublicPath) {
    return NextResponse.redirect(new URL('/', request.url))
  }

  return NextResponse.next()
}

export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
}

6.2.3 创建认证布局

javascript
// app/(auth)/layout.js
export default function AuthLayout({ children }) {
  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-100">
      <div className="bg-white p-8 rounded-lg shadow-md w-full max-w-md">
        {children}
      </div>
    </div>
  )
}

6.2.4 创建后台布局(平行路由)

javascript
// app/(dashboard)/layout.js
import Link from 'next/link'

export default function DashboardLayout({ sidebar, content }) {
  return (
    <div className="min-h-screen flex">
      {/* 侧边栏 */}
      <aside className="w-64 bg-gray-800 text-white">
        <div className="p-4">
          <h1 className="text-xl font-bold">Admin</h1>
        </div>
        {sidebar}
      </aside>

      {/* 主内容区 */}
      <main className="flex-1 bg-gray-100">
        <header className="bg-white shadow p-4">
          <div className="flex justify-between items-center">
            <h2>Dashboard</h2>
            <button>Logout</button>
          </div>
        </header>
        <div className="p-6">
          {content}
        </div>
      </main>
    </div>
  )
}

6.2.5 创建侧边栏插槽

javascript
// app/(dashboard)/@sidebar/page.js
'use client'
import Link from 'next/link'
import { usePathname } from 'next/navigation'

export default function Sidebar() {
  const pathname = usePathname()

  const navItems = [
    { href: '/', label: '首页' },
    { href: '/users', label: '用户管理' },
    { href: '/settings', label: '设置' },
  ]

  return (
    <nav className="mt-4">
      {navItems.map((item) => (
        <Link
          key={item.href}
          href={item.href}
          className={`block px-4 py-2 hover:bg-gray-700 ${
            pathname === item.href ? 'bg-gray-700' : ''
          }`}
        >
          {item.label}
        </Link>
      ))}
    </nav>
  )
}

6.2.6 创建 API 路由

javascript
// app/api/users/route.js
import { NextResponse } from 'next/server'

// 获取用户列表
export async function GET(request) {
  const searchParams = request.nextUrl.searchParams
  const page = searchParams.get('page') || 1
  const limit = searchParams.get('limit') || 10

  // 模拟数据
  const users = [
    { id: 1, name: 'Alice', email: 'alice@example.com' },
    { id: 2, name: 'Bob', email: 'bob@example.com' },
  ]

  return NextResponse.json({
    data: users,
    pagination: { page, limit, total: users.length },
  })
}

// 创建用户
export async function POST(request) {
  const body = await request.json()
  const newUser = {
    id: Date.now(),
    ...body,
  }

  return NextResponse.json({ data: newUser }, { status: 201 })
}

6.2.7 创建用户详情页(动态路由)

javascript
// app/(dashboard)/@content/users/[id]/page.js
async function getUser(id) {
  // 模拟 API 调用
  return { id, name: 'Alice', email: 'alice@example.com' }
}

export default async function UserPage({ params }) {
  const user = await getUser(params.id)

  return (
    <div>
      <h1 className="text-2xl font-bold mb-4">用户详情</h1>
      <div className="bg-white p-6 rounded-lg shadow">
        <div className="mb-4">
          <label className="block text-sm font-medium mb-1">ID</label>
          <p>{user.id}</p>
        </div>
        <div className="mb-4">
          <label className="block text-sm font-medium mb-1">姓名</label>
          <p>{user.name}</p>
        </div>
        <div>
          <label className="block text-sm font-medium mb-1">邮箱</label>
          <p>{user.email}</p>
        </div>
      </div>
    </div>
  )
}

6.2.8 添加加载状态

javascript
// app/(dashboard)/@content/users/[id]/loading.js
export default function UserLoading() {
  return (
    <div className="flex items-center justify-center h-64">
      <div className="text-gray-500">加载中...</div>
    </div>
  )
}

6.2.9 添加错误处理

javascript
// app/(dashboard)/@content/users/[id]/error.js
'use client'
import { useEffect } from 'react'

export default function UserError({ error, reset }) {
  useEffect(() => {
    console.error(error)
  }, [error])

  return (
    <div className="bg-red-50 border border-red-200 rounded-lg p-6">
      <h2 className="text-xl font-bold text-red-800 mb-2">加载失败</h2>
      <p className="text-red-600 mb-4">
        无法加载用户信息,请稍后重试。
      </p>
      <button
        onClick={reset}
        className="bg-red-600 text-white px-4 py-2 rounded hover:bg-red-700"
      >
        重试
      </button>
    </div>
  )
}

6.3 功能清单

  • ✅ 路由分组:(auth)(dashboard)
  • ✅ 平行路由:@sidebar@content
  • ✅ 动态路由:users/[id]
  • ✅ 中间件鉴权
  • ✅ API 路由:api/users
  • ✅ 加载状态:loading.js
  • ✅ 错误处理:error.js
  • ✅ 嵌套布局

总结

Next.js App Router 提供了一套完整的路由解决方案:

核心概念

  1. 文件系统路由:通过文件夹和文件定义路由
  2. 特殊文件:page.js、layout.js、loading.js、error.js、not-found.js
  3. 路由导航:Link、useRouter、redirect、History API
  4. 高级路由:动态路由、路由组、平行路由、拦截路由
  5. API 开发:route.js 处理各种 HTTP 请求
  6. 中间件:全局请求拦截和控制

最佳实践

  1. 路由组织:使用路由组组织代码,避免 URL 暴露内部结构
  2. 布局复用:充分利用嵌套布局和状态保持特性
  3. 性能优化:合理使用 loading.js 和缓存策略
  4. 用户体验:使用平行路由实现复杂的布局场景
  5. 代码维护:中间件使用链式调用组织逻辑

学习路径建议

  1. 先掌握基础路由和文件约定
  2. 学习导航方式和状态管理
  3. 深入理解动态路由和平行路由
  4. 掌握 API 开发和中间件
  5. 通过实际项目整合所有知识

参考资源