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