React 18.x 学习计划 - 第十一天:服务端渲染与Next.js

学习目标

  • 理解服务端渲染(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>&copy; 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;
  }
}

练习题目

基础练习

  1. Next.js基础练习
typescript 复制代码
// 练习1:创建Next.js项目并实现基础路由
// 实现:首页、关于页、联系页
// 包含:布局组件、导航、样式

// 练习2:实现动态路由和API路由
// 实现:博客文章详情页、文章API
// 包含:数据获取、静态生成、动态路由
  1. SSR和SSG练习
typescript 复制代码
// 练习3:实现服务端渲染页面
// 实现:从API获取数据并在服务器渲染
// 包含:数据获取、错误处理、加载状态

// 练习4:实现静态站点生成
// 实现:构建时生成静态页面
// 包含:generateStaticParams、ISR

进阶练习

  1. 完整项目练习
typescript 复制代码
// 练习5:构建完整的Next.js应用
// 实现:博客系统、用户认证、管理后台
// 包含:SSR、SSG、API路由、中间件

// 练习6:优化Next.js应用性能
// 实现:图片优化、代码分割、缓存策略
// 包含:性能监控、优化报告
相关推荐
wulijuan88866618 小时前
Web Worker
前端·javascript
老朋友此林19 小时前
React Hook原理速通笔记1(useEffect 原理、使用踩坑、渲染周期、依赖项)
javascript·笔记·react.js
克里斯蒂亚诺更新19 小时前
vue3使用pinia替代vuex举例
前端·javascript·vue.js
茶猫_19 小时前
C++学习记录-旧题新做-链表求和
数据结构·c++·学习·算法·leetcode·链表
龘龍龙19 小时前
Python基础学习(十一)
python·学习·mysql
冰暮流星19 小时前
javascript赋值运算符
开发语言·javascript·ecmascript
Chris_121919 小时前
Halcon学习笔记-Day5
人工智能·笔记·python·学习·机器学习·halcon
悠哉悠哉愿意19 小时前
【嵌入式学习笔记】AD/DA
笔记·单片机·嵌入式硬件·学习
前端程序猿之路20 小时前
30天大模型学习之Day 2:Prompt 工程基础系统
大数据·人工智能·学习·算法·语言模型·prompt·ai编程
硬件yun20 小时前
汽车CAN为何选用0.25W电阻?
学习