学习目标
- 理解服务端渲染(SSR)概念
- 掌握Next.js框架基础
- 学会静态站点生成(SSG)
- 理解API路由和中间件
- 构建完整的Next.js应用
学习时间安排
总时长:8-9小时
- SSR基础概念:1.5小时
- Next.js基础:2小时
- Next.js高级特性:2小时
- 完整项目实践:3-4小时
第一部分:SSR基础概念 (1.5小时)
1.1 服务端渲染原理
SSR基础概念(详细注释版)
javascript
// 服务端渲染(SSR)的基本原理
// SSR是在服务器上渲染React组件,然后将HTML发送给客户端
// 客户端渲染(CSR)的问题
// 1. 首屏加载慢:需要下载JavaScript后才能渲染
// 2. SEO不友好:搜索引擎无法读取JavaScript生成的内容
// 3. 初始加载体验差:用户看到空白页面
// 服务端渲染(SSR)的优势
// 1. 首屏加载快:服务器直接返回HTML
// 2. SEO友好:搜索引擎可以直接读取HTML内容
// 3. 更好的用户体验:用户立即看到内容
// 基础SSR实现示例
// server.js
const express = require('express');
const React = require('react');
const ReactDOMServer = require('react-dom/server');
const App = require('./App').default;
const app = express();
// 设置静态文件目录
app.use(express.static('public'));
// SSR路由处理
app.get('*', (req, res) => {
// 在服务器上渲染React组件
const html = ReactDOMServer.renderToString(
React.createElement(App)
);
// 返回完整的HTML文档
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>SSR App</title>
</head>
<body>
<div id="root">${html}</div>
<script src="/bundle.js"></script>
</body>
</html>
`);
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
1.2 React SSR实现
完整SSR实现(详细注释版)
javascript
// src/server.js
// 导入Express
const express = require('express');
// 导入React和ReactDOM Server
const React = require('react');
const ReactDOMServer = require('react-dom/server');
// 导入应用组件
const App = require('./App').default;
// 创建Express应用
const app = express();
// 设置静态文件目录
app.use(express.static('build'));
// SSR处理函数
function renderApp(req, res) {
try {
// 在服务器上渲染React组件为HTML字符串
const html = ReactDOMServer.renderToString(
React.createElement(App, { url: req.url })
);
// 返回完整的HTML文档
const fullHtml = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SSR React App</title>
<link rel="stylesheet" href="/static/css/main.css">
</head>
<body>
<div id="root">${html}</div>
<script src="/static/js/bundle.js"></script>
</body>
</html>
`;
res.send(fullHtml);
} catch (error) {
console.error('SSR Error:', error);
res.status(500).send('Internal Server Error');
}
}
// 处理所有路由
app.get('*', renderApp);
// 启动服务器
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
第二部分:Next.js基础 (2小时)
2.1 Next.js项目创建
创建Next.js项目(详细注释版)
bash
# 创建Next.js项目
npx create-next-app@latest my-next-app
# 选择配置选项
# - TypeScript: Yes
# - ESLint: Yes
# - Tailwind CSS: Yes (可选)
# - App Router: Yes (推荐)
# - src/ directory: Yes
# - import alias: Yes
# 进入项目目录
cd my-next-app
# 启动开发服务器
npm run dev
Next.js项目结构(详细注释版)
my-next-app/
├── public/ # 静态文件目录
│ ├── images/ # 图片文件
│ └── favicon.ico # 网站图标
├── src/
│ ├── app/ # App Router目录(Next.js 13+)
│ │ ├── layout.tsx # 根布局组件
│ │ ├── page.tsx # 首页
│ │ ├── loading.tsx # 加载状态组件
│ │ ├── error.tsx # 错误页面
│ │ └── [slug]/ # 动态路由
│ ├── components/ # 组件目录
│ │ ├── common/ # 通用组件
│ │ └── features/ # 功能组件
│ ├── lib/ # 工具函数
│ ├── hooks/ # 自定义Hooks
│ ├── store/ # 状态管理
│ └── styles/ # 样式文件
├── next.config.js # Next.js配置文件
├── tsconfig.json # TypeScript配置
└── package.json # 项目配置
2.2 Next.js页面和路由
基础页面组件(详细注释版)
typescript
// src/app/page.tsx
// 导入React
import React from 'react';
// 导入Next.js组件
import Link from 'next/link';
// 导入样式
import './globals.css';
// 定义首页组件
// 这是一个服务器组件(默认)
// 服务器组件在服务器上渲染,不包含客户端JavaScript
export default function Home() {
return (
<main className="home-page">
<div className="hero-section">
<h1>Welcome to Next.js</h1>
<p>This is a server-rendered React application</p>
<div className="cta-buttons">
<Link href="/about" className="btn btn-primary">
Learn More
</Link>
<Link href="/blog" className="btn btn-secondary">
Read Blog
</Link>
</div>
</div>
<div className="features-section">
<h2>Features</h2>
<div className="features-grid">
<div className="feature-card">
<h3>Server-Side Rendering</h3>
<p>Fast initial page loads with SSR</p>
</div>
<div className="feature-card">
<h3>Static Site Generation</h3>
<p>Pre-render pages at build time</p>
</div>
<div className="feature-card">
<h3>API Routes</h3>
<p>Build API endpoints with Next.js</p>
</div>
</div>
</div>
</main>
);
}
// 导出元数据(用于SEO)
export const metadata = {
title: 'Home - Next.js App',
description: 'Welcome to our Next.js application'
};
动态路由页面(详细注释版)
typescript
// src/app/blog/[slug]/page.tsx
// 导入React
import React from 'react';
// 导入Next.js组件和函数
import { notFound } from 'next/navigation';
import Link from 'next/link';
// 导入类型
import { Metadata } from 'next';
// 定义页面参数接口
interface PageProps {
params: {
slug: string;
};
}
// 定义页面组件
// params包含动态路由参数
export default async function BlogPost({ params }: PageProps) {
// 获取博客文章数据
// 这是一个异步函数,可以在服务器上获取数据
const post = await getPostBySlug(params.slug);
// 如果文章不存在,返回404
if (!post) {
notFound();
}
return (
<article className="blog-post">
<Link href="/blog" className="back-link">
← Back to Blog
</Link>
<header className="post-header">
<h1>{post.title}</h1>
<div className="post-meta">
<span>By {post.author}</span>
<span>{new Date(post.date).toLocaleDateString()}</span>
</div>
</header>
<div className="post-content">
{post.content}
</div>
</article>
);
}
// 生成静态参数(用于静态生成)
export async function generateStaticParams() {
// 获取所有博客文章的slug
const posts = await getAllPosts();
// 返回所有slug数组
return posts.map(post => ({
slug: post.slug
}));
}
// 生成元数据(用于SEO)
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const post = await getPostBySlug(params.slug);
if (!post) {
return {
title: 'Post Not Found'
};
}
return {
title: post.title,
description: post.excerpt
};
}
// 模拟获取文章数据
async function getPostBySlug(slug: string) {
// 模拟API调用
const posts = [
{
slug: 'first-post',
title: 'First Post',
content: 'This is the first post content...',
author: 'John Doe',
date: '2024-01-01',
excerpt: 'This is the first post'
}
];
return posts.find(post => post.slug === slug);
}
// 模拟获取所有文章
async function getAllPosts() {
return [
{ slug: 'first-post' },
{ slug: 'second-post' }
];
}
2.3 布局组件
根布局组件(详细注释版)
typescript
// src/app/layout.tsx
// 导入React
import React from 'react';
// 导入Next.js组件
import Link from 'next/link';
// 导入样式
import './globals.css';
// 定义根布局组件的Props接口
interface RootLayoutProps {
children: React.ReactNode;
}
// 定义根布局组件
// 这个组件会包装所有页面
export default function RootLayout({ children }: RootLayoutProps) {
return (
<html lang="en">
<head>
<meta charSet="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
{/* 导航栏 */}
<nav className="navbar">
<div className="nav-container">
<Link href="/" className="nav-logo">
My Next.js App
</Link>
<ul className="nav-links">
<li>
<Link href="/">Home</Link>
</li>
<li>
<Link href="/about">About</Link>
</li>
<li>
<Link href="/blog">Blog</Link>
</li>
<li>
<Link href="/contact">Contact</Link>
</li>
</ul>
</div>
</nav>
{/* 主要内容 */}
<main className="main-content">
{children}
</main>
{/* 页脚 */}
<footer className="footer">
<div className="footer-content">
<p>© 2024 My Next.js App. All rights reserved.</p>
</div>
</footer>
</body>
</html>
);
}
// 导出元数据
export const metadata = {
title: {
default: 'My Next.js App',
template: '%s | My Next.js App'
},
description: 'A Next.js application built with React 18'
};
第三部分:Next.js高级特性 (2小时)
3.1 数据获取
服务端数据获取(详细注释版)
typescript
// src/app/blog/page.tsx
// 导入React
import React from 'react';
// 导入Next.js组件
import Link from 'next/link';
// 定义博客列表页面组件
// 这是一个异步服务器组件
export default async function BlogPage() {
// 在服务器上获取数据
// 这个函数在每次请求时都会执行(SSR)
const posts = await getPosts();
return (
<div className="blog-page">
<h1>Blog Posts</h1>
<div className="posts-grid">
{posts.map(post => (
<article key={post.id} className="post-card">
<Link href={`/blog/${post.slug}`}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
<span className="post-date">
{new Date(post.date).toLocaleDateString()}
</span>
</Link>
</article>
))}
</div>
</div>
);
}
// 获取博客文章数据
// 这是一个服务器端函数
async function getPosts() {
// 模拟API调用
// 在实际应用中,这里会调用数据库或API
const res = await fetch('https://api.example.com/posts', {
// 禁用缓存,确保每次获取最新数据
cache: 'no-store'
});
if (!res.ok) {
throw new Error('Failed to fetch posts');
}
return res.json();
}
静态生成数据获取(详细注释版)
typescript
// src/app/products/page.tsx
// 导入React
import React from 'react';
// 定义产品列表页面组件
// 使用静态生成(SSG)
export default async function ProductsPage() {
// 在构建时获取数据
// 这个函数只在构建时执行一次
const products = await getProducts();
return (
<div className="products-page">
<h1>Products</h1>
<div className="products-grid">
{products.map(product => (
<div key={product.id} className="product-card">
<h3>{product.name}</h3>
<p>{product.description}</p>
<p className="product-price">${product.price}</p>
</div>
))}
</div>
</div>
);
}
// 获取产品数据
// 使用revalidate选项启用增量静态再生(ISR)
async function getProducts() {
const res = await fetch('https://api.example.com/products', {
// 启用缓存,每60秒重新生成一次
next: { revalidate: 60 }
});
if (!res.ok) {
throw new Error('Failed to fetch products');
}
return res.json();
}
// 导出生成静态参数函数
// 这个函数在构建时执行,生成所有静态页面
export async function generateStaticParams() {
const products = await getProducts();
return products.map(product => ({
id: product.id.toString()
}));
}
3.2 API路由
API路由实现(详细注释版)
typescript
// src/app/api/posts/route.ts
// 导入Next.js类型
import { NextRequest, NextResponse } from 'next/server';
// 定义GET请求处理函数
// 处理获取所有文章的请求
export async function GET(request: NextRequest) {
try {
// 获取查询参数
const searchParams = request.nextUrl.searchParams;
const page = parseInt(searchParams.get('page') || '1');
const limit = parseInt(searchParams.get('limit') || '10');
// 从数据库获取文章
// 这里使用模拟数据
const posts = await getPostsFromDatabase(page, limit);
// 返回JSON响应
return NextResponse.json({
success: true,
data: posts,
pagination: {
page,
limit,
total: posts.length
}
});
} catch (error) {
// 处理错误
return NextResponse.json(
{ success: false, error: 'Failed to fetch posts' },
{ status: 500 }
);
}
}
// 定义POST请求处理函数
// 处理创建新文章的请求
export async function POST(request: NextRequest) {
try {
// 解析请求体
const body = await request.json();
// 验证数据
if (!body.title || !body.content) {
return NextResponse.json(
{ success: false, error: 'Title and content are required' },
{ status: 400 }
);
}
// 创建新文章
const post = await createPost(body);
// 返回创建的文章
return NextResponse.json(
{ success: true, data: post },
{ status: 201 }
);
} catch (error) {
return NextResponse.json(
{ success: false, error: 'Failed to create post' },
{ status: 500 }
);
}
}
// 模拟从数据库获取文章
async function getPostsFromDatabase(page: number, limit: number) {
// 实际应用中这里会查询数据库
return Array.from({ length: limit }, (_, i) => ({
id: (page - 1) * limit + i + 1,
title: `Post ${(page - 1) * limit + i + 1}`,
content: `Content for post ${(page - 1) * limit + i + 1}`,
createdAt: new Date().toISOString()
}));
}
// 模拟创建文章
async function createPost(data: any) {
// 实际应用中这里会插入数据库
return {
id: Date.now(),
...data,
createdAt: new Date().toISOString()
};
}
动态API路由(详细注释版)
typescript
// src/app/api/posts/[id]/route.ts
// 导入Next.js类型
import { NextRequest, NextResponse } from 'next/server';
// 定义页面参数接口
interface RouteParams {
params: {
id: string;
};
}
// 定义GET请求处理函数
// 处理获取单个文章的请求
export async function GET(
request: NextRequest,
{ params }: RouteParams
) {
try {
const post = await getPostById(params.id);
if (!post) {
return NextResponse.json(
{ success: false, error: 'Post not found' },
{ status: 404 }
);
}
return NextResponse.json({
success: true,
data: post
});
} catch (error) {
return NextResponse.json(
{ success: false, error: 'Failed to fetch post' },
{ status: 500 }
);
}
}
// 定义PUT请求处理函数
// 处理更新文章的请求
export async function PUT(
request: NextRequest,
{ params }: RouteParams
) {
try {
const body = await request.json();
const post = await updatePost(params.id, body);
if (!post) {
return NextResponse.json(
{ success: false, error: 'Post not found' },
{ status: 404 }
);
}
return NextResponse.json({
success: true,
data: post
});
} catch (error) {
return NextResponse.json(
{ success: false, error: 'Failed to update post' },
{ status: 500 }
);
}
}
// 定义DELETE请求处理函数
// 处理删除文章的请求
export async function DELETE(
request: NextRequest,
{ params }: RouteParams
) {
try {
const success = await deletePost(params.id);
if (!success) {
return NextResponse.json(
{ success: false, error: 'Post not found' },
{ status: 404 }
);
}
return NextResponse.json({
success: true,
message: 'Post deleted successfully'
});
} catch (error) {
return NextResponse.json(
{ success: false, error: 'Failed to delete post' },
{ status: 500 }
);
}
}
// 模拟获取文章
async function getPostById(id: string) {
// 实际应用中这里会查询数据库
return {
id,
title: `Post ${id}`,
content: `Content for post ${id}`,
createdAt: new Date().toISOString()
};
}
// 模拟更新文章
async function updatePost(id: string, data: any) {
// 实际应用中这里会更新数据库
return {
id,
...data,
updatedAt: new Date().toISOString()
};
}
// 模拟删除文章
async function deletePost(id: string) {
// 实际应用中这里会删除数据库记录
return true;
}
3.3 中间件
Next.js中间件(详细注释版)
typescript
// src/middleware.ts
// 导入Next.js类型
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
// 定义中间件函数
// 这个函数会在每个请求之前执行
export function middleware(request: NextRequest) {
// 获取请求路径
const path = request.nextUrl.pathname;
// 获取认证token
const token = request.cookies.get('token');
// 检查是否是受保护的路由
const isProtectedRoute = path.startsWith('/dashboard') ||
path.startsWith('/admin');
// 如果访问受保护的路由但没有token,重定向到登录页
if (isProtectedRoute && !token) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('from', path);
return NextResponse.redirect(loginUrl);
}
// 如果已登录用户访问登录页,重定向到首页
if (path === '/login' && token) {
return NextResponse.redirect(new URL('/', request.url));
}
// 添加自定义响应头
const response = NextResponse.next();
response.headers.set('X-Custom-Header', 'Custom Value');
return response;
}
// 配置中间件匹配规则
export const config = {
// 匹配所有路径,除了以下路径
matcher: [
/*
* 匹配所有请求路径,除了:
* - api路由
* - _next/static (静态文件)
* - _next/image (图片优化)
* - favicon.ico (网站图标)
*/
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
};
第四部分:完整项目实践 (3-4小时)
项目:Next.js博客系统
项目结构(详细注释版)
nextjs-blog/
├── public/
│ ├── images/
│ └── favicon.ico
├── src/
│ ├── app/
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ ├── blog/
│ │ │ ├── page.tsx
│ │ │ └── [slug]/
│ │ │ └── page.tsx
│ │ ├── api/
│ │ │ ├── posts/
│ │ │ │ ├── route.ts
│ │ │ │ └── [id]/
│ │ │ │ └── route.ts
│ │ │ └── auth/
│ │ │ └── route.ts
│ │ └── admin/
│ │ ├── layout.tsx
│ │ └── page.tsx
│ ├── components/
│ │ ├── common/
│ │ │ ├── Header.tsx
│ │ │ ├── Footer.tsx
│ │ │ └── Navigation.tsx
│ │ └── blog/
│ │ ├── PostCard.tsx
│ │ ├── PostList.tsx
│ │ └── PostContent.tsx
│ ├── lib/
│ │ ├── api.ts
│ │ ├── db.ts
│ │ └── utils.ts
│ ├── types/
│ │ ├── post.ts
│ │ └── user.ts
│ └── styles/
│ └── globals.css
├── next.config.js
├── tsconfig.json
└── package.json
主页面组件(详细注释版)
typescript
// src/app/page.tsx
// 导入React
import React from 'react';
// 导入Next.js组件
import Link from 'next/link';
// 导入组件
import PostList from '../components/blog/PostList';
// 定义首页组件
// 这是一个异步服务器组件
export default async function Home() {
// 在服务器上获取最新的博客文章
const recentPosts = await getRecentPosts(3);
return (
<div className="home-page">
<section className="hero-section">
<h1>Welcome to My Blog</h1>
<p>A Next.js blog application with server-side rendering</p>
<Link href="/blog" className="btn btn-primary">
Read Blog
</Link>
</section>
<section className="recent-posts-section">
<h2>Recent Posts</h2>
<PostList posts={recentPosts} />
<Link href="/blog" className="view-all-link">
View All Posts →
</Link>
</section>
</div>
);
}
// 获取最新文章
async function getRecentPosts(limit: number) {
// 在实际应用中,这里会从数据库获取数据
// 这里使用模拟数据
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/posts?limit=${limit}`, {
cache: 'no-store' // 禁用缓存,每次获取最新数据
});
if (!res.ok) {
return [];
}
const data = await res.json();
return data.data || [];
}
// 导出元数据
export const metadata = {
title: 'Home - My Blog',
description: 'Welcome to my Next.js blog'
};
博客列表页面(详细注释版)
typescript
// src/app/blog/page.tsx
// 导入React
import React from 'react';
// 导入Next.js组件
import { Suspense } from 'react';
// 导入组件
import PostList from '../../components/blog/PostList';
import Loading from '../../components/common/Loading';
// 定义博客列表页面组件
export default async function BlogPage({
searchParams
}: {
searchParams: { page?: string; category?: string }
}) {
// 获取当前页码
const page = parseInt(searchParams.page || '1');
// 获取分类
const category = searchParams.category;
return (
<div className="blog-page">
<h1>Blog Posts</h1>
{/* 使用Suspense包装异步组件 */}
<Suspense fallback={<Loading />}>
<BlogPostsList page={page} category={category} />
</Suspense>
</div>
);
}
// 定义博客文章列表组件
async function BlogPostsList({
page,
category
}: {
page: number;
category?: string
}) {
// 获取博客文章
const posts = await getPosts(page, category);
return <PostList posts={posts} />;
}
// 获取博客文章
async function getPosts(page: number, category?: string) {
// 构建查询参数
const params = new URLSearchParams({
page: page.toString(),
limit: '10'
});
if (category) {
params.append('category', category);
}
// 获取文章数据
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/posts?${params.toString()}`,
{
cache: 'no-store' // SSR模式,每次获取最新数据
}
);
if (!res.ok) {
return [];
}
const data = await res.json();
return data.data || [];
}
// 导出元数据
export const metadata = {
title: 'Blog - My Blog',
description: 'Read our latest blog posts'
};
博客详情页面(详细注释版)
typescript
// src/app/blog/[slug]/page.tsx
// 导入React
import React from 'react';
// 导入Next.js函数
import { notFound } from 'next/navigation';
import Link from 'next/link';
// 导入组件
import PostContent from '../../../components/blog/PostContent';
// 导入类型
import { Metadata } from 'next';
// 定义页面参数接口
interface PageProps {
params: {
slug: string;
};
}
// 定义博客详情页面组件
export default async function BlogPostPage({ params }: PageProps) {
// 获取博客文章
const post = await getPostBySlug(params.slug);
// 如果文章不存在,返回404
if (!post) {
notFound();
}
return (
<article className="blog-post-page">
<Link href="/blog" className="back-link">
← Back to Blog
</Link>
<PostContent post={post} />
</article>
);
}
// 生成静态参数
// 这个函数在构建时执行,生成所有静态页面
export async function generateStaticParams() {
// 获取所有文章的slug
const posts = await getAllPostSlugs();
return posts.map(post => ({
slug: post.slug
}));
}
// 生成元数据
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const post = await getPostBySlug(params.slug);
if (!post) {
return {
title: 'Post Not Found'
};
}
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.image || '/default-image.jpg']
}
};
}
// 获取文章数据
async function getPostBySlug(slug: string) {
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/posts/slug/${slug}`,
{
cache: 'no-store'
}
);
if (!res.ok) {
return null;
}
const data = await res.json();
return data.data;
}
// 获取所有文章的slug
async function getAllPostSlugs() {
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/posts/slugs`,
{
cache: 'no-store'
}
);
if (!res.ok) {
return [];
}
const data = await res.json();
return data.data || [];
}
Next.js配置文件(详细注释版)
javascript
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
// React严格模式
reactStrictMode: true,
// 启用SWC压缩
swcMinify: true,
// 图片优化配置
images: {
// 允许的图片域名
domains: ['example.com', 'cdn.example.com'],
// 图片格式
formats: ['image/avif', 'image/webp'],
// 图片大小限制
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384]
},
// 环境变量
env: {
CUSTOM_KEY: process.env.CUSTOM_KEY
},
// 重定向配置
async redirects() {
return [
{
source: '/old-page',
destination: '/new-page',
permanent: true
}
];
},
// 重写配置
async rewrites() {
return [
{
source: '/api/:path*',
destination: '/api/:path*'
}
];
},
// 头部配置
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'X-Content-Type-Options',
value: 'nosniff'
},
{
key: 'X-Frame-Options',
value: 'DENY'
},
{
key: 'X-XSS-Protection',
value: '1; mode=block'
}
]
}
];
},
// 输出配置
output: 'standalone',
// 实验性功能
experimental: {
// 启用服务器组件
serverComponents: true,
// 启用App Router
appDir: true
}
};
module.exports = nextConfig;
样式文件(详细注释版)
css
/* src/styles/globals.css */
/* 全局样式重置 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* 根元素样式 */
html {
font-size: 16px;
line-height: 1.5;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: #ffffff;
color: #333;
}
/* 导航栏样式 */
.navbar {
background-color: #333;
color: white;
padding: 1rem 0;
position: sticky;
top: 0;
z-index: 100;
}
.nav-container {
max-width: 1200px;
margin: 0 auto;
padding: 0 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.nav-logo {
font-size: 1.5rem;
font-weight: bold;
color: white;
text-decoration: none;
}
.nav-links {
display: flex;
list-style: none;
gap: 2rem;
}
.nav-links a {
color: white;
text-decoration: none;
transition: opacity 0.2s;
}
.nav-links a:hover {
opacity: 0.8;
}
/* 主要内容区域 */
.main-content {
min-height: calc(100vh - 200px);
padding: 2rem 0;
}
/* 首页样式 */
.home-page {
max-width: 1200px;
margin: 0 auto;
padding: 0 2rem;
}
.hero-section {
text-align: center;
padding: 4rem 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 8px;
margin-bottom: 3rem;
}
.hero-section h1 {
font-size: 3rem;
margin-bottom: 1rem;
}
.hero-section p {
font-size: 1.2rem;
margin-bottom: 2rem;
opacity: 0.9;
}
.recent-posts-section {
margin-top: 3rem;
}
.recent-posts-section h2 {
margin-bottom: 2rem;
font-size: 2rem;
}
.view-all-link {
display: inline-block;
margin-top: 2rem;
color: #667eea;
text-decoration: none;
font-weight: 500;
}
.view-all-link:hover {
text-decoration: underline;
}
/* 博客页面样式 */
.blog-page {
max-width: 1200px;
margin: 0 auto;
padding: 0 2rem;
}
.blog-page h1 {
font-size: 2.5rem;
margin-bottom: 2rem;
}
/* 博客文章卡片样式 */
.post-card {
background-color: white;
border-radius: 8px;
padding: 2rem;
margin-bottom: 2rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
transition: box-shadow 0.2s;
}
.post-card:hover {
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.post-card h2 {
margin-bottom: 1rem;
font-size: 1.5rem;
}
.post-card h2 a {
color: #333;
text-decoration: none;
}
.post-card h2 a:hover {
color: #667eea;
}
.post-card p {
color: #666;
line-height: 1.6;
margin-bottom: 1rem;
}
.post-meta {
display: flex;
gap: 1rem;
font-size: 0.9rem;
color: #999;
}
/* 博客文章详情页样式 */
.blog-post-page {
max-width: 800px;
margin: 0 auto;
padding: 0 2rem;
}
.back-link {
display: inline-block;
margin-bottom: 2rem;
color: #667eea;
text-decoration: none;
}
.back-link:hover {
text-decoration: underline;
}
.post-header {
margin-bottom: 2rem;
padding-bottom: 2rem;
border-bottom: 1px solid #eee;
}
.post-header h1 {
font-size: 2.5rem;
margin-bottom: 1rem;
color: #333;
}
.post-content {
line-height: 1.8;
color: #333;
}
.post-content h2 {
font-size: 1.8rem;
margin-top: 2rem;
margin-bottom: 1rem;
}
.post-content h3 {
font-size: 1.5rem;
margin-top: 1.5rem;
margin-bottom: 0.75rem;
}
.post-content p {
margin-bottom: 1rem;
}
.post-content ul,
.post-content ol {
margin-bottom: 1rem;
padding-left: 2rem;
}
.post-content li {
margin-bottom: 0.5rem;
}
/* 按钮样式 */
.btn {
display: inline-block;
padding: 0.75rem 1.5rem;
border-radius: 4px;
text-decoration: none;
font-weight: 500;
transition: all 0.2s;
border: none;
cursor: pointer;
}
.btn-primary {
background-color: #667eea;
color: white;
}
.btn-primary:hover {
background-color: #5568d3;
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-secondary:hover {
background-color: #5a6268;
}
/* 页脚样式 */
.footer {
background-color: #333;
color: white;
padding: 2rem 0;
margin-top: 4rem;
}
.footer-content {
max-width: 1200px;
margin: 0 auto;
padding: 0 2rem;
text-align: center;
}
/* 加载状态样式 */
.loading {
text-align: center;
padding: 4rem 2rem;
color: #666;
}
/* 响应式设计 */
@media (max-width: 768px) {
.nav-container {
flex-direction: column;
gap: 1rem;
}
.nav-links {
flex-direction: column;
gap: 1rem;
text-align: center;
}
.hero-section h1 {
font-size: 2rem;
}
.blog-page,
.blog-post-page {
padding: 0 1rem;
}
}
练习题目
基础练习
- Next.js基础练习
typescript
// 练习1:创建Next.js项目并实现基础路由
// 实现:首页、关于页、联系页
// 包含:布局组件、导航、样式
// 练习2:实现动态路由和API路由
// 实现:博客文章详情页、文章API
// 包含:数据获取、静态生成、动态路由
- SSR和SSG练习
typescript
// 练习3:实现服务端渲染页面
// 实现:从API获取数据并在服务器渲染
// 包含:数据获取、错误处理、加载状态
// 练习4:实现静态站点生成
// 实现:构建时生成静态页面
// 包含:generateStaticParams、ISR
进阶练习
- 完整项目练习
typescript
// 练习5:构建完整的Next.js应用
// 实现:博客系统、用户认证、管理后台
// 包含:SSR、SSG、API路由、中间件
// 练习6:优化Next.js应用性能
// 实现:图片优化、代码分割、缓存策略
// 包含:性能监控、优化报告