Skip to content

第11章:基础实战项目(新手必练)

实战 1:个人博客网站(Next.js 14 + App Router)

在本章中,我们将创建一个完整的个人博客网站,使用 Next.js 14 和 App Router,涵盖从项目初始化到部署的全过程。通过这个实战项目,你将巩固前面学习的所有知识点,包括路由系统、Server Components、数据获取、样式解决方案等。

11.1 需求分析与项目初始化

11.1.1 需求分析

我们要创建一个个人博客网站,包含以下功能:

  • 首页:展示最新文章列表
  • 文章列表页:分页展示所有文章
  • 文章详情页:展示文章内容
  • 关于页:展示个人信息
  • 搜索功能:根据关键词搜索文章
  • 分类筛选:根据分类浏览文章
  • SEO 优化:添加元数据,提升搜索引擎排名
  • 响应式设计:适配不同设备屏幕

11.1.2 项目初始化

步骤 1:创建 Next.js 项目

bash
npx create-next-app@latest my-blog --tailwind --eslint --app --src-dir

步骤 2:安装依赖

bash
cd my-blog
npm install

步骤 3:项目结构规划

src/
  app/
    layout.js       # 全局布局
    page.js         # 首页
    about/          # 关于页
      page.js
    blog/           # 博客相关路由
      page.js       # 文章列表页
      [slug]/       # 文章详情页
        page.js
    api/            # API 路由
      posts/        # 文章相关 API
        route.js
  components/       # 组件
    Header.js       # 头部组件
    Footer.js       # 底部组件
    PostCard.js     # 文章卡片组件
    SearchBar.js    # 搜索组件
  lib/              # 工具函数
    posts.js        # 文章数据处理
  styles/           # 样式文件
    globals.css     # 全局样式

11.2 路由与布局搭建

11.2.1 全局布局

javascript
// src/app/layout.js
import Header from '@/components/Header';
import Footer from '@/components/Footer';
import './globals.css';

export const metadata = {
  title: 'My Blog',
  description: 'A personal blog built with Next.js 14',
};

export default function RootLayout({ children }) {
  return (
    <html lang="zh-CN">
      <body>
        <Header />
        <main className="container mx-auto px-4 py-8">
          {children}
        </main>
        <Footer />
      </body>
    </html>
  );
}

11.2.2 头部组件

javascript
// src/components/Header.js
import Link from 'next/link';
import SearchBar from './SearchBar';

export default function Header() {
  return (
    <header className="bg-gray-800 text-white py-4">
      <div className="container mx-auto px-4 flex justify-between items-center">
        <div className="flex items-center space-x-4">
          <Link href="/" className="text-2xl font-bold">
            My Blog
          </Link>
          <nav className="hidden md:flex space-x-6">
            <Link href="/" className="hover:text-gray-300">
              首页
            </Link>
            <Link href="/blog" className="hover:text-gray-300">
              文章
            </Link>
            <Link href="/about" className="hover:text-gray-300">
              关于
            </Link>
          </nav>
        </div>
        <SearchBar />
      </div>
    </header>
  );
}

11.2.3 底部组件

javascript
// src/components/Footer.js
export default function Footer() {
  const currentYear = new Date().getFullYear();
  return (
    <footer className="bg-gray-800 text-white py-8 mt-12">
      <div className="container mx-auto px-4 text-center">
        <p>&copy; {currentYear} My Blog. All rights reserved.</p>
      </div>
    </footer>
  );
}

11.3 数据获取与展示

11.3.1 模拟文章数据

javascript
// src/lib/posts.js
export const posts = [
  {
    id: 1,
    title: 'Next.js 14 新特性详解',
    slug: 'nextjs-14-features',
    content: 'Next.js 14 带来了许多新特性,包括 App Router、Server Components、Turbo 打包等...',
    category: '前端框架',
    date: '2024-01-15',
    excerpt: '探索 Next.js 14 的核心新特性,了解如何利用这些特性提升开发效率。',
  },
  {
    id: 2,
    title: 'React Server Components 实战指南',
    slug: 'react-server-components',
    content: 'Server Components 是 React 18 引入的新特性,在 Next.js 13+ 中得到了广泛应用...',
    category: 'React',
    date: '2024-01-10',
    excerpt: '学习如何在 Next.js 中使用 Server Components,提升应用性能。',
  },
  {
    id: 3,
    title: 'Tailwind CSS 最佳实践',
    slug: 'tailwind-css-best-practices',
    content: 'Tailwind CSS 是一个实用优先的 CSS 框架,通过类名组合来构建界面...',
    category: 'CSS',
    date: '2024-01-05',
    excerpt: '掌握 Tailwind CSS 的最佳实践,写出更简洁、可维护的样式代码。',
  },
];

export function getPosts() {
  return posts;
}

export function getPostBySlug(slug) {
  return posts.find(post => post.slug === slug);
}

export function getPostsByCategory(category) {
  return posts.filter(post => post.category === category);
}

export function searchPosts(query) {
  const lowerQuery = query.toLowerCase();
  return posts.filter(post => 
    post.title.toLowerCase().includes(lowerQuery) ||
    post.content.toLowerCase().includes(lowerQuery) ||
    post.excerpt.toLowerCase().includes(lowerQuery)
  );
}

11.3.2 首页实现

javascript
// src/app/page.js
import PostCard from '@/components/PostCard';
import { getPosts } from '@/lib/posts';

export default function Home() {
  const posts = getPosts();
  const latestPosts = posts.slice(0, 3);

  return (
    <div>
      <h1 className="text-4xl font-bold mb-8">欢迎来到我的博客</h1>
      <h2 className="text-2xl font-semibold mb-4">最新文章</h2>
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
        {latestPosts.map(post => (
          <PostCard key={post.id} post={post} />
        ))}
      </div>
      <div className="mt-8 text-center">
        <a href="/blog" className="text-blue-600 hover:underline text-lg">
          查看所有文章
        </a>
      </div>
    </div>
  );
}

11.3.3 文章列表页

javascript
// src/app/blog/page.js
import PostCard from '@/components/PostCard';
import { getPosts } from '@/lib/posts';

export default function BlogPage() {
  const posts = getPosts();

  return (
    <div>
      <h1 className="text-4xl font-bold mb-8">文章列表</h1>
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
        {posts.map(post => (
          <PostCard key={post.id} post={post} />
        ))}
      </div>
    </div>
  );
}

11.3.4 文章详情页

javascript
// src/app/blog/[slug]/page.js
import { getPostBySlug } from '@/lib/posts';

export async function generateMetadata({ params }) {
  const post = getPostBySlug(params.slug);
  if (!post) {
    return {
      title: '文章未找到',
      description: '抱歉,您请求的文章不存在。',
    };
  }
  return {
    title: post.title,
    description: post.excerpt,
  };
}

export default function PostPage({ params }) {
  const post = getPostBySlug(params.slug);
  if (!post) {
    return (
      <div>
        <h1 className="text-4xl font-bold mb-4">文章未找到</h1>
        <p>抱歉,您请求的文章不存在。</p>
      </div>
    );
  }

  return (
    <div>
      <h1 className="text-4xl font-bold mb-4">{post.title}</h1>
      <div className="flex items-center text-gray-600 mb-6">
        <span className="mr-4">{post.date}</span>
        <span className="bg-gray-200 px-3 py-1 rounded-full text-sm">
          {post.category}
        </span>
      </div>
      <div className="prose max-w-none">
        <p>{post.content}</p>
      </div>
    </div>
  );
}

11.3.5 关于页

javascript
// src/app/about/page.js
export default function AboutPage() {
  return (
    <div>
      <h1 className="text-4xl font-bold mb-8">关于我</h1>
      <div className="bg-gray-100 p-6 rounded-lg">
        <h2 className="text-2xl font-semibold mb-4">个人介绍</h2>
        <p className="mb-4">
          你好,我是一名前端开发者,专注于 React 和 Next.js 技术栈。
          我热爱分享技术知识,希望通过这个博客记录我的学习心得和项目经验。
        </p>
        <h2 className="text-2xl font-semibold mb-4">技术栈</h2>
        <ul className="list-disc pl-5 mb-4">
          <li>前端:React、Next.js、Tailwind CSS</li>
          <li>后端:Node.js、Express、MongoDB</li>
          <li>工具:Git、VS Code、Vercel</li>
        </ul>
        <h2 className="text-2xl font-semibold mb-4">联系方式</h2>
        <p>邮箱:example@example.com</p>
      </div>
    </div>
  );
}

11.4 搜索功能实现

11.4.1 搜索组件

javascript
// src/components/SearchBar.js
'use client';

import { useState } from 'react';
import { useRouter } from 'next/navigation';

export default function SearchBar() {
  const [query, setQuery] = useState('');
  const router = useRouter();

  const handleSearch = (e) => {
    e.preventDefault();
    if (query.trim()) {
      router.push(`/blog?search=${encodeURIComponent(query)}`);
    }
  };

  return (
    <form onSubmit={handleSearch} className="flex">
      <input
        type="text"
        placeholder="搜索文章..."
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        className="px-4 py-2 rounded-l-md focus:outline-none"
      />
      <button
        type="submit"
        className="bg-blue-600 text-white px-4 py-2 rounded-r-md hover:bg-blue-700"
      >
        搜索
      </button>
    </form>
  );
}

11.4.2 搜索结果处理

javascript
// src/app/blog/page.js
import { useSearchParams } from 'next/navigation';
import PostCard from '@/components/PostCard';
import { getPosts, searchPosts } from '@/lib/posts';

export default function BlogPage() {
  const searchParams = useSearchParams();
  const searchQuery = searchParams.get('search');

  let posts;
  if (searchQuery) {
    posts = searchPosts(searchQuery);
  } else {
    posts = getPosts();
  }

  return (
    <div>
      <h1 className="text-4xl font-bold mb-8">
        {searchQuery ? `搜索结果: ${searchQuery}` : '文章列表'}
      </h1>
      {posts.length === 0 ? (
        <p>没有找到匹配的文章。</p>
      ) : (
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
          {posts.map(post => (
            <PostCard key={post.id} post={post} />
          ))}
        </div>
      )}
    </div>
  );
}

11.5 元数据配置与 SEO 优化

11.5.1 全局元数据

javascript
// src/app/layout.js
export const metadata = {
  title: {
    default: 'My Blog',
    template: '%s | My Blog',
  },
  description: 'A personal blog built with Next.js 14',
  keywords: ['Next.js', 'React', '前端开发', '技术博客'],
  authors: [
    {
      name: 'Your Name',
      url: 'https://yourwebsite.com',
    },
  ],
  openGraph: {
    title: 'My Blog',
    description: 'A personal blog built with Next.js 14',
    url: 'https://yourwebsite.com',
    siteName: 'My Blog',
    images: [
      {
        url: 'https://yourwebsite.com/og-image.jpg',
        width: 1200,
        height: 630,
        alt: 'My Blog',
      },
    ],
  },
  twitter: {
    card: 'summary_large_image',
    title: 'My Blog',
    description: 'A personal blog built with Next.js 14',
    images: ['https://yourwebsite.com/og-image.jpg'],
  },
};

11.5.2 动态元数据

javascript
// src/app/blog/[slug]/page.js
export async function generateMetadata({ params }) {
  const post = getPostBySlug(params.slug);
  if (!post) {
    return {
      title: '文章未找到',
      description: '抱歉,您请求的文章不存在。',
    };
  }
  return {
    title: post.title,
    description: post.excerpt,
    keywords: [post.category, 'Next.js', 'React'],
    openGraph: {
      title: post.title,
      description: post.excerpt,
      url: `https://yourwebsite.com/blog/${post.slug}`,
      images: [
        {
          url: 'https://yourwebsite.com/og-image.jpg',
          width: 1200,
          height: 630,
          alt: post.title,
        },
      ],
    },
  };
}

11.6 样式优化与响应式适配

11.6.1 全局样式

css
/* src/app/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
  --foreground-rgb: 0, 0, 0;
  --background-rgb: 255, 255, 255;
}

@media (prefers-color-scheme: dark) {
  :root {
    --foreground-rgb: 255, 255, 255;
    --background-rgb: 0, 0, 0;
  }
}

body {
  color: rgb(var(--foreground-rgb));
  background: rgb(var(--background-rgb));
}

.prose {
  max-width: 100%;
}

/* 自定义动画 */
@keyframes fadeIn {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.fade-in {
  animation: fadeIn 0.5s ease-in-out;
}

11.6.2 响应式设计

javascript
// src/components/Header.js
import Link from 'next/link';
import SearchBar from './SearchBar';
import { useState } from 'react';

export default function Header() {
  const [isMenuOpen, setIsMenuOpen] = useState(false);

  return (
    <header className="bg-gray-800 text-white py-4">
      <div className="container mx-auto px-4 flex justify-between items-center">
        <div className="flex items-center space-x-4">
          <Link href="/" className="text-2xl font-bold">
            My Blog
          </Link>
          {/* 桌面导航 */}
          <nav className="hidden md:flex space-x-6">
            <Link href="/" className="hover:text-gray-300">
              首页
            </Link>
            <Link href="/blog" className="hover:text-gray-300">
              文章
            </Link>
            <Link href="/about" className="hover:text-gray-300">
              关于
            </Link>
          </nav>
          {/* 移动端菜单按钮 */}
          <button
            className="md:hidden"
            onClick={() => setIsMenuOpen(!isMenuOpen)}
          >
            <svg
              xmlns="http://www.w3.org/2000/svg"
              fill="none"
              viewBox="0 0 24 24"
              strokeWidth={1.5}
              stroke="currentColor"
              className="w-6 h-6"
            >
              <path
                strokeLinecap="round"
                strokeLinejoin="round"
                d={isMenuOpen
                  ? "M6 18L18 6M6 6l12 12"
                  : "M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
                }
              />
            </svg>
          </button>
        </div>
        {/* 桌面搜索 */}
        <div className="hidden md:block">
          <SearchBar />
        </div>
      </div>
      {/* 移动端菜单 */}
      {isMenuOpen && (
        <div className="md:hidden container mx-auto px-4 py-4">
          <nav className="flex flex-col space-y-4">
            <Link href="/" className="hover:text-gray-300">
              首页
            </Link>
            <Link href="/blog" className="hover:text-gray-300">
              文章
            </Link>
            <Link href="/about" className="hover:text-gray-300">
              关于
            </Link>
          </nav>
          <div className="mt-4">
            <SearchBar />
          </div>
        </div>
      )}
    </header>
  );
}

11.7 完整代码讲解与优化建议

11.7.1 代码结构优化

  1. 组件拆分:将页面拆分为多个可复用组件,如 Header、Footer、PostCard 等
  2. 数据管理:将数据处理逻辑封装到 lib 目录下的文件中
  3. 样式组织:使用 Tailwind CSS 进行样式管理,保持代码简洁

11.7.2 性能优化

  1. 使用 Server Components:对于不需要交互的组件,使用 Server Components 提升性能
  2. 图片优化:使用 Next.js 的 Image 组件优化图片加载
  3. 路由预加载:使用 Link 组件的 prefetch 属性预加载路由
  4. 数据缓存:对于静态数据,使用缓存策略减少重复获取

11.7.3 可扩展性考虑

  1. 数据库集成:将模拟数据替换为真实数据库
  2. 内容管理系统:集成 CMS 系统,方便管理文章内容
  3. 部署优化:使用 Vercel 或其他云平台部署,实现自动构建和部署

11.7.4 后续功能扩展

  1. 评论系统:添加文章评论功能
  2. 深色模式:实现深色/浅色模式切换
  3. 分页功能:为文章列表添加分页
  4. 标签系统:添加文章标签,方便分类浏览
  5. RSS 订阅:生成 RSS 订阅 feed

通过本实战项目的学习,你已经掌握了 Next.js 14 的核心功能和最佳实践。这个博客网站虽然简单,但涵盖了 Next.js 开发的主要方面,包括路由系统、Server Components、数据获取、SEO 优化和响应式设计。在实际开发中,你可以根据具体需求对这个项目进行扩展和优化,打造一个功能更丰富的个人博客。

© 2026 编程马·菜鸟教程 版权所有