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 布局特点 #
- 必须接收
childrenprop - 布局在导航时保持状态
- 不会重新渲染
- 可以嵌套
三、嵌套布局 #
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