Next.js页面与布局 #

一、页面基础 #

1.1 页面定义 #

页面是路由的UI组件,通过 page.tsx 文件定义:

tsx
export default function HomePage() {
    return (
        <main>
            <h1>首页</h1>
        </main>
    )
}

1.2 页面位置 #

text
app/
├── page.tsx              → /
├── about/
│   └── page.tsx          → /about
├── blog/
│   ├── page.tsx          → /blog
│   └── [slug]/
│       └── page.tsx      → /blog/:slug

1.3 页面组件特点 #

  • 必须默认导出一个 React 组件
  • 组件名称可以任意命名
  • 默认是服务端组件
  • 可以是 async 函数
tsx
export default async function BlogPage() {
    const posts = await getPosts()
    
    return (
        <div>
            {posts.map(post => (
                <article key={post.id}>{post.title}</article>
            ))}
        </div>
    )
}

二、布局基础 #

2.1 布局定义 #

布局是多个页面共享的UI组件,通过 layout.tsx 文件定义:

tsx
export default function RootLayout({
    children,
}: {
    children: React.ReactNode
}) {
    return (
        <html lang="zh-CN">
            <body>
                <header>导航栏</header>
                <main>{children}</main>
                <footer>页脚</footer>
            </body>
        </html>
    )
}

2.2 根布局 #

根布局是必需的,位于 app/layout.tsx

tsx
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'

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

export const metadata: Metadata = {
    title: '我的应用',
    description: '使用 Next.js 构建的应用',
}

export default function RootLayout({
    children,
}: {
    children: React.ReactNode
}) {
    return (
        <html lang="zh-CN">
            <body className={inter.className}>
                {children}
            </body>
        </html>
    )
}

2.3 布局特点 #

  • 必须接收 children prop
  • 布局在导航时保持状态
  • 不会重新渲染
  • 可以嵌套

三、嵌套布局 #

3.1 目录结构 #

text
app/
├── layout.tsx            # 根布局
├── page.tsx              # 首页
├── dashboard/
│   ├── layout.tsx        # 仪表盘布局
│   ├── page.tsx          # /dashboard
│   ├── analytics/
│   │   └── page.tsx      # /dashboard/analytics
│   └── settings/
│       └── page.tsx      # /dashboard/settings

3.2 嵌套布局示例 #

根布局

tsx
export default function RootLayout({
    children,
}: {
    children: React.ReactNode
}) {
    return (
        <html lang="zh-CN">
            <body>
                <TopNav />
                {children}
            </body>
        </html>
    )
}

仪表盘布局

tsx
export default function DashboardLayout({
    children,
}: {
    children: React.ReactNode
}) {
    return (
        <div className="flex">
            <Sidebar />
            <main className="flex-1 p-6">
                {children}
            </main>
        </div>
    )
}

3.3 布局层级关系 #

text
RootLayout
└── DashboardLayout
    ├── DashboardPage
    ├── AnalyticsPage
    └── SettingsPage

四、布局与页面通信 #

4.1 通过params传递 #

tsx
interface LayoutProps {
    children: React.ReactNode
    params: Promise<{ category: string }>
}

export default async function CategoryLayout({
    children,
    params,
}: LayoutProps) {
    const { category } = await params
    const categoryInfo = await getCategoryInfo(category)
    
    return (
        <div>
            <header>
                <h1>{categoryInfo.name}</h1>
            </header>
            {children}
        </div>
    )
}

4.2 通过搜索参数传递 #

tsx
interface PageProps {
    searchParams: Promise<{ view?: string }>
}

export default async function ProductsPage({ searchParams }: PageProps) {
    const { view = 'grid' } = await searchParams
    
    return (
        <div>
            <ViewToggle currentView={view} />
            <ProductList view={view} />
        </div>
    )
}

五、布局最佳实践 #

5.1 导航布局 #

tsx
import Link from 'next/link'
import { usePathname } from 'next/navigation'

const navItems = [
    { href: '/', label: '首页' },
    { href: '/products', label: '产品' },
    { href: '/about', label: '关于' },
]

export default function MainLayout({
    children,
}: {
    children: React.ReactNode
}) {
    return (
        <div className="min-h-screen flex flex-col">
            <header className="bg-white border-b">
                <div className="container mx-auto px-4">
                    <div className="flex items-center justify-between h-16">
                        <Link href="/" className="text-xl font-bold">
                            Logo
                        </Link>
                        <nav className="flex gap-6">
                            {navItems.map((item) => (
                                <NavLink key={item.href} href={item.href}>
                                    {item.label}
                                </NavLink>
                            ))}
                        </nav>
                    </div>
                </div>
            </header>
            <main className="flex-1">
                {children}
            </main>
            <footer className="bg-gray-100 py-8">
                <div className="container mx-auto px-4">
                    <p className="text-center text-gray-600">
                        © 2024 我的应用. 保留所有权利.
                    </p>
                </div>
            </footer>
        </div>
    )
}

5.2 仪表盘布局 #

tsx
import Sidebar from '@/components/Sidebar'
import Header from '@/components/Header'

export default function DashboardLayout({
    children,
}: {
    children: React.ReactNode
}) {
    return (
        <div className="min-h-screen bg-gray-50">
            <Sidebar />
            <div className="lg:pl-64">
                <Header />
                <main className="p-6">
                    {children}
                </main>
            </div>
        </div>
    )
}

5.3 认证布局 #

tsx
export default function AuthLayout({
    children,
}: {
    children: React.ReactNode
}) {
    return (
        <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-500 to-purple-600">
            <div className="bg-white p-8 rounded-lg shadow-xl w-full max-w-md">
                <div className="text-center mb-8">
                    <h1 className="text-2xl font-bold">欢迎</h1>
                </div>
                {children}
            </div>
        </div>
    )
}

六、页面组件模式 #

6.1 服务端组件 #

tsx
async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
    const { id } = await params
    const product = await getProduct(id)
    
    return (
        <div>
            <h1>{product.name}</h1>
            <p>{product.description}</p>
        </div>
    )
}

6.2 客户端组件 #

tsx
'use client'

import { useState } from 'react'

export default function CounterPage() {
    const [count, setCount] = useState(0)
    
    return (
        <div>
            <h1>计数器</h1>
            <p>当前计数: {count}</p>
            <button onClick={() => setCount(count + 1)}>
                增加
            </button>
        </div>
    )
}

6.3 混合组件 #

tsx
import { Suspense } from 'react'
import ProductList from './ProductList'
import ProductListSkeleton from './ProductListSkeleton'

export default function ProductsPage() {
    return (
        <div>
            <h1>产品列表</h1>
            <Suspense fallback={<ProductListSkeleton />}>
                <ProductList />
            </Suspense>
        </div>
    )
}

七、布局状态管理 #

7.1 布局保持状态 #

tsx
'use client'

import { useState } from 'react'

export default function Layout({
    children,
}: {
    children: React.ReactNode
}) {
    const [sidebarOpen, setSidebarOpen] = useState(true)
    
    return (
        <div className="flex">
            <aside className={sidebarOpen ? 'w-64' : 'w-16'}>
                <button onClick={() => setSidebarOpen(!sidebarOpen)}>
                    {sidebarOpen ? '收起' : '展开'}
                </button>
            </aside>
            <main>{children}</main>
        </div>
    )
}

7.2 使用Context共享状态 #

tsx
'use client'

import { createContext, useContext, useState } from 'react'

interface SidebarContextType {
    isOpen: boolean
    toggle: () => void
}

const SidebarContext = createContext<SidebarContextType | null>(null)

export function useSidebar() {
    const context = useContext(SidebarContext)
    if (!context) {
        throw new Error('useSidebar must be used within SidebarProvider')
    }
    return context
}

export default function Layout({
    children,
}: {
    children: React.ReactNode
}) {
    const [isOpen, setIsOpen] = useState(true)
    
    return (
        <SidebarContext.Provider value={{ isOpen, toggle: () => setIsOpen(!isOpen) }}>
            <div className="flex">
                <Sidebar />
                <main>{children}</main>
            </div>
        </SidebarContext.Provider>
    )
}

八、总结 #

页面与布局要点:

要点 说明
页面定义 page.tsx 文件
布局定义 layout.tsx 文件
根布局 必需,包含html/body
嵌套布局 支持多层嵌套
状态保持 布局导航时不重新渲染
组件类型 服务端/客户端组件

下一步,让我们学习模板与默认组件!

最后更新:2026-03-28