04. 第一个页面实战
通过实际案例,我们将创建多个页面,学习 Next.js 的文件系统路由和页面组织方式。
1. 理解文件系统路由
在 Next.js App Router 中,文件夹结构 = 路由结构。
基础规则
| 文件路径 | URL 路径 | 说明 |
|---|---|---|
app/page.tsx | / | 首页 |
app/about/page.tsx | /about | 关于页 |
app/blog/page.tsx | /blog | 博客列表页 |
app/blog/[id]/page.tsx | /blog/123 | 动态路由(稍后讲解) |
⚠️ 重要:只有包含
page.tsx的文件夹才会生成路由。其他文件(如components.tsx、utils.ts)不会生成路由。
2. 实战案例 1:个人作品集网站
需求
创建一个简单的个人作品集网站,包含:
- 首页(介绍)
- 关于页
- 作品页
- 联系页
项目结构
src/app/
├── layout.tsx # 全局布局(导航栏、页脚)
├── page.tsx # 首页 (/)
├── about/
│ └── page.tsx # 关于页 (/about)
├── portfolio/
│ └── page.tsx # 作品页 (/portfolio)
└── contact/
└── page.tsx # 联系页 (/contact)
实现步骤
步骤 1:创建全局布局(导航栏)
// src/app/layout.tsx
import Link from 'next/link'
import './globals.css'
export const metadata = {
title: '我的作品集',
description: '一个使用 Next.js 构建的个人作品集网站',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="zh-CN">
<body>
<nav className="bg-white shadow-sm">
<div className="max-w-6xl mx-auto px-4 py-4">
<div className="flex justify-between items-center">
<Link href="/" className="text-2xl font-bold">
我的作品集
</Link>
<div className="flex gap-6">
<Link href="/" className="hover:text-blue-600">
首页
</Link>
<Link href="/about" className="hover:text-blue-600">
关于
</Link>
<Link href="/portfolio" className="hover:text-blue-600">
作品
</Link>
<Link href="/contact" className="hover:text-blue-600">
联系
</Link>
</div>
</div>
</div>
</nav>
<main>{children}</main>
<footer className="bg-gray-100 mt-20 py-8 text-center">
<p className="text-gray-600">
© 2024 我的作品集. All rights reserved.
</p>
</footer>
</body>
</html>
)
}步骤 2:创建首页
// src/app/page.tsx
import Link from 'next/link'
export default function Home() {
return (
<div className="min-h-screen">
<section className="max-w-6xl mx-auto px-4 py-20">
<div className="text-center">
<h1 className="text-5xl font-bold mb-6">
欢迎来到我的作品集
</h1>
<p className="text-xl text-gray-600 mb-8 max-w-2xl mx-auto">
我是一名前端开发者,专注于使用 React 和 Next.js 构建现代化的 Web 应用。
</p>
<div className="flex gap-4 justify-center">
<Link
href="/portfolio"
className="bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700"
>
查看作品
</Link>
<Link
href="/contact"
className="border border-blue-600 text-blue-600 px-6 py-3 rounded-lg hover:bg-blue-50"
>
联系我
</Link>
</div>
</div>
<div className="mt-20 grid md:grid-cols-3 gap-8">
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-xl font-semibold mb-3">前端开发</h3>
<p className="text-gray-600">
使用 React、Next.js、TypeScript 构建高性能的 Web 应用
</p>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-xl font-semibold mb-3">UI/UX 设计</h3>
<p className="text-gray-600">
注重用户体验,设计美观且易用的界面
</p>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-xl font-semibold mb-3">全栈开发</h3>
<p className="text-gray-600">
从数据库到前端的完整解决方案
</p>
</div>
</div>
</section>
</div>
)
}步骤 3:创建关于页
// src/app/about/page.tsx
export default function About() {
return (
<div className="min-h-screen">
<section className="max-w-4xl mx-auto px-4 py-20">
<h1 className="text-4xl font-bold mb-8">关于我</h1>
<div className="prose max-w-none">
<p className="text-lg text-gray-700 mb-6">
我是一名充满热情的前端开发者,拥有 3 年的 Web 开发经验。
</p>
<h2 className="text-2xl font-semibold mt-8 mb-4">技能</h2>
<ul className="list-disc list-inside space-y-2 text-gray-700">
<li>React & Next.js</li>
<li>TypeScript</li>
<li>Tailwind CSS</li>
<li>Node.js</li>
<li>Git & GitHub</li>
</ul>
<h2 className="text-2xl font-semibold mt-8 mb-4">教育背景</h2>
<p className="text-gray-700">
计算机科学学士学位,专注于 Web 开发技术。
</p>
</div>
</section>
</div>
)
}步骤 4:创建作品页
// src/app/portfolio/page.tsx
import Link from 'next/link'
const projects = [
{
id: 1,
title: '电商平台',
description: '使用 Next.js 和 Stripe 构建的全栈电商应用',
tech: ['Next.js', 'TypeScript', 'Stripe'],
link: '#',
},
{
id: 2,
title: '任务管理应用',
description: '基于 React 的实时协作任务管理工具',
tech: ['React', 'Firebase', 'Tailwind CSS'],
link: '#',
},
{
id: 3,
title: '博客系统',
description: '使用 Next.js 和 Markdown 构建的静态博客',
tech: ['Next.js', 'MDX', 'Vercel'],
link: '#',
},
]
export default function Portfolio() {
return (
<div className="min-h-screen">
<section className="max-w-6xl mx-auto px-4 py-20">
<h1 className="text-4xl font-bold mb-12 text-center">我的作品</h1>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
{projects.map((project) => (
<div
key={project.id}
className="bg-white rounded-lg shadow-lg overflow-hidden hover:shadow-xl transition-shadow"
>
<div className="p-6">
<h3 className="text-xl font-semibold mb-2">{project.title}</h3>
<p className="text-gray-600 mb-4">{project.description}</p>
<div className="flex flex-wrap gap-2 mb-4">
{project.tech.map((tech) => (
<span
key={tech}
className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded"
>
{tech}
</span>
))}
</div>
<Link
href={project.link}
className="text-blue-600 hover:underline"
>
查看详情 →
</Link>
</div>
</div>
))}
</div>
</section>
</div>
)
}步骤 5:创建联系页
// src/app/contact/page.tsx
'use client'
import { useState } from 'react'
export default function Contact() {
const [formData, setFormData] = useState({
name: '',
email: '',
message: '',
})
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
// 这里可以添加表单提交逻辑
alert('感谢您的留言!我会尽快回复。')
setFormData({ name: '', email: '', message: '' })
}
return (
<div className="min-h-screen">
<section className="max-w-2xl mx-auto px-4 py-20">
<h1 className="text-4xl font-bold mb-8 text-center">联系我</h1>
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label htmlFor="name" className="block text-sm font-medium mb-2">
姓名
</label>
<input
type="text"
id="name"
value={formData.name}
onChange={(e) =>
setFormData({ ...formData, name: e.target.value })
}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
required
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium mb-2">
邮箱
</label>
<input
type="email"
id="email"
value={formData.email}
onChange={(e) =>
setFormData({ ...formData, email: e.target.value })
}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
required
/>
</div>
<div>
<label htmlFor="message" className="block text-sm font-medium mb-2">
留言
</label>
<textarea
id="message"
value={formData.message}
onChange={(e) =>
setFormData({ ...formData, message: e.target.value })
}
rows={6}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
required
/>
</div>
<button
type="submit"
className="w-full bg-blue-600 text-white py-3 rounded-lg hover:bg-blue-700 transition-colors"
>
发送消息
</button>
</form>
<div className="mt-12 text-center">
<p className="text-gray-600 mb-4">或者通过以下方式联系我:</p>
<div className="flex justify-center gap-6">
<a href="mailto:your@email.com" className="text-blue-600 hover:underline">
邮箱
</a>
<a href="https://github.com" className="text-blue-600 hover:underline">
GitHub
</a>
<a href="https://linkedin.com" className="text-blue-600 hover:underline">
LinkedIn
</a>
</div>
</div>
</section>
</div>
)
}3. 实战案例 2:博客列表页
需求
创建一个博客列表页,展示文章列表,点击可以查看详情。
实现
// src/app/blog/page.tsx
import Link from 'next/link'
// 模拟数据(实际项目中应该从数据库或 API 获取)
const posts = [
{
id: '1',
title: 'Next.js 入门指南',
excerpt: '学习如何使用 Next.js 构建现代化的 Web 应用',
date: '2024-01-15',
author: '张三',
},
{
id: '2',
title: 'TypeScript 最佳实践',
excerpt: '掌握 TypeScript 的类型系统和最佳实践',
date: '2024-01-10',
author: '李四',
},
{
id: '3',
title: 'Tailwind CSS 进阶技巧',
excerpt: '使用 Tailwind CSS 构建美观的界面',
date: '2024-01-05',
author: '王五',
},
]
export default function Blog() {
return (
<div className="min-h-screen">
<section className="max-w-4xl mx-auto px-4 py-20">
<h1 className="text-4xl font-bold mb-12">博客</h1>
<div className="space-y-8">
{posts.map((post) => (
<article
key={post.id}
className="bg-white p-6 rounded-lg shadow hover:shadow-lg transition-shadow"
>
<Link href={`/blog/${post.id}`}>
<h2 className="text-2xl font-semibold mb-2 hover:text-blue-600">
{post.title}
</h2>
</Link>
<p className="text-gray-600 mb-4">{post.excerpt}</p>
<div className="flex items-center gap-4 text-sm text-gray-500">
<span>{post.date}</span>
<span>•</span>
<span>{post.author}</span>
</div>
</article>
))}
</div>
</section>
</div>
)
}4. 使用 Link 组件进行导航
Next.js 提供了 Link 组件用于客户端导航,比 <a> 标签性能更好。
基础用法
import Link from 'next/link'
export default function Navigation() {
return (
<nav>
<Link href="/">首页</Link>
<Link href="/about">关于</Link>
<Link href="/blog">博客</Link>
</nav>
)
}带样式的 Link
<Link
href="/about"
className="text-blue-600 hover:text-blue-800 underline"
>
关于我们
</Link>外部链接
对于外部链接,使用普通的 <a> 标签:
<a
href="https://example.com"
target="_blank"
rel="noopener noreferrer"
>
外部链接
</a>5. 页面元数据(Metadata)
使用 metadata 导出为页面设置 SEO 信息:
// src/app/about/page.tsx
export const metadata = {
title: '关于我 - 我的作品集',
description: '了解我的背景、技能和经历',
}
export default function About() {
return <div>...</div>
}6. 实战练习:扩展作品集网站
练习任务
- 添加一个
/skills页面,展示你的技能树 - 在首页添加一个”最新作品”部分,展示最近 3 个项目
- 为每个页面添加合适的 metadata
参考实现
// src/app/skills/page.tsx
export const metadata = {
title: '技能 - 我的作品集',
description: '我的技术栈和专业技能',
}
const skills = [
{ category: '前端', items: ['React', 'Next.js', 'TypeScript', 'Tailwind CSS'] },
{ category: '后端', items: ['Node.js', 'Express', 'PostgreSQL'] },
{ category: '工具', items: ['Git', 'Docker', 'Vercel'] },
]
export default function Skills() {
return (
<div className="min-h-screen">
<section className="max-w-4xl mx-auto px-4 py-20">
<h1 className="text-4xl font-bold mb-12">技能</h1>
<div className="space-y-8">
{skills.map((skill) => (
<div key={skill.category}>
<h2 className="text-2xl font-semibold mb-4">{skill.category}</h2>
<div className="flex flex-wrap gap-3">
{skill.items.map((item) => (
<span
key={item}
className="bg-blue-100 text-blue-800 px-4 py-2 rounded-lg"
>
{item}
</span>
))}
</div>
</div>
))}
</div>
</section>
</div>
)
}7. 总结
本章我们学习了:
- ✅ 文件系统路由的基本规则
- ✅ 创建多个页面的实战案例
- ✅ 使用
Link组件进行导航 - ✅ 设置页面元数据(Metadata)
- ✅ 构建了一个完整的作品集网站
关键要点:
- 📁 文件夹结构 = 路由结构
- 🔗 使用
Link组件进行客户端导航 - 📝 每个页面都可以导出
metadata用于 SEO
下一步:在下一章,我们将深入学习路由系统,包括动态路由、查询参数等高级功能。