创建布局组件 | 自在学
创建布局组件
在前面的课堂中,我们已经了解了布局组件的基本概念。现在让我们学习如何创建专业的布局组件,包括头部导航、页脚,以及理解服务器组件和客户端组件的区别。
在 Next.js 中,几乎所有组件默认都是服务器组件,这意味着这些组件的代码和渲染过程都只会发生在服务器端。这样做的好处是可以提升网页的加载速度和安全性,因为浏览器端不会拿到不需要的信息。服务器组件适合用来展示静态内容、从数据库获取数据或者渲染不需要交互的页面。
但是,如果你希望组件具有像点击按钮、输入表单、弹窗这些实时交互能力,就必须让组件在浏览器里运行。只有客户端组件才能使用如 useState、useEffect 这些 React 的钩子,也只有客户端组件才真正处理用户的互动。每次你看到页面上有“点一下就变”的效果,基本就是客户端组件在起作用。
要创建一个客户端组件,我们则需要在文件的顶部添加 'use client' 指令。这个指令告诉 Next.js 这个组件需要在浏览器中运行。
让我们通过一个例子来理解两者的区别:
// 这是一个服务器组件(默认)
export default function ServerComponent () {
return < div >这个组件在服务器端渲染</ div >
}
'use client'
// 这是一个客户端组件
import { useState } from 'react'
export default function ClientComponent () {
const [ count , setCount ] = useState ( 0 )
return (
< button onClick = {() => setCount (count + 1 )}>
点击次数: {count}
</ button >
)
}
最佳实践是默认使用服务器组件,只在需要交互功能时才使用客户端组件。这样可以最大化性能,同时保持代码的简洁。
创建头部导航组件
首先,在项目根目录创建 components 文件夹,然后创建 Header.tsx,它应该是这样的目录结构 components/layout/Header.tsx,注意components是与app同级的。
然后我们将以下代码写入:
import Link from 'next/link'
export default function Header () {
return (
< header className = "bg-white shadow-sm sticky top-0 z-50" >
< nav className = "container mx-auto px-4" >
< div className = "flex items-center justify-between h-16" >
< Link href = "/" className = "text-2xl font-bold text-gray-900" >
Free Education
这个组件使用了 sticky top-0 来让导航栏在滚动时固定在顶部。z-50 确保导航栏在其他内容之上。
因为在刚才的代码中,我们没有添加移动端的响应式菜单,所以现在让我们添加移动端的响应式菜单。这需要交互功能,所以我们需要将刚才的组件变为一个客户端组件,
还记得怎么做嘛?对,就是在文件的顶部添加 'use client' 指令:
'use client'
import { useState } from 'react'
import Link from 'next/link'
export default function Header () {
const [ mobileMenuOpen , setMobileMenuOpen ] = useState ( false )
return (
< header className = "bg-white shadow-sm sticky top-0 z-50" >
< nav className = "container mx-auto px-4"
这里我们使用了 useState 来管理移动端菜单的打开/关闭状态。注意文件开头的 'use client' 指令,这是必需的,因为我们需要使用 React 的 useState Hook。
创建页脚组件
页脚通常包含链接、版权信息等。让我们创建一个页脚组件,它的路径应该是 components/layout/Footer.tsx,然后写入以下代码:
import Link from 'next/link'
export default function Footer () {
return (
< footer className = "bg-gray-900 text-white" >
< div className = "container mx-auto px-4 py-12" >
< div className = "grid md:grid-cols-4 gap-8" >
< div >
< h3 className = "text-xl font-bold mb-4" >Free Education</ h3
这是一个服务器组件,因为它不需要任何交互功能。页脚使用了网格布局,在不同屏幕尺寸下自动调整列数。
更新根布局
现在让我们更新我们的根布局,将头部和页脚组件集成进去。我们现在更新 app/layout.tsx 文件,将头部和页脚组件集成进去:
import Header from '@/components/layout/Header'
import Footer from '@/components/layout/Footer'
import type { Metadata } from 'next'
import './globals.css'
export const metadata : Metadata = {
title: 'Free Education - 免费教育资源平台' ,
description: '为每个人提供免费、高质量的教育资源' ,
}
export default function RootLayout ({
children ,
}
这里我们使用了 flex flex-col 和 flex-grow 来确保页脚始终在页面底部,即使内容不够多时也是如此。
组件组合的最佳实践
在构建布局时,我们应该遵循一些最佳实践:
单一职责原则 :每个组件应该只负责一个功能。Header 负责导航,Footer 负责页脚信息,Logo 负责品牌标识。
可复用性 :组件应该尽可能通用,可以在不同地方复用。例如,Logo 组件可以在 Header 和 Footer 中都使用。
组合优于继承 :通过组合小组件来构建大组件,而不是创建复杂的单一组件。
让我们创建一个可复用的按钮组件来演示这个概念:
// components/Button.tsx
import { ReactNode } from 'react'
interface ButtonProps {
children : ReactNode
variant ?: 'primary' | 'secondary' | 'outline'
size ?: 'sm' | 'md' | 'lg'
onClick ?: () => void
}
export default function Button ({
children ,
这是一个客户端组件(虽然这里没有使用状态,但按钮通常需要交互)。它使用了 TypeScript 接口来定义属性类型,提供了不同的变体和尺寸选项。
实践:完善布局
让我们更新 Header 组件,使用新的 Button 组件:
'use client'
import { useState } from 'react'
import Link from 'next/link'
import Logo from '@/components/Logo'
import Button from '@/components/Button'
export default function Header () {
const [ mobileMenuOpen , setMobileMenuOpen ] = useState ( false )
return (
< header className
下一步
在这一部分中,我们学习了如何创建布局组件,理解了服务器组件和客户端组件的区别,并创建了可复用的组件。在下一部分,我们将学习如何构建首页的 Hero 区域,创建一个吸引人的首屏体验。
你已经掌握了布局组件的创建方法!现在你可以构建专业的导航栏、页脚和可复用的组件。这些技能将帮助你创建一致、美观的用户界面。
</ Link >
< div className = "hidden md:flex items-center gap-6" >
< Link href = "/" className = "text-gray-600 hover:text-gray-900" >
首页
</ Link >
< Link href = "/courses" className = "text-gray-600 hover:text-gray-900" >
课程
</ Link >
< Link href = "/about" className = "text-gray-600 hover:text-gray-900" >
关于
</ Link >
</ div >
< button className = "bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600 transition-colors" >
登录
</ button >
</ div >
</ nav >
</ header >
)
}
>
< div className = "flex items-center justify-between h-16" >
< Link href = "/" className = "text-2xl font-bold text-gray-900" >
Free Education
</ Link >
{ /* 桌面端导航 */ }
< div className = "hidden md:flex items-center gap-6" >
< Link href = "/" className = "text-gray-600 hover:text-gray-900" >
首页
</ Link >
< Link href = "/courses" className = "text-gray-600 hover:text-gray-900" >
课程
</ Link >
< Link href = "/about" className = "text-gray-600 hover:text-gray-900" >
关于
</ Link >
</ div >
{ /* 移动端菜单按钮 */ }
< button
onClick = {() => setMobileMenuOpen ( ! mobileMenuOpen)}
className = "md:hidden p-2"
>
< svg className = "w-6 h-6" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M4 6h16M4 12h16M4 18h16" />
</ svg >
</ button >
{ /* 桌面端登录按钮 */ }
< button className = "hidden md:block bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600 transition-colors" >
登录
</ button >
</ div >
{ /* 移动端菜单 */ }
{mobileMenuOpen && (
< div className = "md:hidden border-t" >
< div className = "flex flex-col gap-4 py-4" >
< Link href = "/" className = "text-gray-600 hover:text-gray-900" >
首页
</ Link >
< Link href = "/courses" className = "text-gray-600 hover:text-gray-900" >
课程
</ Link >
< Link href = "/about" className = "text-gray-600 hover:text-gray-900" >
关于
</ Link >
< button className = "bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600 transition-colors w-full" >
登录
</ button >
</ div >
</ div >
)}
</ nav >
</ header >
)
}
>
< p className = "text-gray-400" >
为每个人提供免费、高质量的教育资源
</ p >
</ div >
< div >
< h4 className = "font-semibold mb-4" >快速链接</ h4 >
< ul className = "space-y-2" >
< li >
< Link href = "/" className = "text-gray-400 hover:text-white" >
首页
</ Link >
</ li >
< li >
< Link href = "/courses" className = "text-gray-400 hover:text-white" >
课程
</ Link >
</ li >
< li >
< Link href = "/about" className = "text-gray-400 hover:text-white" >
关于我们
</ Link >
</ li >
</ ul >
</ div >
< div >
< h4 className = "font-semibold mb-4" >资源</ h4 >
< ul className = "space-y-2" >
< li >
< Link href = "/blog" className = "text-gray-400 hover:text-white" >
博客
</ Link >
</ li >
< li >
< Link href = "/faq" className = "text-gray-400 hover:text-white" >
常见问题
</ Link >
</ li >
</ ul >
</ div >
< div >
< h4 className = "font-semibold mb-4" >联系我们</ h4 >
< ul className = "space-y-2" >
< li className = "text-gray-400" >邮箱: info@freeeducation.com</ li >
< li className = "text-gray-400" >电话: +86 123 456 7890</ li >
</ ul >
</ div >
</ div >
< div className = "border-t border-gray-800 mt-8 pt-8 text-center text-gray-400" >
< p > © 2025 Free Education. All rights reserved.</ p >
</ div >
</ div >
</ footer >
)
}
:
{
children : React . ReactNode
}) {
return (
< html lang = "zh-CN" >
< body className = "min-h-screen flex flex-col" >
< Header />
< main className = "flex-grow" >
{children}
</ main >
< Footer />
</ body >
</ html >
)
}
variant = 'primary' ,
size = 'md' ,
onClick ,
} : ButtonProps ) {
const baseStyles = 'font-semibold rounded-lg transition-colors'
const variants = {
primary: 'bg-blue-500 text-white hover:bg-blue-600' ,
secondary: 'bg-gray-500 text-white hover:bg-gray-600' ,
outline: 'border-2 border-blue-500 text-blue-500 hover:bg-blue-50' ,
}
const sizes = {
sm: 'px-3 py-1.5 text-sm' ,
md: 'px-4 py-2' ,
lg: 'px-6 py-3 text-lg' ,
}
return (
< button
onClick = {onClick}
className = { `${ baseStyles } ${ variants [ variant ] } ${ sizes [ size ] }` }
>
{children}
</ button >
)
}
=
"bg-white shadow-sm sticky top-0 z-50"
>
< nav className = "container mx-auto px-4" >
< div className = "flex items-center justify-between h-16" >
< Logo />
< div className = "hidden md:flex items-center gap-6" >
< Link href = "/" className = "text-gray-600 hover:text-gray-900" >
首页
</ Link >
< Link href = "/courses" className = "text-gray-600 hover:text-gray-900" >
课程
</ Link >
< Link href = "/about" className = "text-gray-600 hover:text-gray-900" >
关于
</ Link >
</ div >
< div className = "hidden md:block" >
< Button >登录</ Button >
</ div >
< button
onClick = {() => setMobileMenuOpen ( ! mobileMenuOpen)}
className = "md:hidden p-2"
>
< svg className = "w-6 h-6" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M4 6h16M4 12h16M4 18h16" />
</ svg >
</ button >
</ div >
{mobileMenuOpen && (
< div className = "md:hidden border-t" >
< div className = "flex flex-col gap-4 py-4" >
< Link href = "/" className = "text-gray-600 hover:text-gray-900" >
首页
</ Link >
< Link href = "/courses" className = "text-gray-600 hover:text-gray-900" >
课程
</ Link >
< Link href = "/about" className = "text-gray-600 hover:text-gray-900" >
关于
</ Link >
< Button variant = "primary" size = "md" >登录</ Button >
</ div >
</ div >
)}
</ nav >
</ header >
)
}