布局与页面
深入理解 Next.js 的布局和页面系统,掌握文件系统路由和组件组织的最佳实践。
概述
Next.js 使用基于文件系统的路由,通过文件夹和文件定义路由。布局(Layout)和页面(Page)是构建应用的核心概念。
创建页面
页面是在特定路由上渲染的 UI。通过在 app 目录中添加 page 文件并默认导出 React 组件来创建页面。
基础页面
// app/page.tsx
export default function Page() {
return <h1>Hello Next.js!</h1>
}嵌套页面
页面的关键特性:
- 支持
.js、.jsx或.tsx文件扩展名 - 页面始终是路由子树的叶子节点
- 需要
page文件才能使路由段公开访问 - 默认为服务器组件,可设置为客户端组件
创建布局
布局是在多个页面之间共享的 UI。在导航时,布局保持状态、保持交互性且不会重新渲染。
基础布局
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<section>
<nav>Dashboard Navigation</nav>
{children}
</section>
)
}根布局
根布局是 app 目录中最顶层的布局,用于定义 <html> 和 <body> 标签以及其他全局共享的 UI。
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="zh">
<body>{children}</body>
</html>
)
}根布局要求:
- 必须包含
<html>和<body>标签 - 是必需的,不能删除
- 可以使用内置的 SEO 支持管理
<head>元素
嵌套布局
布局可以嵌套,形成布局层次结构。
// app/layout.tsx (根布局)
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="zh">
<body>
<header>全局头部</header>
{children}
<footer>全局底部</footer>
</body>
</html>
)
}
// app/dashboard/layout.tsx (嵌套布局)
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="dashboard">
<aside>侧边栏</aside>
<main>{children}</main>
</div>
)
}渲染结果:
<html lang="zh">
<body>
<header>全局头部</header>
<div class="dashboard">
<aside>侧边栏</aside>
<main>
<!-- 页面内容 -->
</main>
</div>
<footer>全局底部</footer>
</body>
</html>页面 Props
params
包含从根段到该页面的动态路由参数的 Promise 对象。
// app/shop/[slug]/page.tsx
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
return <h1>产品:{slug}</h1>
}路由参数示例:
| 路由 | URL | params |
|---|---|---|
app/shop/[slug]/page.js | /shop/1 | Promise<{ slug: '1' }> |
app/shop/[category]/[item]/page.js | /shop/shoes/nike | Promise<{ category: 'shoes', item: 'nike' }> |
app/shop/[...slug]/page.js | /shop/clothes/tops | Promise<{ slug: ['clothes', 'tops'] }> |
searchParams
包含当前 URL 搜索参数的 Promise 对象。
// app/shop/page.tsx
export default async function Page({
searchParams,
}: {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}) {
const { page = '1', sort = 'asc', query = '' } = await searchParams
return (
<div>
<h1>产品列表</h1>
<p>搜索查询:{query}</p>
<p>当前页:{page}</p>
<p>排序:{sort}</p>
</div>
)
}URL 示例:
/shop?page=2&sort=desc&query=shoessearchParams 值:
{
page: '2',
sort: 'desc',
query: 'shoes'
}布局 Props
children(必需)
布局组件必须接受并使用 children prop。在渲染时,children 将填充布局包裹的路由段。
export default function Layout({
children,
}: {
children: React.ReactNode
}) {
return <section>{children}</section>
}params(可选)
包含从根段到该布局的动态路由参数的 Promise 对象。
// app/dashboard/[team]/layout.tsx
export default async function DashboardLayout({
children,
params,
}: {
children: React.ReactNode
params: Promise<{ team: string }>
}) {
const { team } = await params
return (
<section>
<header>
<h1>欢迎来到 {team} 的仪表板</h1>
</header>
<main>{children}</main>
</section>
)
}在客户端组件中使用 Props
客户端组件不能是 async 的,需要使用 React 的 use 函数读取 Promise。
在页面中使用
'use client'
import { use } from 'react'
export default function Page({
params,
searchParams,
}: {
params: Promise<{ slug: string }>
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}) {
const { slug } = use(params)
const { query } = use(searchParams)
return (
<div>
<h1>{slug}</h1>
<p>搜索:{query}</p>
</div>
)
}在布局中使用
'use client'
import { use } from 'react'
export default function Layout({
children,
params,
}: {
children: React.ReactNode
params: Promise<{ team: string }>
}) {
const { team } = use(params)
return (
<section>
<h1>{team} 团队</h1>
{children}
</section>
)
}路由段配置
可以通过导出特殊变量来配置页面或布局的行为。
dynamic
控制路由段的动态行为。
// app/blog/[slug]/page.tsx
export const dynamic = 'auto' // 默认
// export const dynamic = 'force-dynamic'
// export const dynamic = 'error'
// export const dynamic = 'force-static'
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
return <h1>{slug}</h1>
}选项:
auto(默认):尽可能缓存,不阻止组件选择动态行为force-dynamic:强制动态渲染,每次请求时重新获取数据force-static:强制静态渲染,缓存数据error:如果组件使用动态函数或未缓存的数据,则报错
dynamicParams
控制访问未通过 generateStaticParams 生成的动态段时的行为。
// app/blog/[slug]/page.tsx
export const dynamicParams = true // 默认
export async function generateStaticParams() {
return [{ slug: 'post-1' }, { slug: 'post-2' }]
}
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
return <h1>{slug}</h1>
}选项:
true(默认):未包含在generateStaticParams中的动态段按需生成false:未包含的动态段返回 404
revalidate
设置路由段的默认重新验证时间。
// app/blog/page.tsx
export const revalidate = 3600 // 每小时重新验证
export default async function Page() {
const posts = await fetchPosts()
return <div>{/* 渲染文章 */}</div>
}选项:
false:无限期缓存(默认)0:始终动态渲染number:以秒为单位的重新验证时间
fetchCache
控制路由段的 fetch 缓存行为。
// app/blog/page.tsx
export const fetchCache = 'auto' // 默认
// export const fetchCache = 'default-cache'
// export const fetchCache = 'only-cache'
// export const fetchCache = 'force-cache'
// export const fetchCache = 'default-no-store'
// export const fetchCache = 'only-no-store'
// export const fetchCache = 'force-no-store'runtime
指定路由段的运行时。
// app/api/route.tsx
export const runtime = 'nodejs' // 默认
// export const runtime = 'edge'preferredRegion
指定路由段的首选区域。
// app/api/route.tsx
export const preferredRegion = 'auto' // 默认
// export const preferredRegion = 'global'
// export const preferredRegion = 'home'
// export const preferredRegion = ['iad1', 'sfo1']生成元数据
可以通过导出 metadata 对象或 generateMetadata 函数来定义页面或布局的元数据。
静态元数据
// app/page.tsx
import { Metadata } from 'next'
export const metadata: Metadata = {
title: '我的网站',
description: '欢迎来到我的网站',
}
export default function Page() {
return <h1>首页</h1>
}动态元数据
// app/blog/[slug]/page.tsx
import { Metadata } from 'next'
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>
}): Promise<Metadata> {
const { slug } = await params
const post = await fetchPost(slug)
return {
title: post.title,
description: post.excerpt,
}
}
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const post = await fetchPost(slug)
return <article>{/* 渲染文章 */}</article>
}布局中的元数据
// app/dashboard/layout.tsx
import { Metadata } from 'next'
export const metadata: Metadata = {
title: {
template: '%s | 仪表板',
default: '仪表板',
},
}
export default function Layout({
children,
}: {
children: React.ReactNode
}) {
return <section>{children}</section>
}子页面继承:
// app/dashboard/settings/page.tsx
import { Metadata } from 'next'
export const metadata: Metadata = {
title: '设置', // 渲染为 "设置 | 仪表板"
}生成静态参数
generateStaticParams 函数与动态路由段结合使用,在构建时静态生成路由。
基础用法
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await fetchPosts()
return posts.map((post) => ({
slug: post.slug,
}))
}
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const post = await fetchPost(slug)
return <article>{/* 渲染文章 */}</article>
}多个动态段
// app/products/[category]/[product]/page.tsx
export async function generateStaticParams() {
const products = await fetchProducts()
return products.map((product) => ({
category: product.category,
product: product.id,
}))
}从父段生成
// app/products/[category]/[product]/page.tsx
export async function generateStaticParams({
params,
}: {
params: Promise<{ category: string }>
}) {
const { category } = await params
const products = await fetchProductsByCategory(category)
return products.map((product) => ({
product: product.id,
}))
}链接和导航
使用 Link 组件在页面之间导航。
基础链接
import Link from 'next/link'
export default function Page() {
return (
<nav>
<Link href="/">首页</Link>
<Link href="/about">关于</Link>
<Link href="/blog">博客</Link>
</nav>
)
}动态链接
import Link from 'next/link'
export default function BlogList({ posts }) {
return (
<ul>
{posts.map((post) => (
<li key={post.id}>
<Link href={`/blog/${post.slug}`}>{post.title}</Link>
</li>
))}
</ul>
)
}编程式导航
'use client'
import { useRouter } from 'next/navigation'
export default function Page() {
const router = useRouter()
return (
<button onClick={() => router.push('/dashboard')}>
前往仪表板
</button>
)
}TypeScript 支持
Next.js 提供了全局类型助手,用于页面和布局的 props。
PageProps
// app/blog/[slug]/page.tsx
export default async function Page(props: PageProps<'/blog/[slug]'>) {
const { slug } = await props.params
return <h1>博客文章:{slug}</h1>
}LayoutProps
// app/dashboard/layout.tsx
export default function Layout(props: LayoutProps<'/dashboard'>) {
return (
<section>
{props.children}
{/* 如果有 app/dashboard/@analytics,它会作为类型化的插槽出现 */}
{/* {props.analytics} */}
</section>
)
}TypeScript 类型助手:
- 静态路由将
params解析为{} PageProps和LayoutProps是全局助手,无需导入- 类型在
next dev、next build或next typegen期间生成
实际应用示例
博客应用
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="zh">
<body>
<header>
<nav>
<Link href="/">首页</Link>
<Link href="/blog">博客</Link>
</nav>
</header>
{children}
<footer>© 2025</footer>
</body>
</html>
)
}
// app/blog/layout.tsx
export default function BlogLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="blog-container">
<aside>
<h2>分类</h2>
{/* 分类列表 */}
</aside>
<main>{children}</main>
</div>
)
}
// app/blog/page.tsx
export default async function BlogPage() {
const posts = await fetchPosts()
return (
<div>
<h1>博客文章</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>
<Link href={`/blog/${post.slug}`}>{post.title}</Link>
</li>
))}
</ul>
</div>
)
}
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await fetchPosts()
return posts.map((post) => ({ slug: post.slug }))
}
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const post = await fetchPost(slug)
return {
title: post.title,
description: post.excerpt,
}
}
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const post = await fetchPost(slug)
return (
<article>
<h1>{post.title}</h1>
<time>{post.date}</time>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
)
}电商应用
// app/products/[category]/[product]/page.tsx
export async function generateStaticParams() {
const products = await fetchProducts()
return products.map((product) => ({
category: product.category,
product: product.id,
}))
}
export async function generateMetadata({
params,
}: {
params: Promise<{ category: string; product: string }>
}) {
const { product } = await params
const productData = await fetchProduct(product)
return {
title: productData.name,
description: productData.description,
}
}
export default async function ProductPage({
params,
}: {
params: Promise<{ category: string; product: string }>
}) {
const { category, product } = await params
const productData = await fetchProduct(product)
return (
<div>
<h1>{productData.name}</h1>
<p>分类:{category}</p>
<p>价格:¥{productData.price}</p>
<button>加入购物车</button>
</div>
)
}最佳实践
1. 合理使用布局层次
将共享的 UI 放在适当的布局层级中。
2. 利用路由段配置优化性能
// 静态页面
export const dynamic = 'force-static'
export const revalidate = 3600
// 动态页面
export const dynamic = 'force-dynamic'3. 使用 generateStaticParams 预渲染
export async function generateStaticParams() {
// 在构建时生成所有可能的路由
return await fetchAllSlugs()
}4. 正确处理元数据
// 布局中设置模板
export const metadata = {
title: {
template: '%s | 网站名',
default: '网站名',
},
}
// 页面中设置具体标题
export const metadata = {
title: '页面标题', // 渲染为 "页面标题 | 网站名"
}5. 使用 TypeScript 类型助手
export default async function Page(props: PageProps<'/blog/[slug]'>) {
// 自动获得类型安全的 params
const { slug } = await props.params
}总结
Next.js 的布局和页面系统提供了强大而灵活的方式来组织应用:
- 页面定义特定路由的 UI
- 布局在多个页面间共享 UI,保持状态
- 支持嵌套布局,形成布局层次
- 通过
params和searchParams访问路由参数 - 使用路由段配置优化性能
- 通过元数据 API 管理 SEO
- 使用
generateStaticParams预渲染动态路由 - TypeScript 提供完整的类型支持
理解并掌握这些概念,可以构建高性能、可维护的 Next.js 应用。
上次更新于