Skip to content

第3章:React Server 基础概念详解

3.1 组件分类与边界划分

组件类型分类图

Server Components 详解

定义特征:

  • 只在服务器端执行
  • 可以直接访问后端资源(数据库、文件系统、API)
  • 不包含在客户端 Bundle 中
  • 不能使用浏览器专有 API
  • 不能使用状态(useState)或效果(useEffect)
javascript
// ✅ 正确的 Server Component 示例
async function ProductList({ category }) {
  // 直接访问数据库
  const products = await db.products.findByCategory(category);
  
  // 直接访问文件系统
  const config = await fs.readFile('./config.json', 'utf8');
  
  // 直接调用内部 API
  const analytics = await internalAPI.getAnalytics(category);
  
  return (
    <div className="product-list">
      <h2>{category} 产品列表</h2>
      {products.map(product => (
        <ProductCard 
          key={product.id} 
          product={product}
          analytics={analytics[product.id]}
        />
      ))}
    </div>
  );
}

// ✅ 另一个 Server Component 示例
async function BlogPost({ slug }) {
  // 服务器端数据获取
  const [post, author, comments] = await Promise.all([
    cms.posts.findBySlug(slug),
    cms.authors.findById(post.authorId),
    cms.comments.findByPostId(post.id)
  ]);
  
  return (
    <article>
      <header>
        <h1>{post.title}</h1>
        <AuthorInfo author={author} />
        <PublishDate date={post.publishedAt} />
      </header>
      
      <main>
        <PostContent content={post.content} />
      </main>
      
      <footer>
        <CommentList comments={comments} />
        {/* 客户端组件用于交互 */}
        <CommentForm postId={post.id} />
      </footer>
    </article>
  );
}

Server Components 的限制:

javascript
// ❌ Server Component 中不能使用的功能

// 1. 不能使用状态
function ServerComponent() {
  const [count, setCount] = useState(0); // ❌ 错误
  return <div>{count}</div>;
}

// 2. 不能使用效果
function ServerComponent() {
  useEffect(() => { // ❌ 错误
    console.log('mounted');
  }, []);
  return <div>Hello</div>;
}

// 3. 不能使用浏览器 API
function ServerComponent() {
  const handleClick = () => {
    window.alert('Hello'); // ❌ 错误:window 不存在
  };
  return <button onClick={handleClick}>Click</button>;
}

// 4. 不能使用事件处理器
function ServerComponent() {
  return (
    <button onClick={() => console.log('clicked')}> {/* ❌ 错误 */}
      Click me
    </button>
  );
}

Client Components 详解

定义特征:

  • 在客户端执行(也可能在服务器端预渲染)
  • 包含在客户端 Bundle 中
  • 可以使用所有 React 功能(状态、效果、事件处理)
  • 可以访问浏览器 API
  • 需要 'use client' 指令声明
javascript
// ✅ 正确的 Client Component 示例
'use client';

import { useState, useEffect } from 'react';

function InteractiveCounter({ initialValue = 0 }) {
  const [count, setCount] = useState(initialValue);
  const [isClient, setIsClient] = useState(false);
  
  // 客户端效果
  useEffect(() => {
    setIsClient(true);
    
    // 访问浏览器 API
    const savedCount = localStorage.getItem('counter');
    if (savedCount) {
      setCount(parseInt(savedCount, 10));
    }
  }, []);
  
  // 事件处理
  const handleIncrement = () => {
    const newCount = count + 1;
    setCount(newCount);
    
    // 保存到本地存储
    localStorage.setItem('counter', newCount.toString());
  };
  
  const handleDecrement = () => {
    const newCount = count - 1;
    setCount(newCount);
    localStorage.setItem('counter', newCount.toString());
  };
  
  // 防止 hydration 不匹配
  if (!isClient) {
    return <div>计数器: {initialValue}</div>;
  }
  
  return (
    <div className="counter">
      <h3>交互式计数器</h3>
      <div className="count-display">{count}</div>
      <div className="controls">
        <button onClick={handleDecrement}>-</button>
        <button onClick={handleIncrement}>+</button>
      </div>
    </div>
  );
}

// ✅ 复杂的 Client Component 示例
'use client';

import { useState, useEffect, useCallback } from 'react';

function ChatWidget({ userId, initialMessages = [] }) {
  const [messages, setMessages] = useState(initialMessages);
  const [newMessage, setNewMessage] = useState('');
  const [isConnected, setIsConnected] = useState(false);
  const [ws, setWs] = useState(null);
  
  // WebSocket 连接
  useEffect(() => {
    const websocket = new WebSocket(`ws://localhost:3001/chat/${userId}`);
    
    websocket.onopen = () => {
      setIsConnected(true);
      setWs(websocket);
    };
    
    websocket.onmessage = (event) => {
      const message = JSON.parse(event.data);
      setMessages(prev => [...prev, message]);
    };
    
    websocket.onclose = () => {
      setIsConnected(false);
      setWs(null);
    };
    
    return () => {
      websocket.close();
    };
  }, [userId]);
  
  // 发送消息
  const sendMessage = useCallback(() => {
    if (newMessage.trim() && ws && isConnected) {
      const message = {
        id: Date.now(),
        userId,
        content: newMessage,
        timestamp: new Date().toISOString()
      };
      
      ws.send(JSON.stringify(message));
      setMessages(prev => [...prev, message]);
      setNewMessage('');
    }
  }, [newMessage, ws, isConnected, userId]);
  
  // 键盘事件处理
  const handleKeyPress = (e) => {
    if (e.key === 'Enter' && !e.shiftKey) {
      e.preventDefault();
      sendMessage();
    }
  };
  
  return (
    <div className="chat-widget">
      <div className="chat-header">
        <h4>聊天室</h4>
        <div className={`status ${isConnected ? 'connected' : 'disconnected'}`}>
          {isConnected ? '已连接' : '未连接'}
        </div>
      </div>
      
      <div className="messages">
        {messages.map(message => (
          <div key={message.id} className="message">
            <span className="timestamp">
              {new Date(message.timestamp).toLocaleTimeString()}
            </span>
            <span className="content">{message.content}</span>
          </div>
        ))}
      </div>
      
      <div className="input-area">
        <textarea
          value={newMessage}
          onChange={(e) => setNewMessage(e.target.value)}
          onKeyPress={handleKeyPress}
          placeholder="输入消息..."
          disabled={!isConnected}
        />
        <button 
          onClick={sendMessage}
          disabled={!newMessage.trim() || !isConnected}
        >
          发送
        </button>
      </div>
    </div>
  );
}

Shared Components

定义特征:

  • 可以在服务器和客户端都使用
  • 通常是纯函数组件
  • 不依赖特定环境的 API
  • 主要用于工具函数和类型定义
javascript
// ✅ Shared Component 示例

// 1. 纯函数组件
function formatDate(date) {
  return new Intl.DateTimeFormat('zh-CN', {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  }).format(new Date(date));
}

function DateDisplay({ date, className }) {
  return (
    <time className={className} dateTime={date}>
      {formatDate(date)}
    </time>
  );
}

// 2. 工具组件
function Avatar({ src, alt, size = 'medium' }) {
  const sizeClasses = {
    small: 'w-8 h-8',
    medium: 'w-12 h-12',
    large: 'w-16 h-16'
  };
  
  return (
    <img 
      src={src} 
      alt={alt}
      className={`rounded-full ${sizeClasses[size]}`}
    />
  );
}

// 3. 布局组件
function Card({ children, className = '', ...props }) {
  return (
    <div 
      className={`bg-white rounded-lg shadow-md p-6 ${className}`}
      {...props}
    >
      {children}
    </div>
  );
}

// 4. 类型定义(TypeScript)
interface User {
  id: string;
  name: string;
  email: string;
  avatar?: string;
}

interface Post {
  id: string;
  title: string;
  content: string;
  authorId: string;
  publishedAt: string;
  tags: string[];
}

3.2 渲染机制深入解析

RSC 渲染流程图

组件树渲染过程

数据流向详解

javascript
// 数据流示例:博客应用

// 1. 根 Server Component
async function BlogApp({ slug }) {
  // 服务器端数据获取
  const post = await db.posts.findBySlug(slug);
  const author = await db.users.findById(post.authorId);
  
  return (
    <div className="blog-app">
      {/* Server Component - 静态内容 */}
      <BlogHeader post={post} author={author} />
      
      {/* Server Component - 文章内容 */}
      <BlogContent content={post.content} />
      
      {/* Client Component - 交互功能 */}
      <BlogInteractions 
        postId={post.id}
        initialLikes={post.likes}
        initialComments={post.commentCount}
      />
    </div>
  );
}

// 2. Server Component - 博客头部
async function BlogHeader({ post, author }) {
  // 可以进行额外的数据获取
  const authorStats = await db.users.getStats(author.id);
  
  return (
    <header className="blog-header">
      <h1>{post.title}</h1>
      <div className="author-info">
        <Avatar src={author.avatar} alt={author.name} />
        <div>
          <h3>{author.name}</h3>
          <p>{authorStats.totalPosts} 篇文章</p>
        </div>
      </div>
      <time>{formatDate(post.publishedAt)}</time>
    </header>
  );
}

// 3. Server Component - 文章内容
function BlogContent({ content }) {
  // 纯渲染逻辑,无需数据获取
  return (
    <main className="blog-content">
      <div dangerouslySetInnerHTML={{ __html: content }} />
    </main>
  );
}

// 4. Client Component - 交互功能
'use client';

function BlogInteractions({ postId, initialLikes, initialComments }) {
  const [likes, setLikes] = useState(initialLikes);
  const [hasLiked, setHasLiked] = useState(false);
  const [showComments, setShowComments] = useState(false);
  
  const handleLike = async () => {
    if (!hasLiked) {
      setLikes(prev => prev + 1);
      setHasLiked(true);
      
      // API 调用
      await fetch(`/api/posts/${postId}/like`, {
        method: 'POST'
      });
    }
  };
  
  return (
    <div className="blog-interactions">
      <button 
        onClick={handleLike}
        className={`like-button ${hasLiked ? 'liked' : ''}`}
      >
        ❤️ {likes}
      </button>
      
      <button 
        onClick={() => setShowComments(!showComments)}
        className="comments-button"
      >
        💬 {initialComments} 评论
      </button>
      
      {showComments && (
        <CommentSection postId={postId} />
      )}
    </div>
  );
}

Flight Protocol 数据格式

javascript
// Flight Protocol 序列化示例

// 原始组件树
const componentTree = (
  <BlogApp slug="react-server-components">
    <BlogHeader post={post} author={author} />
    <BlogContent content={post.content} />
    <BlogInteractions 
      postId={post.id}
      initialLikes={post.likes}
      initialComments={post.commentCount}
    />
  </BlogApp>
);

// 序列化后的 Flight 数据
const flightPayload = {
  // 组件树结构
  "0": {
    type: "div",
    props: {
      className: "blog-app",
      children: ["1", "2", "3"]
    }
  },
  
  // BlogHeader 渲染结果
  "1": {
    type: "header",
    props: {
      className: "blog-header",
      children: [
        {
          type: "h1",
          props: { children: "React Server Components 深入解析" }
        },
        {
          type: "div",
          props: {
            className: "author-info",
            children: [/* 作者信息 */]
          }
        }
      ]
    }
  },
  
  // BlogContent 渲染结果
  "2": {
    type: "main",
    props: {
      className: "blog-content",
      children: {
        type: "div",
        props: {
          dangerouslySetInnerHTML: {
            __html: "<p>文章内容...</p>"
          }
        }
      }
    }
  },
  
  // BlogInteractions 客户端组件引用
  "3": {
    $$typeof: Symbol.for("react.element"),
    type: {
      $$typeof: Symbol.for("react.client.reference"),
      name: "BlogInteractions",
      id: "./components/BlogInteractions.js"
    },
    props: {
      postId: "123",
      initialLikes: 42,
      initialComments: 8
    }
  },
  
  // 客户端组件映射
  clientReferences: {
    "./components/BlogInteractions.js": {
      id: "chunk-interactions",
      chunks: ["chunk-interactions.js"],
      name: "BlogInteractions"
    }
  }
};

3.3 数据获取模式

数据获取策略对比

并行数据获取示例

javascript
// ✅ RSC 中的高效数据获取

// 1. 并行数据获取
async function UserDashboard({ userId }) {
  // 并行获取多个数据源
  const [user, posts, notifications, analytics] = await Promise.all([
    db.users.findById(userId),
    db.posts.findByUserId(userId),
    db.notifications.findByUserId(userId),
    analytics.getUserStats(userId)
  ]);
  
  return (
    <div className="dashboard">
      <UserProfile user={user} analytics={analytics} />
      <PostList posts={posts} />
      <NotificationCenter notifications={notifications} />
    </div>
  );
}

// 2. 嵌套数据获取
async function PostList({ posts }) {
  // 为每个帖子获取额外数据
  const postsWithStats = await Promise.all(
    posts.map(async (post) => {
      const [likes, comments] = await Promise.all([
        db.likes.countByPostId(post.id),
        db.comments.countByPostId(post.id)
      ]);
      
      return {
        ...post,
        likesCount: likes,
        commentsCount: comments
      };
    })
  );
  
  return (
    <div className="post-list">
      {postsWithStats.map(post => (
        <PostCard key={post.id} post={post} />
      ))}
    </div>
  );
}

// 3. 条件数据获取
async function PostCard({ post }) {
  // 根据条件获取不同数据
  let additionalData = null;
  
  if (post.type === 'premium') {
    additionalData = await db.premium.getPostData(post.id);
  } else if (post.type === 'sponsored') {
    additionalData = await db.sponsors.getPostData(post.id);
  }
  
  return (
    <article className="post-card">
      <h3>{post.title}</h3>
      <p>{post.excerpt}</p>
      
      {additionalData && (
        <div className="additional-info">
          {/* 渲染额外数据 */}
        </div>
      )}
      
      <PostInteractions postId={post.id} />
    </article>
  );
}

数据缓存策略

javascript
// 数据缓存实现示例

// 1. 内存缓存
const cache = new Map();

async function getCachedUserData(userId) {
  const cacheKey = `user:${userId}`;
  
  if (cache.has(cacheKey)) {
    return cache.get(cacheKey);
  }
  
  const userData = await db.users.findById(userId);
  cache.set(cacheKey, userData);
  
  // 设置过期时间
  setTimeout(() => {
    cache.delete(cacheKey);
  }, 5 * 60 * 1000); // 5分钟
  
  return userData;
}

// 2. Redis 缓存
import Redis from 'ioredis';

const redis = new Redis(process.env.REDIS_URL);

async function getCachedPostData(postId) {
  const cacheKey = `post:${postId}`;
  
  // 尝试从 Redis 获取
  const cached = await redis.get(cacheKey);
  if (cached) {
    return JSON.parse(cached);
  }
  
  // 从数据库获取
  const postData = await db.posts.findById(postId);
  
  // 存储到 Redis(1小时过期)
  await redis.setex(cacheKey, 3600, JSON.stringify(postData));
  
  return postData;
}

// 3. 使用缓存的 Server Component
async function CachedUserProfile({ userId }) {
  const user = await getCachedUserData(userId);
  const posts = await getCachedPostData(userId);
  
  return (
    <div className="user-profile">
      <h2>{user.name}</h2>
      <p>{user.bio}</p>
      <div className="user-stats">
        <span>{posts.length} 篇文章</span>
        <span>{user.followers} 关注者</span>
      </div>
    </div>
  );
}

3.4 错误处理与边界

错误边界层次图

Server Component 错误处理

javascript
// Server Component 错误处理示例

// 1. 基础错误处理
async function UserProfile({ userId }) {
  try {
    const user = await db.users.findById(userId);
    
    if (!user) {
      return (
        <div className="error-state">
          <h3>用户不存在</h3>
          <p>请检查用户 ID 是否正确</p>
        </div>
      );
    }
    
    return (
      <div className="user-profile">
        <h2>{user.name}</h2>
        <p>{user.bio}</p>
      </div>
    );
  } catch (error) {
    console.error('获取用户数据失败:', error);
    
    return (
      <div className="error-state">
        <h3>加载失败</h3>
        <p>无法加载用户信息,请稍后重试</p>
      </div>
    );
  }
}

// 2. 带降级的错误处理
async function PostList({ userId }) {
  try {
    const posts = await db.posts.findByUserId(userId);
    return <PostGrid posts={posts} />;
  } catch (error) {
    console.error('获取帖子失败:', error);
    
    // 尝试获取缓存数据
    try {
      const cachedPosts = await cache.get(`posts:${userId}`);
      if (cachedPosts) {
        return (
          <div>
            <div className="warning-banner">
              显示的是缓存数据,可能不是最新的
            </div>
            <PostGrid posts={cachedPosts} />
          </div>
        );
      }
    } catch (cacheError) {
      console.error('缓存获取失败:', cacheError);
    }
    
    // 最终降级 UI
    return (
      <div className="error-state">
        <h3>暂时无法加载帖子</h3>
        <p>请稍后刷新页面重试</p>
      </div>
    );
  }
}

// 3. 错误边界组件
'use client';

import { Component } from 'react';

class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }
  
  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }
  
  componentDidCatch(error, errorInfo) {
    console.error('错误边界捕获到错误:', error, errorInfo);
    
    // 错误上报
    this.reportError(error, errorInfo);
  }
  
  reportError = (error, errorInfo) => {
    // 发送错误报告到监控服务
    fetch('/api/error-report', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        error: error.toString(),
        stack: error.stack,
        componentStack: errorInfo.componentStack,
        timestamp: new Date().toISOString()
      })
    }).catch(reportError => {
      console.error('错误上报失败:', reportError);
    });
  };
  
  render() {
    if (this.state.hasError) {
      return (
        <div className="error-boundary">
          <h2>出现了一些问题</h2>
          <p>我们已经记录了这个错误,请稍后重试</p>
          <button 
            onClick={() => this.setState({ hasError: false, error: null })}
          >
            重试
          </button>
        </div>
      );
    }
    
    return this.props.children;
  }
}

// 使用错误边界
function App() {
  return (
    <ErrorBoundary>
      <Header />
      <main>
        <ErrorBoundary>
          <UserDashboard userId="123" />
        </ErrorBoundary>
        <ErrorBoundary>
          <PostList userId="123" />
        </ErrorBoundary>
      </main>
      <Footer />
    </ErrorBoundary>
  );
}

3.5 性能优化策略

性能优化层次图

具体优化实现

javascript
// 1. 数据库查询优化

// ❌ 低效的 N+1 查询
async function PostListBad({ userId }) {
  const posts = await db.posts.findByUserId(userId);
  
  const postsWithAuthors = [];
  for (const post of posts) {
    const author = await db.users.findById(post.authorId); // N+1 问题
    postsWithAuthors.push({ ...post, author });
  }
  
  return <PostGrid posts={postsWithAuthors} />;
}

// ✅ 优化的批量查询
async function PostListGood({ userId }) {
  const posts = await db.posts.findByUserId(userId);
  
  // 批量获取作者信息
  const authorIds = [...new Set(posts.map(post => post.authorId))];
  const authors = await db.users.findByIds(authorIds);
  const authorMap = new Map(authors.map(author => [author.id, author]));
  
  const postsWithAuthors = posts.map(post => ({
    ...post,
    author: authorMap.get(post.authorId)
  }));
  
  return <PostGrid posts={postsWithAuthors} />;
}

// 2. 智能缓存实现
class SmartCache {
  constructor() {
    this.cache = new Map();
    this.timestamps = new Map();
    this.ttl = 5 * 60 * 1000; // 5分钟
  }
  
  set(key, value, customTTL) {
    this.cache.set(key, value);
    this.timestamps.set(key, Date.now() + (customTTL || this.ttl));
  }
  
  get(key) {
    if (!this.cache.has(key)) {
      return null;
    }
    
    const expiry = this.timestamps.get(key);
    if (Date.now() > expiry) {
      this.cache.delete(key);
      this.timestamps.delete(key);
      return null;
    }
    
    return this.cache.get(key);
  }
  
  invalidate(pattern) {
    for (const key of this.cache.keys()) {
      if (key.includes(pattern)) {
        this.cache.delete(key);
        this.timestamps.delete(key);
      }
    }
  }
}

const smartCache = new SmartCache();

// 3. 流式渲染优化
async function OptimizedPage({ params }) {
  // 立即可用的数据
  const basicData = await getBasicData(params.id);
  
  return (
    <div>
      {/* 立即渲染的内容 */}
      <PageHeader data={basicData} />
      
      {/* 延迟加载的内容 */}
      <Suspense fallback={<SkeletonLoader />}>
        <HeavyContent id={params.id} />
      </Suspense>
      
      <Suspense fallback={<SkeletonLoader />}>
        <AnotherHeavyContent id={params.id} />
      </Suspense>
    </div>
  );
}

// 4. 预加载策略
function PreloadingExample() {
  return (
    <div>
      {/* 预加载关键资源 */}
      <link rel="preload" href="/api/critical-data" as="fetch" />
      <link rel="prefetch" href="/api/secondary-data" as="fetch" />
      
      {/* 预加载组件 */}
      <link 
        rel="modulepreload" 
        href="/chunks/important-component.js" 
      />
    </div>
  );
}

小结

本章深入探讨了 React Server 的核心概念:

  1. 组件分类:Server Components、Client Components 和 Shared Components 的特点和使用场景
  2. 渲染机制:RSC 的渲染流程和 Flight Protocol 的工作原理
  3. 数据获取:高效的数据获取模式和缓存策略
  4. 错误处理:完善的错误边界和降级机制
  5. 性能优化:从服务器到客户端的全链路优化策略

下一章我们将深入研究传统 SSR 的实现细节,为理解 RSC 的优势打下基础。


微信公众号二维码