Appearance
第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>© {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 代码结构优化
- 组件拆分:将页面拆分为多个可复用组件,如 Header、Footer、PostCard 等
- 数据管理:将数据处理逻辑封装到 lib 目录下的文件中
- 样式组织:使用 Tailwind CSS 进行样式管理,保持代码简洁
11.7.2 性能优化
- 使用 Server Components:对于不需要交互的组件,使用 Server Components 提升性能
- 图片优化:使用 Next.js 的 Image 组件优化图片加载
- 路由预加载:使用 Link 组件的 prefetch 属性预加载路由
- 数据缓存:对于静态数据,使用缓存策略减少重复获取
11.7.3 可扩展性考虑
- 数据库集成:将模拟数据替换为真实数据库
- 内容管理系统:集成 CMS 系统,方便管理文章内容
- 部署优化:使用 Vercel 或其他云平台部署,实现自动构建和部署
11.7.4 后续功能扩展
- 评论系统:添加文章评论功能
- 深色模式:实现深色/浅色模式切换
- 分页功能:为文章列表添加分页
- 标签系统:添加文章标签,方便分类浏览
- RSS 订阅:生成 RSS 订阅 feed
通过本实战项目的学习,你已经掌握了 Next.js 14 的核心功能和最佳实践。这个博客网站虽然简单,但涵盖了 Next.js 开发的主要方面,包括路由系统、Server Components、数据获取、SEO 优化和响应式设计。在实际开发中,你可以根据具体需求对这个项目进行扩展和优化,打造一个功能更丰富的个人博客。
