Next.js路由组与并行路由 #
一、路由组 #
1.1 概念介绍 #
路由组使用括号 () 包裹文件夹名称,不会影响URL路径,仅用于组织代码:
text
app/
├── (marketing)/
│ ├── about/
│ │ └── page.tsx → /about
│ ├── contact/
│ │ └── page.tsx → /contact
│ └── layout.tsx # 营销页面共享布局
├── (shop)/
│ ├── cart/
│ │ └── page.tsx → /cart
│ ├── products/
│ │ └── page.tsx → /products
│ └── layout.tsx # 商店页面共享布局
└── layout.tsx # 根布局
1.2 路由组用途 #
| 用途 | 说明 |
|---|---|
| 分组布局 | 不同路由组使用不同布局 |
| 代码组织 | 按功能模块组织代码 |
| 条件渲染 | 根据条件显示不同内容 |
| 权限控制 | 按组设置访问权限 |
1.3 共享布局示例 #
营销页面布局
tsx
export default function MarketingLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="marketing-layout">
<header className="bg-blue-600 text-white">
<MarketingNav />
</header>
<main className="container mx-auto py-8">
{children}
</main>
<footer>
<MarketingFooter />
</footer>
</div>
)
}
商店页面布局
tsx
export default function ShopLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="shop-layout">
<header className="bg-gray-100">
<ShopNav />
<CartIndicator />
</header>
<div className="flex">
<aside className="w-64">
<CategorySidebar />
</aside>
<main className="flex-1 p-6">
{children}
</main>
</div>
</div>
)
}
1.4 认证路由组 #
text
app/
├── (auth)/
│ ├── login/
│ │ └── page.tsx → /login
│ ├── register/
│ │ └── page.tsx → /register
│ ├── forgot-password/
│ │ └── page.tsx → /forgot-password
│ └── layout.tsx # 认证页面布局(无导航栏)
└── (dashboard)/
├── layout.tsx # 仪表盘布局(需要认证)
├── dashboard/
│ └── page.tsx → /dashboard
└── settings/
└── page.tsx → /settings
认证布局
tsx
export default function AuthLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="max-w-md w-full">
<Logo />
{children}
</div>
</div>
)
}
仪表盘布局
tsx
import { auth } from '@/auth'
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
const session = await auth()
if (!session) {
redirect('/login')
}
return (
<div className="flex min-h-screen">
<Sidebar user={session.user} />
<main className="flex-1 p-6">
{children}
</main>
</div>
)
}
1.5 多语言路由组 #
text
app/
├── [lang]/
│ ├── (marketing)/
│ │ ├── about/
│ │ │ └── page.tsx → /zh/about, /en/about
│ │ └── layout.tsx
│ ├── (shop)/
│ │ ├── products/
│ │ │ └── page.tsx → /zh/products, /en/products
│ │ └── layout.tsx
│ └── layout.tsx # 语言布局
└── layout.tsx
tsx
interface LayoutProps {
children: React.ReactNode
params: Promise<{ lang: string }>
}
export default async function LangLayout({ children, params }: LayoutProps) {
const { lang } = await params
const dict = await getDictionary(lang)
return (
<html lang={lang}>
<body>
<Navigation dict={dict} />
{children}
</body>
</html>
)
}
二、并行路由 #
2.1 概念介绍 #
并行路由使用 @ 前缀创建命名插槽,允许同时渲染多个页面:
text
app/
└── dashboard/
├── @team/
│ └── page.tsx # 团队插槽
├── @analytics/
│ └── page.tsx # 分析插槽
├── layout.tsx # 接收插槽
└── page.tsx # 默认页面
2.2 布局接收插槽 #
tsx
interface LayoutProps {
children: React.ReactNode
team: React.ReactNode
analytics: React.ReactNode
}
export default function DashboardLayout({
children,
team,
analytics,
}: LayoutProps) {
return (
<div className="dashboard">
<header>
<DashboardNav />
</header>
<div className="grid grid-cols-2 gap-4 p-4">
<div className="border rounded-lg p-4">
<h2>团队</h2>
{team}
</div>
<div className="border rounded-lg p-4">
<h2>分析</h2>
{analytics}
</div>
</div>
{children}
</div>
)
}
2.3 插槽页面 #
团队插槽
tsx
export default async function TeamSlot() {
const members = await getTeamMembers()
return (
<ul className="space-y-2">
{members.map((member) => (
<li key={member.id} className="flex items-center gap-2">
<img src={member.avatar} className="w-8 h-8 rounded-full" />
<span>{member.name}</span>
</li>
))}
</ul>
)
}
分析插槽
tsx
export default async function AnalyticsSlot() {
const stats = await getAnalytics()
return (
<div className="space-y-4">
<div className="flex justify-between">
<span>访问量</span>
<span>{stats.visits}</span>
</div>
<div className="flex justify-between">
<span>用户数</span>
<span>{stats.users}</span>
</div>
</div>
)
}
2.4 默认页面 default.tsx #
当插槽没有匹配的页面时,显示默认页面:
tsx
export default function DefaultTeamSlot() {
return <p>选择一个团队查看详情</p>
}
2.5 条件渲染 #
tsx
interface LayoutProps {
children: React.ReactNode
modal: React.ReactNode
}
export default function Layout({ children, modal }: LayoutProps) {
return (
<>
{children}
{modal}
</>
)
}
三、拦截路由 #
3.1 概念介绍 #
拦截路由允许在当前布局中加载另一个路由的内容,常用于模态框:
text
app/
├── feed/
│ └── page.tsx → /feed
├── photo/
│ └── [id]/
│ └── page.tsx → /photo/:id
└── (.)photo/
└── [id]/
└── page.tsx # 拦截路由
3.2 拦截路由约定 #
| 约定 | 说明 |
|---|---|
(.) |
拦截同级路由 |
(..) |
拦截上一级路由 |
(..)(..) |
拦截上两级路由 |
(...) |
拦截根目录路由 |
3.3 模态框示例 #
Feed页面
tsx
import Link from 'next/link'
export default function FeedPage() {
return (
<div className="feed">
<h1>动态</h1>
<div className="grid grid-cols-3 gap-4">
{photos.map((photo) => (
<Link
key={photo.id}
href={`/photo/${photo.id}`}
className="aspect-square"
>
<img src={photo.url} className="w-full h-full object-cover" />
</Link>
))}
</div>
</div>
)
}
拦截路由(模态框)
tsx
'use client'
import { useRouter } from 'next/navigation'
interface PageProps {
params: Promise<{ id: string }>
}
export default function PhotoModal({ params }: PageProps) {
const router = useRouter()
const { id } = use(params)
const photo = getPhoto(id)
return (
<div
className="fixed inset-0 bg-black/50 flex items-center justify-center"
onClick={() => router.back()}
>
<div
className="bg-white rounded-lg max-w-2xl"
onClick={(e) => e.stopPropagation()}
>
<img src={photo.url} className="w-full" />
<div className="p-4">
<h2>{photo.title}</h2>
<p>{photo.description}</p>
</div>
</div>
</div>
)
}
实际照片页面
tsx
interface PageProps {
params: Promise<{ id: string }>
}
export default async function PhotoPage({ params }: PageProps) {
const { id } = await params
const photo = await getPhoto(id)
return (
<div className="max-w-4xl mx-auto py-8">
<img src={photo.url} className="w-full" />
<h1 className="text-2xl font-bold mt-4">{photo.title}</h1>
<p className="mt-2">{photo.description}</p>
</div>
)
}
3.4 使用场景 #
| 场景 | 说明 |
|---|---|
| 图片预览 | 点击图片弹出模态框 |
| 登录弹窗 | 在任意页面弹出登录框 |
| 表单编辑 | 弹出编辑表单 |
| 购物车 | 侧边栏购物车 |
四、综合案例 #
4.1 仪表盘布局 #
text
app/
└── dashboard/
├── @sidebar/
│ └── page.tsx
├── @content/
│ ├── page.tsx
│ └── users/
│ └── page.tsx
├── layout.tsx
└── page.tsx
tsx
interface LayoutProps {
sidebar: React.ReactNode
content: React.ReactNode
}
export default function DashboardLayout({ sidebar, content }: LayoutProps) {
return (
<div className="flex min-h-screen">
<aside className="w-64 bg-gray-800 text-white">
{sidebar}
</aside>
<main className="flex-1 p-6">
{content}
</main>
</div>
)
}
4.2 带模态框的应用 #
text
app/
├── layout.tsx
├── page.tsx
├── login/
│ └── page.tsx → /login
└── (.)login/
└── page.tsx # 拦截登录路由
拦截登录(模态框)
tsx
'use client'
import { useRouter } from 'next/navigation'
import LoginForm from '@/components/LoginForm'
export default function LoginModal() {
const router = useRouter()
return (
<div
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
onClick={() => router.back()}
>
<div
className="bg-white rounded-lg p-6 w-full max-w-md"
onClick={(e) => e.stopPropagation()}
>
<LoginForm onSuccess={() => router.back()} />
</div>
</div>
)
}
4.3 复杂布局组合 #
text
app/
├── (marketing)/
│ ├── layout.tsx
│ ├── page.tsx → /
│ ├── about/
│ │ └── page.tsx → /about
│ └── (.)login/
│ └── page.tsx # 登录模态框
├── (dashboard)/
│ ├── layout.tsx
│ ├── @sidebar/
│ │ └── page.tsx
│ ├── @main/
│ │ └── page.tsx
│ └── dashboard/
│ └── page.tsx → /dashboard
└── api/
└── auth/
└── route.ts
五、最佳实践 #
5.1 路由组命名 #
text
app/
├── (public)/ # 公开页面
├── (auth)/ # 认证页面
├── (admin)/ # 管理页面
└── (api)/ # API路由
5.2 布局层级 #
text
app/
├── layout.tsx # 根布局
├── (marketing)/
│ ├── layout.tsx # 营销布局
│ └── about/
│ └── layout.tsx # 关于页面布局
5.3 并行路由命名 #
text
app/
└── dashboard/
├── @sidebar/ # 侧边栏
├── @header/ # 头部
├── @content/ # 主内容
└── layout.tsx
六、总结 #
路由组织要点:
| 特性 | 语法 | 用途 |
|---|---|---|
| 路由组 | (group) |
组织代码、共享布局 |
| 并行路由 | @slot |
多区域渲染 |
| 拦截路由 | (.) |
模态框、预览 |
| 默认页面 | default.tsx |
插槽后备 |
下一步,让我们学习中间件!
最后更新:2026-03-28