上节我们了解了 Next.js 的项目结构,本节来深入了解一下布局和页面的使用。布局和页面是 Next.js 中最基础也是最重要的概念,掌握好它们能让你更高效地组织应用的 UI 结构。
Layouts(布局)
Layout 是 React 组件,用来定义多个页面之间共享的 UI。很多初学者会问,既然每个页面都有自己的内容,为什么还需要布局呢?其实想一想,大部分网站的导航栏、侧边栏、页脚等都是通用的,如果在每个页面都重复写一遍,不仅代码冗余,维护起来也非常麻烦。这就是 Layout 存在的意义。
Layout 有几个关键特性值得注意:
- 状态保持 - 导航时不会重新渲染,所以状态得以保留
- 默认是 Server Components - 性能更好,但也可以改成客户端组件
- 可以访问数据获取方法 - 在布局层面获取数据很方便
- 不能设置
<title>标签 - 这个要在页面级别设置
根布局(Root Layout)
每个 Next.js 应用都必须有一个根布局 app/layout.tsx,这是最外层的布局,所有页面都会继承它。
bash
// app/layout.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: '我的 Next.js 应用',
description: '使用 Next.js 16 构建的现代化应用',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="zh-CN">
<body className={inter.className}>
<nav className="bg-gray-800 text-white p-4">
<a href="/">首页</a>
<a href="/about">关于</a>
<a href="/contact">联系</a>
</nav>
<main className="container mx-auto p-4">
{children}
</main>
<footer className="bg-gray-200 p-4 text-center">
© 2026 My App
</footer>
</body>
</html>
)
}
根布局有两个必须包含的元素:<html> 和 <body> 标签。这个不能省,否则 Next.js 会报错。
嵌套布局
实际开发中,我们经常需要针对不同的页面区域使用不同的布局。Next.js 支持嵌套布局,子路由可以继承父路由的布局。这个特性在管理后台、多面板应用等场景中特别有用。
bash
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="flex">
<aside className="w-64 bg-gray-100 p-4">
<h2>仪表盘</h2>
<nav>
<a href="/dashboard/overview">概览</a>
<a href="/dashboard/analytics">分析</a>
<a href="/dashboard/settings">设置</a>
</nav>
</aside>
<div className="flex-1 p-4">
{children}
</div>
</div>
)
}
多个布局
有时候我们想要不同的页面区域使用完全独立的布局,这时就可以使用路由组。路由组用括号括起来,不会影响 URL 路径,但可以有自己的布局。
bash
app/
├── (marketing)/ # 营销布局
│ ├── layout.tsx
│ ├── about/
│ └── contact/
├── (dashboard)/ # 仪表盘布局
│ ├── layout.tsx
│ ├── overview/
│ └── settings/
└── layout.tsx # 根布局
bash
// app/(marketing)/layout.tsx
export default function MarketingLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="marketing-layout">
<Header />
{children}
<Footer />
</div>
)
}
// app/(dashboard)/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="dashboard-layout">
<Sidebar />
<div className="main-content">
<TopBar />
{children}
</div>
</div>
)
}
Pages(页面)
Page 是路由的独特 UI,每个路由都有自己的 page.tsx 文件。页面和布局配合使用,布局负责共享的 UI,页面负责独有的内容。
基本页面
最简单的页面就是一个导出的 React 组件:
bash
// app/about/page.tsx
export default function AboutPage() {
return (
<div>
<h1>关于我们</h1>
<p>这是关于页面</p>
</div>
)
}
带数据获取的页面
Next.js 的强大之处在于可以直接在页面组件中获取数据,而不需要像传统 React 那样用 useEffect。
bash
// app/posts/page.tsx
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 }, // 缓存 1 小时
})
return res.json()
}
export default async function PostsPage() {
const posts = await getPosts()
return (
<div>
<h1>博客文章</h1>
<ul>
{posts.map((post: any) => (
<li key={post.id}>
<a href={`/posts/${post.id}`}>{post.title}</a>
</li>
))}
</ul>
</div>
)
}
动态路由页面
动态路由在实际开发中非常常见,比如博客文章页、用户详情页等。使用方括号 [param] 就可以创建动态路由。
bash
// app/posts/[slug]/page.tsx
async function getPost(slug: string) {
const res = await fetch(`https://api.example.com/posts/${slug}`)
return res.json()
}
export default async function PostPage({
params,
}: {
params: { slug: string }
}) {
const post = await getPost(params.slug)
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
)
}
生成静态参数
如果你的动态路由是静态内容,可以使用 generateStaticParams 在构建时生成所有可能的路径,这样访问速度会更快。
bash
// app/posts/[slug]/page.tsx
// 生成所有可能的 slug 值
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then((res) =>
res.json()
)
return posts.map((post: any) => ({
slug: post.slug,
}))
}
export default async function PostPage({
params,
}: {
params: { slug: string }
}) {
const post = await getPost(params.slug)
return <article>{/* ... */}</article>
}
Template 组件
Template 和 Layout 看起来很像,但有一个重要区别:Template 在每次导航时都会重新创建新的实例。这意味着什么?如果你的布局有一些状态或副作用需要在每次页面切换时重置,就应该用 Template。
bash
// app/template.tsx
export default function Template({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="template">
<div className="banner">横幅广告</div>
{children}
</div>
)
}
Layout vs Template
这个对比表能帮你快速理解两者的区别:
| 特性 | Layout | Template |
|---|---|---|
| 状态保持 | 会保留,导航时不重置 | 每次导航都重新创建 |
| 重新挂载 | 不会 | 会 |
| useEffect 执行 | 仅首次 | 每次导航都执行 |
| 使用场景 | 导航、侧边栏等共享 UI | 登录页面、动画等需要重置的场景 |
实用模式
下面分享几个在实际项目中常用的布局模式。
1. 条件布局
根据设备类型或用户状态显示不同的布局:
bash
// app/layout.tsx
import { headers } from 'next/headers'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
const headersList = headers()
const isMobile = headersList.get('user-agent')?.includes('Mobile')
return (
<html>
<body>
{isMobile ? <MobileNav /> : <DesktopNav />}
{children}
</body>
</html>
)
}
2. 布局组合
管理后台常用的布局组合:
bash
// app/admin/layout.tsx
import AdminNav from '@/components/AdminNav'
import AdminSidebar from '@/components/AdminSidebar'
export default function AdminLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="flex h-screen">
<AdminSidebar />
<div className="flex-1 flex flex-col">
<AdminNav />
<main className="flex-1 overflow-auto p-6">
{children}
</main>
</div>
</div>
)
}
3. 认证布局
需要登录才能访问的布局:
bash
// app/(dashboard)/layout.tsx
import { redirect } from 'next/navigation'
import { getServerSession } from '@/lib/auth'
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
const session = await getServerSession()
if (!session) {
redirect('/login')
}
return (
<div className="dashboard">
<UserMenu user={session.user} />
{children}
</div>
)
}
4. 错误边界布局
给布局添加错误处理:
bash
// app/dashboard/error.tsx
'use client'
export default function Error({
error,
reset,
}: {
error: Error
reset: () => void
}) {
return (
<div className="error-container">
<h2>出错了!</h2>
<button onClick={reset}>重试</button>
</div>
)
}
数据获取
服务器组件数据获取
服务器组件可以直接在组件函数中 async/await 获取数据:
bash
// app/users/page.tsx
async function getUsers() {
const res = await fetch('https://api.example.com/users', {
cache: 'force-cache', // 或 'no-store'
})
if (!res.ok) {
throw new Error('获取用户失败')
}
return res.json()
}
export default async function UsersPage() {
const users = await getUsers()
return (
<div>
<h1>用户列表</h1>
<ul>
{users.map((user: any) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
)
}
客户端组件数据获取
客户端组件需要用传统的方式,比如 useState + useEffect:
bash
// app/users/page.tsx
'use client'
import { useState, useEffect } from 'react'
export default function UsersPage() {
const [users, setUsers] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
async function fetchUsers() {
try {
const res = await fetch('https://api.example.com/users')
const data = await res.json()
setUsers(data)
} finally {
setLoading(false)
}
}
fetchUsers()
}, [])
if (loading) return <div>加载中...</div>
return (
<div>
<h1>用户列表</h1>
<ul>
{users.map((user: any) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
)
}
最佳实践
1. 保持布局简单
布局应该尽量简单,不要在布局里放复杂的状态逻辑。如果需要复杂的状态管理,考虑抽成独立的组件。
bash
// 好的做法
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<div>
<Header />
{children}
<Footer />
</div>
)
}
// 不好的做法
export default function Layout({ children }: { children: React.ReactNode }) {
const [data, setData] = useState(null) // 布局不应有复杂状态
useEffect(() => {
fetchData().then(setData)
}, [])
return <div>{/* ... */}</div>
}
2. 使用类型安全
给组件的 props 加上类型定义,能避免很多低级错误:
bash
// app/layout.tsx
import type { ReactNode } from 'react'
type LayoutProps = {
children: ReactNode
}
export default function RootLayout({ children }: LayoutProps) {
return <html><body>{children}</body></html>
}
3. 适当分割布局
不要把所有东西都塞在根布局里,根据业务需求适当分割:
bash
// app/layout.tsx - 根布局(简单)
export default function RootLayout({ children }: LayoutProps) {
return (
<html>
<body>
<Header />
{children}
</body>
</html>
)
}
// app/(dashboard)/layout.tsx - 仪表盘布局(特定)
export default function DashboardLayout({ children }: LayoutProps) {
return (
<div className="dashboard-layout">
<Sidebar />
{children}
</div>
)
}
总结
本节我们深入了解了 Next.js 的布局和页面系统,包括根布局、嵌套布局、多个布局、动态路由等核心概念。布局和页面是 Next.js 应用的基础,掌握好它们能让你的代码结构更清晰、维护更方便。
如果你对本节内容有任何疑问,欢迎在评论区提出来,我们一起学习讨论。