React Server Components实战:服务端渲染数据获取与客户端水合方案

React Server Components(RSC)是React 18引入的架构特性,允许组件在服务端渲染并流式传输到客户端。RSC将组件分为Server Components和Client Components两类,Server Components直接在服务端执行,可访问数据库和文件系统,无需打包到客户端JS bundle中。这种架构减少了发送到浏览器的JavaScript体积,首屏加载速度显著提升。

Server Components与Client Components的区别

Server Components运行在服务端,渲染结果以序列化数据流传输到客户端,不能使用useState、useEffect等客户端API。Client Components通过’use client’指令声明,在浏览器中运行,支持事件处理和状态管理。两类组件可嵌套使用,但Server Components不能导入Client Components的服务端实现。

// app/page.tsx - Server Component(默认)
import { db } from '@/lib/database'
import PostList from '@/components/PostList'

// 直接在服务端查询数据库,无需API层
async function getPosts() {
  const posts = await db.query('SELECT * FROM posts ORDER BY created_at DESC LIMIT 20')
  return posts
}

export default async function HomePage() {
  const posts = await getPosts()
  return (
    <main>
      <h1>最新文章</h1>
      <PostList posts={posts} />
    </main>
  )
}

// components/PostList.tsx - Client Component
'use client'

import { useState } from 'react'

interface Post {
  id: number
  title: string
  content: string
}

export default function PostList({ posts }: { posts: Post[] }) {
  const [selectedId, setSelectedId] = useState<number | null>(null)
  const [searchTerm, setSearchTerm] = useState('')
  
  const filtered = posts.filter(p => 
    p.title.toLowerCase().includes(searchTerm.toLowerCase())
  )
  
  return (
    <div>
      <input 
        type="text"
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
        placeholder="搜索文章..."
      />
      <ul>
        {filtered.map(post => (
          <li 
            key={post.id}
            onClick={() => setSelectedId(post.id)}
            className={selectedId === post.id ? 'active' : ''}
          >
            {post.title}
          </li>
        ))}
      </ul>
    </div>
  )
}

数据获取模式:fetch缓存与流式渲染

// app/blog/page.tsx - 带缓存的数据获取
import { Suspense } from 'react'

async function getBlogPosts() {
  const res = await fetch('https://api.example.com/posts', {
    next: { 
      revalidate: 3600,
      tags: ['blog-posts']
    }
  })
  return res.json()
}

// 流式渲染:Suspense分块传输
export default function BlogPage() {
  return (
    <div>
      <Suspense fallback={<div>加载文章中...</div>}>
        <BlogList />
      </Suspense>
      <Suspense fallback={<div>加载评论中...</div>}>
        <Comments />
      </Suspense>
    </div>
  )
}

// 按需刷新缓存
import { revalidateTag } from 'next/cache'

export async function POST() {
  revalidateTag('blog-posts')
  return Response.json({ revalidated: true })
}

// 静态生成 + 动态渲染混合
export async function generateStaticParams() {
  const posts = await getBlogPosts()
  return posts.slice(0, 100).map(post => ({
    id: post.id.toString()
  }))
}

客户端水合与交互状态衔接

// 水合不匹配的常见场景与处理
'use client'

import { useState, useEffect } from 'react'

export default function CurrentTime() {
  const [time, setTime] = useState<string>('')
  
  useEffect(() => {
    setTime(new Date().toLocaleTimeString())
    const timer = setInterval(() => {
      setTime(new Date().toLocaleTimeString())
    }, 1000)
    return () => clearInterval(timer)
  }, [])
  
  // 使用suppressHydrationWarning避免水合警告
  return (
    <span suppressHydrationWarning>
      {time || '加载中...'}
    </span>
  )
}

// 动态导入避免水合错误
import dynamic from 'next/dynamic'

const ClientOnlyChart = dynamic(
  () => import('@/components/Chart'),
  { ssr: false, loading: () => <div>图表加载中...</div> }
)

// 使用useEffect检测水合完成
function useHydrated() {
  const [hydrated, setHydrated] = useState(false)
  useEffect(() => setHydrated(true), [])
  return hydrated
}

function Navigation() {
  const hydrated = useHydrated()
  return (
    <nav>
      {hydrated && <UserMenu user={getCurrentUser()} />}
    </nav>
  )
}

Server Actions:服务端函数调用

// app/actions.ts - Server Actions定义
'use server'

import { revalidatePath } from 'next/cache'
import { z } from 'zod'

const postSchema = z.object({
  title: z.string().min(1).max(100),
  content: z.string().min(1).max(10000),
})

export async function createPost(formData: FormData) {
  const parsed = postSchema.parse({
    title: formData.get('title'),
    content: formData.get('content'),
  })
  
  await db.execute(
    'INSERT INTO posts (title, content) VALUES (?, ?)',
    [parsed.title, parsed.content]
  )
  revalidatePath('/blog')
}

// app/blog/new/page.tsx
import { createPost } from '@/app/actions'

export default function NewPostPage() {
  return (
    <form action={createPost}>
      <input type="text" name="title" placeholder="文章标题" required />
      <textarea name="content" placeholder="文章内容" required />
      <button type="submit">发布</button>
    </form>
  )
}

// 带进度状态的Server Action
'use client'
import { useTransition } from 'react'

function DeleteButton({ postId }: { postId: number }) {
  const [isPending, startTransition] = useTransition()
  return (
    <button
      disabled={isPending}
      onClick={() => startTransition(async () => {
        await deletePost(postId)
      })}
    >
      {isPending ? '删除中...' : '删除'}
    </button>
  )
}

性能优化:bundle分析与代码分割

// next.config.js
const nextConfig = {
  experimental: {
    optimizePackageImports: ['lodash', 'date-fns'],
  },
}

// 路由级代码分割
import { lazy } from 'react'

const HeavyDashboard = lazy(() => import('./HeavyDashboard'))

export default function Layout({ children }) {
  return (
    <html>
      <body>
        {children}
        <Suspense fallback={null}>
          <HeavyDashboard />
        </Suspense>
      </body>
    </html>
  )
}

// 关键指标对比(App Router vs Pages Router)
// 首屏JS体积:  180KB -> 95KB  (减少47%)
// TTFB:       420ms -> 180ms (减少57%)
// FCP:        1.2s  -> 0.6s  (减少50%)
// TTI:        2.8s  -> 1.5s  (减少46%)

React Server Components重新定义了前端渲染架构的边界。Server Components承担数据获取和静态渲染,Client Components聚焦交互逻辑,两者通过props传递数据。迁移到App Router时,核心原则是默认使用Server Component,仅在需要交互时添加’use client’。Suspense边界划分流式渲染的分块,优先传输首屏可见内容。Server Actions简化了表单提交和数据变更流程,减少API路由的样板代码。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/reactservercomponents-shi-zhan-fu-wu-duan-xuan-ran-shu-ju/

(0)
小编小编
上一篇 5小时前
下一篇 5小时前

相关推荐