跳转至

服务端渲染

服务端渲染

📚 章节目标

本章节将全面介绍服务端渲染的各种技术和方案,包括Next.js、Nuxt.js、SSR、SSG、ISR等,帮助学习者掌握服务端渲染的核心方法。

学习目标

  1. 理解服务端渲染的核心概念
  2. 掌握Next.js服务端渲染
  3. 掌握Nuxt.js服务端渲染
  4. 理解SSR、SSG、ISR的区别
  5. 掌握服务端渲染优化
  6. 理解服务端渲染最佳实践

🌐 服务端渲染概述

1. 什么是服务端渲染

JavaScript
// 服务端渲染 (SSR)
// 在服务器上渲染HTML,然后发送给客户端
// 优点:
// 1. 更快的首屏加载速度
// 2. 更好的SEO
// 3. 更好的社交媒体分享

// 客户端渲染 (CSR)
// 在浏览器上渲染HTML
// 优点:
// 1. 更好的交互体验
// 2. 更小的服务器负载
// 3. 更容易实现复杂交互

// 混合渲染 (SSR + CSR)
// 结合SSR和CSR的优点
// 首屏使用SSR,后续交互使用CSR

2. 渲染模式对比

JavaScript
// SSR (Server-Side Rendering)
// 每次请求都在服务器上渲染
// 适合:动态内容、个性化内容

// SSG (Static Site Generation)
// 构建时生成静态HTML
// 适合:静态内容、博客、文档

// ISR (Incremental Static Regeneration)
// 静态生成 + 按需更新
// 适合:内容不经常变化但需要更新的页面

// CSR (Client-Side Rendering)
// 在浏览器上渲染
// 适合:管理后台、SPA应用

⚛️ Next.js服务端渲染

1. Next.js基础

1.1 基本配置

JavaScript
// 安装Next.js
npx create-next-app@latest my-app

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  // swcMinify 已在 Next.js 15 中默认启用且不再需要显式设置
  images: {
    remotePatterns: [
      { protocol: 'https', hostname: 'example.com' },
    ],
  },
};

module.exports = nextConfig;

1.2 页面路由

JavaScript
// pages/index.js
export default function Home() {
  return (
    <div>
      <h1>Welcome to Next.js!</h1>
      <p>Get started by editing pages/index.js</p>
    </div>
  );
}

// pages/about.js
export default function About() {
  return (
    <div>
      <h1>About</h1>
      <p>This is the about page</p>
    </div>
  );
}

// 动态路由
// pages/users/[id].js
export default function User({ user }) {
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

export async function getStaticPaths() {
  const res = await fetch('https://api.example.com/users');
  const users = await res.json();

  return {
    paths: users.map((user) => ({
      params: { id: user.id.toString() },
    })),
    fallback: false,
  };
}

export async function getStaticProps({ params }) {
  const res = await fetch(`https://api.example.com/users/${params.id}`);
  const user = await res.json();

  return {
    props: { user },
  };
}

2. Next.js数据获取

2.1 getServerSideProps

JavaScript
// getServerSideProps - 每次请求都执行
export async function getServerSideProps(context) {
  const res = await fetch('https://api.example.com/data');
  const data = await res.json();

  return {
    props: { data },
  };
}

export default function Page({ data }) {
  return (
    <div>
      <h1>Data from Server</h1>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
}

2.2 getStaticProps

JavaScript
// getStaticProps - 构建时执行
export async function getStaticProps() {
  const res = await fetch('https://api.example.com/data');
  const data = await res.json();

  return {
    props: { data },
    revalidate: 60, // 60秒后重新生成
  };
}

export default function Page({ data }) {
  return (
    <div>
      <h1>Data from Build Time</h1>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
}

2.3 getInitialProps

⚠️ 已废弃getInitialProps 是 Next.js 早期 API,在 Pages Router 中已被 getServerSideProps(SSR)和 getStaticProps(SSG)取代。App Router 中请使用 Server Components 或 fetch + cache

JavaScript
// getInitialProps - 兼容旧版本(Legacy API,不推荐在新项目中使用)
class Page extends React.Component {
  static async getInitialProps() {
    const res = await fetch('https://api.example.com/data');
    const data = await res.json();

    return { data };
  }

  render() {
    return (
      <div>
        <h1>Data from getInitialProps</h1>
        <pre>{JSON.stringify(this.props.data, null, 2)}</pre>
      </div>
    );
  }
}

export default Page;

3. Next.js API路由

JavaScript
// pages/api/hello.js
export default function handler(req, res) {
  res.status(200).json({ message: 'Hello from Next.js API' });
}

// pages/api/users/[id].js
export default async function handler(req, res) {
  const { id } = req.query;

  const user = await fetchUser(id);

  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }

  res.status(200).json(user);
}

🚀 Next.js App Router(推荐)

说明: Next.js 13.4+ 引入的 App Router 现已成为推荐方案。与 Pages Router 相比,App Router 基于 React Server Components,提供更好的性能和开发体验。新项目应优先使用 App Router。

1. App Router 基础

1.1 目录结构

Text Only
app/
├── layout.tsx        # 根布局(必需)
├── page.tsx          # 首页 → /
├── loading.tsx       # 加载UI(Suspense boundary)
├── error.tsx         # 错误UI(Error boundary)
├── not-found.tsx     # 404页面
├── about/
│   └── page.tsx      # /about
├── users/
│   ├── page.tsx      # /users
│   └── [id]/
│       └── page.tsx  # /users/:id
└── api/
    └── hello/
        └── route.ts  # API路由 → /api/hello

1.2 布局与页面

TSX
// === 类型定义 ===
interface Post {  // interface定义类型契约
  id: string;
  title: string;
  slug: string;
  excerpt: string;
  content: string;
}

interface User {
  id: string;
  name: string;
  email: string;
}

// app/layout.tsx — 根布局(Server Component)
import { Inter } from 'next/font/google';

const inter = Inter({ subsets: ['latin'] });

export const metadata = {
  title: 'My App',
  description: 'Built with Next.js App Router',
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="zh-CN">
      <body className={inter.className}>{children}</body>
    </html>
  );
}

// app/page.tsx — 首页(默认 Server Component)
export default async function HomePage() {
  // 可以直接 await 异步操作,无需 getServerSideProps
  const posts = await fetch('https://api.example.com/posts', {
    next: { revalidate: 60 }, // ISR: 60秒后重新验证
  }).then(res => res.json()) as Post[];

  return (
    <main>
      <h1>Latest Posts</h1>
      {posts.map((post) => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </main>
  );
}

2. 数据获取(App Router 方式)

TSX
// === Server Component 直接 async/await(替代 getServerSideProps)===
// app/users/page.tsx
async function getUsers(): Promise<User[]> {  // Promise异步操作容器:pending→fulfilled/rejected
  const res = await fetch('https://api.example.com/users', {
    cache: 'no-store', // SSR: 每次请求都获取最新数据
  });
  if (!res.ok) throw new Error('Failed to fetch');
  return res.json();
}

export default async function UsersPage() {
  const users = await getUsers();
  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

// === 静态生成 + ISR(替代 getStaticProps + revalidate)===
// app/blog/[slug]/page.tsx
async function getPost(slug: string): Promise<Post> {
  const res = await fetch(`https://api.example.com/posts/${slug}`, {
    next: { revalidate: 3600 }, // ISR: 1小时后重新生成
  });
  return res.json();
}

// 替代 getStaticPaths
export async function generateStaticParams() {
  const posts: Post[] = await fetch('https://api.example.com/posts').then(r => r.json());
  return posts.map((post) => ({ slug: post.slug }));
}

export default async function BlogPost({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug);
  return <article><h1>{post.title}</h1><p>{post.content}</p></article>;
}

// > **📝 Next.js 15+ 变更**:动态路由的 `params` 和 `searchParams` 现在是异步的(Promise),
// > 需使用 `const { slug } = await params` 或在服务端组件中解包。上述写法适用于 Next.js 14,
// > Next.js 15 中签名应为 `{ params }: { params: Promise<{ slug: string }> }`。

3. Server Components vs Client Components

TSX
// === Server Component(默认)===
// 在服务器端渲染,不发送 JS 到客户端
// 可以直接访问数据库、文件系统、环境变量
async function ProductList() {
  const products = await db.product.findMany(); // 直接查数据库
  return (
    <ul>
      {products.map(p => <li key={p.id}>{p.name} - ¥{p.price}</li>)}
    </ul>
  );
}

// === Client Component(需要交互时)===
'use client'; // 必须在文件顶部声明

import { useState } from 'react';

function AddToCartButton({ productId }: { productId: string }) {
  const [loading, setLoading] = useState(false);

  const handleClick = async () => {
    setLoading(true);
    await fetch('/api/cart', {
      method: 'POST',
      body: JSON.stringify({ productId }),
    });
    setLoading(false);
  };

  return (
    <button onClick={handleClick} disabled={loading}>
      {loading ? '添加中...' : '加入购物车'}
    </button>
  );
}

// === 混合使用:Server Component 包裹 Client Component ===
// app/products/page.tsx (Server Component)
import { AddToCartButton } from './AddToCartButton';

export default async function ProductsPage() {
  const products = await db.product.findMany();
  return (
    <div>
      {products.map(p => (
        <div key={p.id}>
          <h3>{p.name}</h3>
          <AddToCartButton productId={p.id} /> {/* Client Component */}
        </div>
      ))}
    </div>
  );
}

4. 加载与错误处理

TSX
// app/users/loading.tsx — 自动 Suspense boundary
export default function Loading() {
  return <div className="skeleton">Loading users...</div>;
}

// app/users/error.tsx — 自动 Error boundary
'use client';

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <div>
      <h2>Something went wrong!</h2>
      <p>{error.message}</p>
      <button onClick={reset}>Try again</button>
    </div>
  );
}

// app/api/hello/route.ts — Route Handler(替代 pages/api)
import { NextResponse } from 'next/server';

export async function GET() {
  return NextResponse.json({ message: 'Hello from App Router API' });
}

export async function POST(request: Request) {
  const body = await request.json();
  return NextResponse.json({ received: body }, { status: 201 });
}

5. Pages Router vs App Router 对照

功能 Pages Router App Router
文件位置 pages/ app/
SSR数据获取 getServerSideProps async Server Component + cache: 'no-store'
SSG数据获取 getStaticProps async Server Component + next: { revalidate }
动态路由预生成 getStaticPaths generateStaticParams
API路由 pages/api/*.ts app/api/*/route.ts
布局 _app.tsx + _document.tsx layout.tsx (嵌套布局)
加载状态 手动实现 loading.tsx (自动Suspense)
错误处理 _error.tsx error.tsx (自动Error Boundary)
默认组件类型 客户端组件 服务端组件

🎨 Nuxt.js服务端渲染

1. Nuxt.js基础

1.1 基本配置

JavaScript
// 安装Nuxt.js
npx nuxi init my-app

// nuxt.config.ts
export default defineNuxtConfig({
  modules: [
    '@nuxtjs/tailwindcss',
    '@pinia/nuxt',
  ],

  runtimeConfig: {
    public: {
      apiBase: '/api',
    },
  },
});

1.2 页面路由

Vue
<!-- pages/index.vue -->
<template>
  <div>
    <h1>Welcome to Nuxt.js!</h1>
    <p>Get started by editing pages/index.vue</p>
  </div>
</template>

<!-- pages/about.vue -->
<template>
  <div>
    <h1>About</h1>
    <p>This is the about page</p>
  </div>
</template>

<!-- pages/users/[id].vue -->
<template>
  <div>
    <h1>{{ user.name }}</h1>
    <p>{{ user.email }}</p>
  </div>
</template>

<script setup>
const route = useRoute();
const { data: user } = await useFetch(`/api/users/${route.params.id}`);
</script>

2. Nuxt.js数据获取

2.1 useFetch

Vue
<template>
  <div>
    <h1>Data from useFetch</h1>
    <p v-if="pending">Loading...</p>
    <p v-else-if="error">Error: {{ error.message }}</p>
    <pre v-else>{{ data }}</pre>
  </div>
</template>

<script setup>
const { data, pending, error } = await useFetch('/api/data');
</script>

2.2 useAsyncData

Vue
<template>
  <div>
    <h1>Data from useAsyncData</h1>
    <pre>{{ data }}</pre>
  </div>
</template>

<script setup>
const { data } = await useAsyncData('data', () => $fetch('/api/data'));
</script>

3. Nuxt.js服务端API

JavaScript
// server/api/hello.get.ts
export default defineEventHandler((event) => {
  return {
    message: 'Hello from Nuxt.js API',
  };
});

// server/api/users/[id].get.ts
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id');
  const user = await fetchUser(id);

  if (!user) {
    throw createError({
      statusCode: 404,
      message: 'User not found',
    });
  }

  return user;
});

📊 渲染模式详解

1. SSR详解

JavaScript
// SSR - Server-Side Rendering
// 每次请求都在服务器上渲染
// 适合:动态内容、个性化内容

// Next.js SSR
export async function getServerSideProps(context) {
  const res = await fetch('https://api.example.com/data');
  const data = await res.json();

  return {
    props: { data },
  };
}

// Nuxt.js SSR
<script setup>
const { data } = await useFetch('/api/data', {
  server: true,
});
</script>

2. SSG详解

JavaScript
// SSG - Static Site Generation
// 构建时生成静态HTML
// 适合:静态内容、博客、文档

// Next.js SSG
export async function getStaticProps() {
  const res = await fetch('https://api.example.com/data');
  const data = await res.json();

  return {
    props: { data },
  };
}

export async function getStaticPaths() {
  const res = await fetch('https://api.example.com/posts');
  const posts = await res.json();

  return {
    paths: posts.map((post) => ({  // map转换每个元素;filter筛选;reduce累积
      params: { id: post.id.toString() },
    })),
    fallback: false,
  };
}

// Nuxt.js SSG(通过 nuxt.config.ts 配置 routeRules)
// nuxt.config.ts:
// export default defineNuxtConfig({
//   routeRules: { '/api/data': { prerender: true } }
// })
<script setup>
const { data } = await useFetch('/api/data');
</script>

3. ISR详解

JavaScript
// ISR - Incremental Static Regeneration
// 静态生成 + 按需更新
// 适合:内容不经常变化但需要更新的页面

// Next.js ISR
export async function getStaticProps() {
  const res = await fetch('https://api.example.com/data');
  const data = await res.json();

  return {
    props: { data },
    revalidate: 60, // 60秒后重新生成
  };
}

// Nuxt.js ISR(通过 nuxt.config.ts 配置 routeRules)
// nuxt.config.ts:
// export default defineNuxtConfig({
//   routeRules: { '/api/data': { isr: 60 } }  // 60秒后重新生成
// })
<script setup>
const { data } = await useFetch('/api/data');  // 解构赋值:从对象/数组提取值
</script>

🚀 服务端渲染优化

1. 性能优化

JavaScript
// 1. 代码分割
import dynamic from 'next/dynamic';

const DynamicComponent = dynamic(() => import('./DynamicComponent'), {  // 箭头函数:简洁的函数语法
  loading: () => <p>Loading...</p>,
  ssr: false, // 禁用SSR
});

// 2. 图片优化
import Image from 'next/image';

<Image
  src="/hero.jpg"
  alt="Hero"
  width={800}
  height={600}
  priority
  placeholder="blur"
/>

// 3. 字体优化
import { Inter } from 'next/font/google';

const inter = Inter({ subsets: ['latin'] });

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body className={inter.className}>{children}</body>
    </html>
  );
}

2. 缓存策略

JavaScript
// 1. HTTP缓存
export async function getServerSideProps({ res }) {
  res.setHeader('Cache-Control', 'public, s-maxage=60, stale-while-revalidate=30');

  const data = await fetchData();  // await等待异步操作完成

  return {
    props: { data },
  };
}

// 2. CDN缓存
// 配置CDN缓存静态资源
// 设置适当的缓存头

// 3. ISR缓存
export async function getStaticProps() {
  const data = await fetchData();  // const不可重新赋值;let块级作用域变量

  return {
    props: { data },
    revalidate: 3600, // 1小时后重新生成
  };
}

📝 练习题

1. 基础题

题目1:使用Next.js创建SSR页面

JavaScript
// pages/index.js
export default function Home({ data }) {
  return (
    <div>
      {/* 渲染数据 */}
    </div>
  );
}

export async function getServerSideProps() {  // async定义异步函数;await等待Promise完成
  // 获取数据
}

2. 进阶题

题目2:使用Nuxt.js创建ISR页面

Vue
<!-- pages/index.vue -->
<template>
  <div>
    <!-- 渲染数据 -->
  </div>
</template>

<script setup>
// 获取数据并设置ISR
</script>

3. 面试题

题目3:解释SSR、SSG、ISR的区别

JavaScript
// 答案要点:
// SSR (Server-Side Rendering):
// - 每次请求都在服务器上渲染
// - 适合动态内容、个性化内容
// - 首屏加载快,SEO好

// SSG (Static Site Generation):
// - 构建时生成静态HTML
// - 适合静态内容、博客、文档
// - 性能最好,CDN友好

// ISR (Incremental Static Regeneration):
// - 静态生成 + 按需更新
// - 适合内容不经常变化但需要更新的页面
// - 结合SSG和SSR的优点

🎯 本章总结

本章节全面介绍了服务端渲染的各种技术和方案,包括Next.js、Nuxt.js、SSR、SSG、ISR等。关键要点:

  1. 渲染概念:理解SSR、SSG、ISR、CSR的区别
  2. Next.js:掌握Next.js服务端渲染和数据获取
  3. Nuxt.js:掌握Nuxt.js服务端渲染和数据获取
  4. 渲染模式:理解不同渲染模式的适用场景
  5. 性能优化:掌握服务端渲染优化策略
  6. 最佳实践:理解服务端渲染最佳实践

下一步将深入学习前端安全。