Appearance
Next.js App Router 实战指南:从基础到进阶的完整路由体系
Next.js 13.4 起,App Router 成为默认路由方案,提供了更强大的路由功能和更灵活的代码组织方式。本文将带你从零掌握 App Router 的核心概念、进阶模式和实战技巧。
目录
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>
}使用场景:
- 依赖
useEffect和useState的功能 - 每次路由切换需要重新执行的场景
布局 vs 模板的区别:
| 特性 | Layout | Template |
|---|---|---|
| 状态保持 | ✅ 是 | ❌ 否 |
| 组件重新挂载 | ❌ 否 | ✅ 是 |
| 适用场景 | 侧边导航、头部 | 登录表单、统计页面 |
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.js或template.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
│ └── Page2. 路由导航:四种方式实现页面跳转
2.1 使用 Link 组件(推荐)
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-world → params = { 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/tops → params = { 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>
}访问 /shop → params = {} 访问 /shop/a → params = { 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 → /cart3.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.jsjavascript
// 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.js3.3.4 子导航
app/
├── @analytics/
│ ├── page.js → /
│ ├── page-views/
│ │ └── page.js → /page-views
│ └── visitors/
│ └── page.js → /visitors
└── layout.js3.3.5 default.js
处理硬导航时插槽不匹配的情况:
javascript
// app/@team/default.js
export default function Default() {
return <div>Team Default Content</div>
}优势:
- 代码更易于管理,适合团队协作
- 每个插槽独立的加载状态和错误处理
- 每个插槽可以有自己的导航和状态管理
3.4 拦截路由(Intercepting Routes)
在当前路由拦截其他路由地址。
3.4.1 实现方式
文件夹命名规则:
(.)- 匹配同一层级(..)- 匹配上一层级(..)(..)- 匹配上上层级(...)- 匹配根目录
3.4.2 实战案例:图片详情 Modal
app/
├── @modal/
│ ├── default.js
│ └── (.)photo/
│ └── [id]/
│ └── page.js
├── photo/
│ └── [id]/
│ └── page.js
└── page.jsjavascript
// 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
}动态路由参数示例:
| 路由文件 | URL | params |
|---|---|---|
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() })
}退出缓存的条件:
- 使用
Request对象 - 添加其他 HTTP 方法(如 POST)
- 使用动态函数(cookies、headers)
- 设置
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)
}4.5.2 处理 Cookie
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 中间件操作
5.3.1 处理 Cookie
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 执行顺序
headers(next.config.js)redirects(next.config.js)- 中间件
beforeFiles(next.config.js)- 基于文件系统的路由
afterFiles(next.config.js)- 动态路由
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.js6.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 提供了一套完整的路由解决方案:
核心概念
- 文件系统路由:通过文件夹和文件定义路由
- 特殊文件:page.js、layout.js、loading.js、error.js、not-found.js
- 路由导航:Link、useRouter、redirect、History API
- 高级路由:动态路由、路由组、平行路由、拦截路由
- API 开发:route.js 处理各种 HTTP 请求
- 中间件:全局请求拦截和控制
最佳实践
- 路由组织:使用路由组组织代码,避免 URL 暴露内部结构
- 布局复用:充分利用嵌套布局和状态保持特性
- 性能优化:合理使用 loading.js 和缓存策略
- 用户体验:使用平行路由实现复杂的布局场景
- 代码维护:中间件使用链式调用组织逻辑
学习路径建议
- 先掌握基础路由和文件约定
- 学习导航方式和状态管理
- 深入理解动态路由和平行路由
- 掌握 API 开发和中间件
- 通过实际项目整合所有知识