在前面的课程中,我们已经初步接触了页面和布局的概念。在这一节课中,我们将探索页面和布局的方方面面,学习如何创建嵌套布局,如何使用模板,以及如何在不同场景下灵活运用这些特性。理解页面和布局的关系,就像理解建筑的结构一样重要——它们共同构成了我们应用的骨架。

页面组件是 Next.js 应用的基本构建块。每个 page.tsx 文件导出的组件都会成为一个可访问的路由。但页面组件不仅仅是简单的 React 组件,它们具有一些特殊的特性。
首先,页面组件默认是服务器组件。这意味着它们可以在服务器端运行,直接访问数据库、文件系统等服务器资源,而无需通过 API。其次,页面组件可以接收特殊的 props。让我们看看页面组件可以接收哪些参数:
|// app/products/[id]/page.tsx export default async function ProductPage({ params, searchParams, }: { params: Promise<{ id: string }>; searchParams: Promise<{ [key: string]: string | string[] | undefined }>; }) { const { id } = await params; const { color, size } = await searchParams; return ( <div> <h1>产品 {id}</h1> {color && <p>颜色: {color}</p>} {size && <p>尺寸: {size}</p>} </div> ); }
在这个例子中,params 包含动态路由参数,searchParams 包含 URL 查询参数。注意在 Next.js 15+ 中,这两个参数都是 Promise,我们需要使用 await 来获取它们的值。
让我们逐步理解这个例子。首先,我们定义了一个异步函数 ProductPage。这个函数接收一个对象作为参数,这个对象包含 params 和 searchParams。
然后,我们使用 await 来获取 params 和 searchParams 的值。params 是一个对象,包含了动态路由段的值。在这个例子中,id 是从 URL 路径中提取的。
searchParams 也是一个对象,包含了 URL 查询字符串的参数。比如,如果 URL 是 /products/123?color=red&size=large,那么 color 的值是 "red",size 的值是 "large"。
最后,我们使用这些参数来渲染页面内容。如果查询参数存在,我们就显示它们。
布局组件是 Next.js 中一个强大的特性,它允许我们在多个页面之间共享 UI 结构。与页面组件不同,布局组件不会创建新的路由,而是为当前路由及其所有子路由提供共享的 UI。
理解布局组件的工作机制很重要。当我们访问一个页面时,Next.js 会从根布局开始,逐层向下渲染所有相关的布局,最后渲染页面组件。这就像洋葱的层次结构,每一层都包裹着内层。
让我们通过一个例子来理解这个过程。假设我们有这样的结构:
|app/ ├── layout.tsx # 根布局 ├── page.tsx # 首页 └── dashboard/ ├── layout.tsx # 仪表板布局 ├── page.tsx # 仪表板首页 └── settings/ └── page.tsx # 设置页面
当我们访问 /dashboard/settings 时,Next.js 会按照以下顺序渲染:
app/layout.tsx(根布局)app/dashboard/layout.tsx(仪表板布局)app/dashboard/settings/page.tsx(设置页面)这些组件会嵌套在一起,形成最终的 DOM 结构。让我们创建一个实际的例子。首先,我们创建根布局:
|// app/layout.tsx export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="zh"> <body> <header> <nav> <a href=
这个布局定义了整个应用的基本结构,包括页头、主内容和页脚。所有页面都会包含这些元素。 然后,我们为仪表板创建一个专门的布局:
|// app/dashboard/layout.tsx export default function DashboardLayout({ children, }: { children: React.ReactNode; }) { return ( <div className="dashboard-container"> <aside> <h2>仪表板菜单</h2> <ul> <li
这个布局为仪表板页面添加了侧边栏菜单。当用户访问 /dashboard 或 /dashboard/settings 时,他们会看到根布局的导航栏和页脚,以及仪表板布局的侧边栏,最后是具体的页面内容。
嵌套布局在实际项目中有很多应用场景。让我们看看几个常见的例子。
第一个例子是创建一个需要认证的页面区域。我们可以创建一个布局来检查用户是否已登录:
|// app/protected/layout.tsx import { redirect } from 'next/navigation'; export default async function ProtectedLayout({ children, }: { children: React.ReactNode; }) { const isAuthenticated = await checkAuth(); if (!isAuthenticated) { redirect('/login'
在这个例子中,checkAuth 是一个假设的函数,用于检查用户是否已登录。如果用户未登录,我们使用 redirect 函数将他们重定向到登录页面。如果用户已登录,我们渲染子组件。
这样,所有放在 app/protected 目录下的页面都会自动受到保护,无需在每个页面中重复检查认证状态。
第二个例子是为不同的设备创建不同的布局。我们可以使用路由组来实现:
|// app/(mobile)/layout.tsx export default function MobileLayout({ children, }: { children: React.ReactNode; }) { return ( <div className="mobile-container"> {children} </div> ); }
|// app/(desktop)/layout.tsx export default function DesktopLayout({ children, }: { children: React.ReactNode; }) { return ( <div className="desktop-container"> {children} </div> ); }
这样,我们可以为移动端和桌面端使用不同的布局,同时保持 URL 结构不变。
模板组件与布局组件类似,但有一个重要的区别:模板组件在每次导航时都会重新挂载,而布局组件会保持状态。 让我们通过一个例子来理解这个区别。首先,我们创建一个布局组件:
|// app/layout.tsx 'use client'; import { useState } from 'react'; export default function RootLayout({ children, }: { children: React.ReactNode; }) { const [count, setCount] = useState(0); return (
在这个布局中,我们使用 useState 来维护一个计数器。当我们在页面之间导航时,这个计数器的值会保持不变,因为布局组件不会重新挂载。
现在,让我们创建一个模板组件:
|// app/template.tsx 'use client'; import { useState } from 'react'; export default function Template({ children, }: { children: React.ReactNode; }) { const [count, setCount] = useState(0); return (
如果我们使用这个模板组件,每次导航时,计数器都会重置为 0,因为模板组件会重新挂载。 模板组件适用于需要在每次导航时重置状态或执行某些操作的场景。比如,我们可能想在每次导航时播放一个过渡动画:
|// app/template.tsx 'use client'; import { useEffect, useState } from 'react'; export default function Template({ children, }: { children: React.ReactNode; }) { const [isAnimating, setIsAnimating] = useState(false); useEffect
在这个例子中,每次 children 改变时(即导航发生时),我们都会触发一个淡入动画。
在实际项目中,我们通常会同时使用布局和模板。布局用于共享的 UI 结构,模板用于需要在每次导航时重置的功能。 让我们看一个完整的例子:
|// app/layout.tsx export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html> <body> <header> <nav>导航栏</nav> </header> {children}
|// app/template.tsx 'use client'; export default function Template({ children, }: { children: React.ReactNode; }) { return ( <div className="page-transition"> {children} </div> ); }
在这个结构中,布局提供了页头和页脚,这些在导航时保持不变。模板提供了页面过渡效果,每次导航时都会重新应用。
页面组件可以导出一些特殊的函数和对象,Next.js 会使用它们来配置页面的行为。
第一个是 generateMetadata 函数,它允许你为每个页面动态生成元数据(如 <title>、<meta> 标签内容等)。你可以在这个函数中根据页面参数(例如 URL 路由参数)、请求内容、甚至从数据库或 API 获取的数据来生成页面的标题、描述、Open Graph 属性等信息。
这使得每个页面都可以自定义自己的 SEO 和展示信息。例如,在产品详情页,可以根据产品的 id 动态设置对应的元数据:
|// app/products/[id]/page.tsx export async function generateMetadata({ params, }: { params: Promise<{ id: string }>; }) { const { id } = await params; const product = await fetchProduct(id); return { title: product.name, description: product.description,
generateMetadata 函数接收 params 和 searchParams 作为参数,返回一个元数据对象。这个函数会在服务器端运行,可以访问数据库等服务器资源。
第二个是 generateStaticParams 函数。这个函数的作用是为动态路由(例如 [id] 这样的参数化路由)预先生成所有可能的路由参数,从而实现静态生成(SSG)。在构建时,Next.js 会调用 generateStaticParams,获取所有需要静态构建的参数,然后为这些参数生成对应的静态页面。
具体来说,generateStaticParams 是一个异步函数,通常会从数据库、API 或本地数据源中读取所有实体(比如产品列表、文章列表等),然后返回一个包含所有参数对象的数组。
例如,在商品详情页的动态路由场景下,该函数会获取所有商品的 id,并返回每个 id 组成的对象数组。Next.js 会据此为每个商品生成一个静态详情页,用户访问这些页面时,能够直接获得静态生成的内容,无需服务器实时渲染,提升了页面打开速度和 SEO 效果。
需要注意的是,generateStaticParams 只在静态生成的情况下生效,如果页面使用了强制动态渲染(如 dynamic = 'force-dynamic'),则不会调用此函数。
|// app/products/[id]/page.tsx export async function generateStaticParams() { const products = await fetchAllProducts(); return products.map((product) => ({ id: product.id, })); } export default async function ProductPage({ params, }: { params: Promise
generateStaticParams 函数返回一个数组,包含所有需要静态生成的路由参数。Next.js 会在构建时为这些路由生成静态页面。
第三个是 dynamic 和 dynamicParams 配置:
|// app/products/[id]/page.tsx export const dynamic = 'force-dynamic'; export const dynamicParams = true; export default async function ProductPage({ params, }: { params: Promise<{ id: string }>; }) { const { id } = await params;
dynamic 可以设置为 'force-dynamic' 或 'force-static',用于强制页面使用动态或静态渲染。dynamicParams 控制是否允许动态参数,如果设置为 false,访问未在 generateStaticParams 中定义的参数会返回 404。
在 Next.js 中,布局组件(如 app/layout.tsx)不仅仅用于结构化页面,还可以通过导出一些特殊的配置来增强页面的能力。
其中最常用也是最重要的配置是 metadata 对象。通过在布局组件中导出 metadata,你可以为整个网站或某个分区统一设置默认的网页标题、描述、以及 SEO 相关的元信息。
例如你可以指定标题模板、默认标题、网站描述等。这样做的好处是:所有子页面如果没有单独定义 metadata,就会继承这里的设置;如果子页面有自己的 metadata,则只会覆盖各自的内容。
|// app/layout.tsx export const metadata = { title: { template: '%s | 我的网站', default: '我的网站', }, description: '这是我的网站', }; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return (
在这个例子中,我们定义了默认的标题模板和描述。子页面可以通过导出自己的 metadata 来覆盖这些值。标题模板中的 %s 会被子页面的标题替换。
有时候,我们可能需要根据某些条件来渲染不同的布局。我们可以通过条件渲染来实现:
|// app/layout.tsx export default async function RootLayout({ children, }: { children: React.ReactNode; }) { const user = await getCurrentUser(); if (user?.isAdmin) { return ( <html> <body> <AdminLayout
在这个例子中,我们根据用户是否是管理员来渲染不同的布局。管理员用户会看到管理布局,普通用户会看到默认布局。
虽然布局组件(layout)默认是服务器组件,因此所有代码在服务器端渲染,但在实际开发中,我们经常会遇到需要在布局中集成交互功能的需求。这时,完全可以将客户端组件嵌入到布局组件中使用。 通过这种方式,我们能够在页面的整体框架(如导航栏、侧边栏等)中实现状态切换、高亮显示菜单项、响应用户操作等前端交互行为,从而提升用户体验。 例如,可以在布局中引入一个“客户端导航”组件,用于根据当前路由动态高亮菜单项,或者添加顶部通知栏、主题切换按钮等需要在客户端渲染和交互的组件。
|// app/layout.tsx import ClientNavigation from './ClientNavigation'; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html> <body> <ClientNavigation /> <main>{children}</main
|// app/ClientNavigation.tsx 'use client'; import { usePathname } from 'next/navigation'; export default function ClientNavigation() { const pathname = usePathname(); return ( <nav> <a href="/" className={pathname === '/' ? 'active' : ''}>
在这个例子中,我们创建了一个客户端导航组件,它可以根据当前路径高亮显示对应的链接。这个组件使用了 usePathname hook,这是 Next.js 提供的客户端 hook,我们会在后面的学习中看到它,现在先让我们把它暂时放到一边。
布局组件会在每次请求时渲染,所以我们应该注意性能优化。一个常见的优化是使用 React 的 cache 函数来缓存数据:
|// app/layout.tsx import { cache } from 'react'; const getSiteConfig = cache(async () => { return await fetchSiteConfig(); }); export default async function RootLayout({ children, }: { children: React.ReactNode; }) {
在这个例子中,我们使用 cache 函数来包装数据获取函数。这样,即使布局组件被多次渲染,数据也只会获取一次。
在这一部分,我们探索了页面和布局的各个知识点。我们学习了页面组件如何接收参数,布局组件如何嵌套工作,模板组件与布局组件的区别,以及如何在实际项目中灵活运用这些特性。 页面和布局是 Next.js 应用的基础结构,理解它们的工作机制对于构建复杂的应用至关重要。通过合理使用布局和模板,我们可以创建既灵活又高效的应用结构。
在下一节课的学习中,我们将了解服务器组件和客户端组件的区别,这是 Next.js 13+ 最重要的特性之一。