react

React Server Components 与 SSR

By AI-Writer 14 min read

前言

React Server Components(RSC) 是 React 18 引入的重大特性,代表了 React 渲染架构的根本性变革。本篇文章将深入讲解 RSC 原理、服务端与客户端组件边界、流式渲染,以及 Next.js App Router 的实践应用。

渲染模式演进

传统 CSR vs SSR

plaintext
CSR (Client-Side Rendering)
┌─────────────────────────────┐
│  服务器返回空白 HTML        │
│  ↓                          │
│  浏览器下载 JS Bundle        │
│  ↓                          │
│  React 执行,生成 DOM        │
│  ↓                          │
│  用户看到内容                │
└─────────────────────────────┘
首次内容绘制 (FCP) 较慢

SSR (Server-Side Rendering)
┌─────────────────────────────┐
│  服务器执行 React,生成 HTML │
│  ↓                          │
│  返回完整 HTML               │
│  ↓                          │
│  浏览器直接显示内容          │
│  ↓                          │
│  下载 JS, hydration         │
│  ↓                          │
│  应用可交互                  │
└─────────────────────────────┘
FCP 快,但 TTI 需要等 hydration

RSC 的核心理念

RSC 将组件分为两类:

  • Server Components:仅在服务端运行,可以直接访问数据库、文件系统
  • Client Components:在客户端运行,支持交互和状态管理
plaintext
RSC (React Server Components)
┌─────────────────────────────────────┐
│  服务器直接渲染组件树                │
│  ↓                                  │
│  流式传输 HTML 给浏览器              │
│  ↓                                  │
│  选择性 hydration(仅必要部分)      │
│  ↓                                  │
│  更快的 FCP,更少的 JS               │
└─────────────────────────────────────┘

Server Components

基础语法

tsx
// app/components/ProductList.tsx
// 这是 Server Component(默认)
// 无需 'use client' 指令

async function ProductList() {
  // 直接访问数据库,无需 API
  const products = await db.products.findMany({
    orderBy: { createdAt: 'desc' },
    take: 10,
  });

  return (
    <ul>
      {products.map(product => (
        <li key={product.id}>
          <h3>{product.name}</h3>
          <p>{product.price}</p>
        </li>
      ))}
    </ul>
  );
}

Server Component 特点

tsx
// ✓ 可以做的
async function Component() {
  const data = await fetch('/api/data');      // 数据获取
  const file = fs.readFileSync('config.json'); // 文件系统
  const dbData = await db.query();            // 直接数据库访问
  const secret = process.env.SECRET;          // 服务端 secret

  return <div>{/* ... */}</div>;
}

// ❌ 不能做的
function Component() {
  const [state, setState] = useState(0);    // 不能用 hooks
  const ref = useRef();                       // 不能用 DOM ref
  useEffect(() => {}, []);                    // 不能有副作用

  return <button onClick={() => setState(1)}>点击</button>;
}

Client Components

声明客户端组件

tsx
// components/AddToCart.tsx
'use client';

import { useState } from 'react';

function AddToCart({ productId }) {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount(c => c - 1)}>-</button>
      <span>{count}</span>
      <button onClick={() => setCount(c => c + 1)}>+</button>
      <button onClick={() => addToCart(productId, count)}>
        加入购物车
      </button>
    </div>
  );
}

混用 Server 与 Client 组件

tsx
// app/products/[id]/page.tsx (Server Component)
async function ProductPage({ params }) {
  const product = await getProduct(params.id);

  return (
    <div>
      {/* Server Component: 静态内容 */}
      <ProductInfo product={product} />

      {/* Client Component: 交互部分 */}
      <AddToCart productId={product.id} />

      {/* Client Component: 评论区 */}
      <Reviews productId={product.id} />

      {/* Client Component: 分享按钮 */}
      <ShareButton url={`/products/${product.id}`} />
    </div>
  );
}

传递 props 给 Client Component

tsx
// Server Component (page.tsx)
async function Page() {
  const data = await fetchData();

  // ✓ 可以传递可序列化的 props
  return <ClientComponent
    initialData={data}
    config={{ theme: 'dark' }}
    items={['a', 'b', 'c']}
  />;

  // ❌ 不能传递函数(非序列化)
  // return <ClientComponent onClick={() => {}} />;
}

// Client Component
'use client';

function ClientComponent({ initialData, config, items }) {
  // 使用从 Server Component 传来的数据
  return <div>{/* ... */}</div>;
}

Next.js App Router

目录结构

plaintext
app/
├── layout.tsx          # 根布局
├── page.tsx            # 首页 /
├── products/
│   ├── page.tsx        # 商品列表 /products
│   ├── layout.tsx      # 商品列表布局
│   └── [id]/
│       ├── page.tsx    # 商品详情 /products/123
│       ├── loading.tsx    # 加载状态
│       └── error.tsx       # 错误边界
├── about/
│   └── page.tsx        # 关于页 /about
└── api/
    └── products/
        └── route.ts    # API 路由

layout.tsx

tsx
// app/layout.tsx
import './globals.css';

export const metadata = {
  title: 'My App',
  description: 'A React RSC app',
};

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

page.tsx

tsx
// app/page.tsx
async function HomePage() {
  // 并行获取多个数据源
  const [featuredProducts, latestNews, categories] = await Promise.all([
    getFeaturedProducts(),
    getLatestNews(),
    getCategories(),
  ]);

  return (
    <>
      <Hero />
      <ProductGrid products={featuredProducts} />
      <NewsList news={latestNews} />
      <CategoryNav categories={categories} />
    </>
  );
}

动态路由

tsx
// app/products/[id]/page.tsx
// 生成静态路径
export async function generateStaticParams() {
  const products = await db.products.findMany({
    select: { id: true },
  });

  return products.map(p => ({ id: p.id.toString() }));
}

// 动态页面
async function ProductPage({ params }) {
  const product = await getProduct(params.id);

  if (!product) {
    notFound();
  }

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
    </div>
  );
}

流式渲染

Suspense 边界

tsx
// app/page.tsx
import { Suspense } from 'react';

async function HomePage() {
  return (
    <div>
      {/* 立即显示的静态内容 */}
      <Hero />

      {/* 异步加载的部分用 Suspense 包裹 */}
      <Suspense fallback={<ProductSkeleton />}>
        <ProductRecommendations />
      </Suspense>

      <Suspense fallback={<NewsSkeleton />}>
        <LatestNews />
      </Suspense>
    </div>
  );
}

Streaming 实现

tsx
// app/blog/[slug]/page.tsx
import { Suspense } from 'react';

async function BlogPost({ params }) {
  const post = await getPost(params.slug);

  return (
    <article>
      <header>
        <h1>{post.title}</h1>
        <p>{post.author}</p>
      </header>

      {/* 文章内容可以立即加载 */}
      <div dangerouslySetInnerHTML={{ __html: post.content }} />

      {/* 评论可能较慢,使用 Suspense */}
      <Suspense fallback={<CommentsSkeleton />}>
        <Comments postId={post.id} />
      </Suspense>

      {/* 相关推荐 */}
      <Suspense fallback={<RecommendationsSkeleton />}>
        <RelatedPosts category={post.category} />
      </Suspense>
    </article>
  );
}

Skeleton 组件

tsx
// app/components/ProductSkeleton.tsx
function ProductSkeleton() {
  return (
    <div className="animate-pulse">
      <div className="aspect-video bg-gray-200 rounded" />
      <div className="h-4 bg-gray-200 rounded mt-2" />
      <div className="h-4 bg-gray-200 rounded w-3/4 mt-2" />
    </div>
  );
}

// loading.tsx - 路由级别的加载状态
// app/products/loading.tsx
export default function ProductsLoading() {
  return (
    <div className="grid grid-cols-4 gap-4">
      {Array.from({ length: 8 }).map((_, i) => (
        <ProductSkeleton key={i} />
      ))}
    </div>
  );
}

数据获取模式

服务端直接查询

tsx
// 最直接的数据获取
async function ProductList() {
  const products = await db.query(`
    SELECT * FROM products
    WHERE status = 'active'
    ORDER BY created_at DESC
    LIMIT 20
  `);

  return <ProductGrid products={products} />;
}

fetch 扩展

tsx
// Next.js 扩展了 fetch,自动缓存
async function Component() {
  // 默认缓存
  const data1 = await fetch('https://api.example.com/data');

  // 重新验证:每 60 秒
  const data2 = await fetch('https://api.example.com/data', {
    next: { revalidate: 60 },
  });

  // 动态:每次都获取最新
  const data3 = await fetch('https://api.example.com/data', {
    cache: 'no-store',
  });

  return <div>{/* ... */}</div>;
}

use() Hook(客户端数据获取)

tsx
'use client';

import { use } from 'react';

function CartItems() {
  // 在 Client Component 中使用 Promise
  const cart = use(fetchCart());

  return (
    <ul>
      {cart.items.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

状态共享

Server Component 间的状态

tsx
// 使用 context(仅 Server Component 中)
// app/context.ts
import { createContext } from 'react';

export const ThemeContext = createContext('light');

// app/layout.tsx
export default function Layout({ children }) {
  return (
    <ThemeContext.Provider value="dark">
      {children}
    </ThemeContext.Provider>
  );
}

// app/page.tsx
import { ThemeContext } from '../context';

async function Page() {
  // Server Component 中使用 context
  return (
    <ThemeContext.Provider value="light">
      <Content />
    </ThemeContext.Provider>
  );
}

Client Component 状态共享

tsx
// context/providers.tsx
'use client';

import { createContext } from 'react';

export const CartContext = createContext(null);

export function CartProvider({ children }) {
  const [items, setItems] = useState([]);

  const addItem = (product) => {
    setItems(prev => [...prev, product]);
  };

  return (
    <CartContext.Provider value={{ items, addItem }}>
      {children}
    </CartContext.Provider>
  );
}

// app/layout.tsx
import { CartProvider } from './context/providers';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <CartProvider>
          {children}
        </CartProvider>
      </body>
    </html>
  );
}

错误处理

error.tsx

tsx
// app/products/error.tsx
'use client';

import { useEffect } from 'react';

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    // 上报错误
    console.error(error);
  }, [error]);

  return (
    <div>
      <h2>出错了!</h2>
      <p>{error.message}</p>
      <button onClick={() => reset()}>
        重试
      </button>
    </div>
  );
}

notFound()

tsx
// app/products/[id]/page.tsx
import { notFound } from 'next/navigation';

async function ProductPage({ params }) {
  const product = await getProduct(params.id);

  if (!product) {
    notFound();
  }

  return <ProductDetail product={product} />;
}

// app/products/not-found.tsx
export default function ProductNotFound() {
  return (
    <div>
      <h1>商品未找到</h1>
      <p>抱歉,您查找的商品不存在。</p>
    </div>
  );
}

最佳实践

组件边界划分

plaintext
┌─────────────────────────────────────────────┐
│           Server Components                 │
│  ┌───────────────────────────────────────┐ │
│  │  布局结构、数据获取、静态内容           │ │
│  │  - Layout, Page                        │ │
│  │  - 数据密集型组件                       │ │
│  │  - 直接访问 DB/FS/API                  │ │
│  └───────────────────────────────────────┘ │
│                      ↓ props               │
│  ┌───────────────────────────────────────┐ │
│  │           Client Components           │ │
│  │  交互逻辑、状态管理、浏览器 API         │ │
│  │  - Button, Input, Modal               │ │
│  │  - useState, useEffect, useContext    │ │
│  │  - 事件处理器                          │ │
│  └───────────────────────────────────────┘ │
└─────────────────────────────────────────────┘

性能优化建议

  1. 保持 Server Component 为主

    tsx
    // ✓ 优先使用 Server Component
    async function Page() {
      return <ExpensiveComponent />;
    }
    
    // ❌ 过度使用 Client Component
    async function Page() {
      return <ClientWrapper><HeavyDataComponent /></ClientWrapper>;
    }
  2. 减少 Client Component 数量

    tsx
    // ❌ 把整个列表变成 Client Component
    'use client';
    async function ProductList() {
      const products = await db.products.findMany();
      return <ul>{/* ... */}</ul>;
    }
    
    // ✓ 只将交互部分设为 Client Component
    async function ProductList() {
      const products = await db.products.findMany();
      return (
        <ul>
          {products.map(p => (
            <li key={p.id}>
              {p.name}
              <AddToCartButton productId={p.id} /> {/* 只有这里是 client */}
            </li>
          ))}
        </ul>
      );
    }
  3. 避免 prop drilling

    tsx
    // 使用 Context 传递数据,避免深层 prop drilling
    // app/providers.tsx
    'use client';
    export function Providers({ children }) {
      return <SomeContext.Provider>{children}</SomeContext.Provider>;
    }

小结

  • Server Components:仅在服务端运行,可直接访问数据库和文件系统,无交互能力
  • Client Components:使用 'use client' 声明,可在客户端执行交互和状态管理
  • 混用原则:Server Component 为主,需要交互的部分用 Client Component
  • App Router:Next.js 13+ 的新路由系统,原生支持 RSC
  • StreamingSuspense 实现流式渲染,提供渐进式加载体验
  • 最佳实践:保持 Server Component 为主,只将必要的交互部分设为 Client Component

RSC 代表了 React 的未来方向,通过合理的组件边界划分,可以显著提升应用性能并改善开发者体验。

#react #rsc #ssr #nextjs #服务端渲染

评论

A

Written by

AI-Writer

Related Articles

react
#9

Context 与全局状态

深入理解 React Context 的工作原理,掌握 createContext、useContext 的使用,以及何时该用 Context 而非状态提升

Read More
react
#5

条件渲染与列表渲染

掌握 React 中的条件渲染模式(&&、||、三元运算符)与列表渲染(map)技巧,深入理解 Key 的作用与最佳实践

Read More
react
#4

事件处理与绑定

深入理解 React 的合成事件系统、事件绑定方式、传参模式以及事件委托机制,掌握各类交互事件的处理方法

Read More