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.tsxutils.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>
  )
}

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
  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. 实战练习:扩展作品集网站

练习任务

  1. 添加一个 /skills 页面,展示你的技能树
  2. 在首页添加一个”最新作品”部分,展示最近 3 个项目
  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. 总结

本章我们学习了:

  1. ✅ 文件系统路由的基本规则
  2. ✅ 创建多个页面的实战案例
  3. ✅ 使用 Link 组件进行导航
  4. ✅ 设置页面元数据(Metadata)
  5. ✅ 构建了一个完整的作品集网站

关键要点

  • 📁 文件夹结构 = 路由结构
  • 🔗 使用 Link 组件进行客户端导航
  • 📝 每个页面都可以导出 metadata 用于 SEO

下一步:在下一章,我们将深入学习路由系统,包括动态路由、查询参数等高级功能。