React Server Components 实战:下一代 SSR 开发指南

React Server Components 实战:下一代 SSR 开发指南


核心理念

  • 组件在服务器渲染并直接输出可序列化的 UI 树片段,客户端仅为交互部分注水
  • 数据靠近服务器与数据库,避免瀑布式请求与冗余水合
  • 按需把交互组件标记为客户端组件,减小包体与提升首屏

技术栈与初始化

bash 复制代码
pnpm create next-app@latest rsc-app --ts --eslint --tailwind --app
cd rsc-app && pnpm dev

目录(App Router):

复制代码
rsc-app/
  app/
    layout.tsx
    page.tsx
    components/
      RepoList.tsx
      InteractiveCounter.tsx
    api/
      items/route.ts
    actions.ts
  lib/
    db.ts
  next.config.js

Server vs Client 组件

  • Server Component:默认;可访问数据库/后端,仅输出可序列化内容
  • Client Component:文件顶部加 use client;可使用状态与事件,不能直接访问服务器资源

Server 组件示例

app/components/RepoList.tsx

tsx 复制代码
import { cache } from 'react'

const fetchRepos = cache(async () => {
  const res = await fetch('https://api.github.com/orgs/vercel/repos', { next: { revalidate: 60 } })
  return res.json() as Promise<Array<{ id:number; name:string }>>
})

export default async function RepoList() {
  const repos = await fetchRepos()
  return (
    <ul>{repos.slice(0, 10).map(r => <li key={r.id}>{r.name}</li>)}</ul>
  )
}

Client 组件示例

app/components/InteractiveCounter.tsx

tsx 复制代码
'use client'
import { useState } from 'react'
export default function InteractiveCounter() {
  const [n, setN] = useState(0)
  return <button onClick={() => setN(n + 1)}>Count: {n}</button>
}

页面与布局

app/page.tsx

tsx 复制代码
import RepoList from './components/RepoList'
import InteractiveCounter from './components/InteractiveCounter'

export default function Page() {
  return (
    <main>
      <h1>RSC Demo</h1>
      <RepoList />
      <InteractiveCounter />
    </main>
  )
}

app/layout.tsx

tsx 复制代码
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="zh">
      <body>{children}</body>
    </html>
  )
}

Streaming 与 Suspense

app/page.tsx

tsx 复制代码
import { Suspense } from 'react'
import RepoList from './components/RepoList'

export default function Page() {
  return (
    <main>
      <h1>Streaming</h1>
      <Suspense fallback={<p>Loading repos...</p>}>
        {/* Server 组件可流式传输 */}
        {/* @ts-expect-error Async Server Component */}
        <RepoList />
      </Suspense>
    </main>
  )
}

数据获取与缓存策略

  • fetchnext: { revalidate } 控制增量静态化;cache: 'no-store' 关闭缓存
  • cache(asyncFn):稳定函数层复用结果,避免重复 IO
  • revalidateTag/revalidatePath:配合标签或路径精细失效

app/actions.ts

tsx 复制代码
'use server'
import { revalidateTag } from 'next/cache'
export async function createItem(formData: FormData) {
  // 写入数据库...
  revalidateTag('items')
}

Server 组件读取:

tsx 复制代码
const res = await fetch('https://api.example.com/items', { next: { tags: ['items'], revalidate: 120 } })

Server Actions(无 API 层的直接提交)

app/actions.ts

tsx 复制代码
'use server'
export async function addTodo(prev: any, formData: FormData) {
  const title = String(formData.get('title') ?? '')
  // 写库...
  return { ok: true, title }
}

app/page.tsx

tsx 复制代码
import { addTodo } from './actions'
export default function Page() {
  return (
    <form action={addTodo}>
      <input name="title" />
      <button type="submit">Add</button>
    </form>
  )
}

Route Handlers(API 路由)

app/api/items/route.ts

ts 复制代码
import { NextResponse } from 'next/server'
export async function GET() {
  return NextResponse.json([{ id: 1, name: 'A' }], { status: 200 })
}
export async function POST(req: Request) {
  const body = await req.json()
  return NextResponse.json({ ok: true, body }, { status: 201 })
}

Edge Runtime(可选)

app/api/hello/route.ts

ts 复制代码
export const runtime = 'edge'
export async function GET() { return new Response('Hi from edge') }

SEO 与 Metadata

app/page.tsx

tsx 复制代码
export const metadata = { title: 'RSC 指南', description: '下一代 SSR 与流式渲染' }

常见坑与修复

  • 第三方库需要 DOM 时必须在客户端组件使用
  • Server 组件不可使用状态/副作用;事件处理放入客户端组件
  • 仅传递可序列化 props;避免函数与类实例穿越边界
  • 注意缓存层级与失效策略,避免陈旧数据

测试与调试建议

  • 组件分层测试:Server 组件快照 + 客户端交互用 @testing-library/react
  • 端到端流式校验:使用 Playwright 验证 Suspense 与流式内容
  • 观测:结合日志与 APM,对边缘与 Node 运行时区分指标

部署与性能

  • Vercel 一键部署支持流式 SSR;或 Node 自托管
  • 按路由分层设置 revalidate,减少后端压力
  • 大列表分页与骨架屏结合,兼顾渲染与交互

迁移提示

  • Page Router → App Router:拆分 Server/Client 组件,数据上移到服务器
  • 移出全局客户端状态,保留必要交互在客户端

总结

  • RSC 通过服务器端组件与流式输出来提升渲染性能与开发体验
  • 与 Server Actions、Route Handlers 配合,形成端到端的数据与渲染闭环
  • 按需选择客户端组件边界,减少包体与水合成本

数据库直连与增删改查

lib/db.ts

ts 复制代码
import { PrismaClient } from '@prisma/client'
export const prisma = new PrismaClient()

app/items/page.tsx

tsx 复制代码
import { prisma } from '@/lib/db'
export default async function ItemsPage() {
  const items = await prisma.item.findMany({ take: 20 })
  return <ul>{items.map(i => <li key={i.id}>{i.title}</li>)}</ul>
}

app/items/actions.ts

tsx 复制代码
'use server'
import { prisma } from '@/lib/db'
import { revalidatePath } from 'next/cache'
export async function addItem(prev: any, formData: FormData) {
  const title = String(formData.get('title') || '')
  await prisma.item.create({ data: { title } })
  revalidatePath('/items')
  return { ok: true }
}

动态路由与静态生成

app/items/[id]/page.tsx

tsx 复制代码
import { prisma } from '@/lib/db'
export async function generateStaticParams() {
  const ids = await prisma.item.findMany({ select: { id: true }, take: 10 })
  return ids.map(i => ({ id: String(i.id) }))
}
export default async function ItemDetail({ params }: { params: { id: string } }) {
  const item = await prisma.item.findUnique({ where: { id: Number(params.id) } })
  if (!item) return null
  return <div>{item.title}</div>
}

并行与拦截路由

并行路由与拦截路由适用于复杂布局与模态场景。示例结构:

复制代码
app/
  @feed/page.tsx
  @sidebar/page.tsx
  intercept/(.)modal/[id]/page.tsx

乐观更新与表单状态

app/items/client/AddForm.tsx

tsx 复制代码
'use client'
import { useOptimistic, useTransition } from 'react'
import { addItem } from '../actions'
export default function AddForm() {
  const [list, addOptimistic] = useOptimistic<string[], string>((state, v) => [v, ...state])
  const [pending, start] = useTransition()
  return (
    <form action={(fd) => start(async () => { addOptimistic(String(fd.get('title'))); await addItem({}, fd) })}>
      <input name="title" />
      <button disabled={pending} type="submit">Add</button>
      <ul>{list.map((t, i) => <li key={i}>{t}</li>)}</ul>
    </form>
  )
}

文件上传(Server Actions)

app/upload/actions.ts

tsx 复制代码
'use server'
export async function upload(prev: any, formData: FormData) {
  const file = formData.get('file') as File
  const buf = Buffer.from(await file.arrayBuffer())
  return { size: buf.length }
}

错误与 404 边界

app/error.tsx

tsx 复制代码
'use client'
export default function Error({ error }: { error: Error }) { return <div>Error</div> }

app/not-found.tsx

tsx 复制代码
export default function NotFound() { return <div>Not Found</div> }

缓存失效矩阵

  • 静态增量:next: { revalidate: N }
  • 标签失效:next: { tags: ['t'] } 配合 revalidateTag('t')
  • 路径失效:revalidatePath('/p')
  • 禁用缓存:cache: 'no-store'

性能与观测

  • Streaming 与 Suspense 缩短 TTFB 与提升可感知速度
  • 边缘运行适用于低延迟与全球分发
  • 日志与指标区分 Node 与 Edge,关注错误率与渲染耗时

安全建议

  • 仅在服务器组件访问密钥与数据库
  • 传递可序列化数据,避免函数与实例泄露
  • 对输入进行校验与清理,避免注入

架构对比与边界清单

  • CSR:数据拉取在客户端,水合成本高;适合强交互页面
  • 传统 SSR:整页水合,数据靠 API 层;适合内容页与 SEO
  • RSC:数据与渲染上移到服务器,仅交互组件注水;适合大多数内容/混合场景
  • 边界检查:
    • 仅在客户端组件使用 useState/useEffect
    • Server 组件禁止使用浏览器 API 与事件处理
    • Props 必须可序列化(JSON 可表达)
    • 缓存与失效路径明确(标签/路径/时间)

迁移步骤(增量采纳)

  1. 迁入 App Router,保留原 CSR/SSR 页于 pages/,新页在 app/
  2. 拆分数据读取到 Server 组件(或 Route Handler/Server Action)
  3. 将交互模块标记为 use client 并最小化客户端边界
  4. 加入 Streaming 与 Suspense,分层渲染慢数据
  5. 引入缓存与失效矩阵(时间/标签/路径),消除冗余请求
  6. 建立错误边界与 404,完善观测与告警

路由段配置与动态性

app/items/[id]/page.tsx

tsx 复制代码
export const dynamic = 'force-static'     // 或 'force-dynamic'
export const revalidate = 300             // 段级增量静态化
export const fetchCache = 'default-cache' // 控制 fetch 缓存

Middleware(鉴权示例)

middleware.ts

ts 复制代码
import { NextResponse } from 'next/server'
export function middleware(req: Request) {
  const url = new URL(req.url)
  const token = (req as any).cookies?.get('token')?.value
  if (url.pathname.startsWith('/dashboard') && !token) return NextResponse.redirect(new URL('/', url))
  return NextResponse.next()
}
export const config = { matcher: ['/dashboard/:path*'] }

generateMetadata(动态 SEO)

app/items/[id]/page.tsx

tsx 复制代码
export async function generateMetadata({ params }: { params: { id: string } }) {
  return { title: `Item #${params.id}`, description: 'RSC 动态元数据' }
}

观测与日志(instrumentation)

instrumentation.ts

ts 复制代码
export async function register() {
  console.log('app start')
}

测试套件与基准

  • 单测:Server 组件快照;Client 组件交互用 @testing-library/react
  • E2E:Playwright 校验 Suspense 占位与流式插入
  • 性能基准:记录 TTFB、LCP、交互就绪(INP),分路由对比 CSR/SSR/RSC

常见错误与修复

  • 在 Server 组件使用 useState:改为客户端组件或上移为 props
  • 传递函数/类实例作为 props:改为 ID 或纯数据
  • 缓存失效未触发:检查 revalidateTag 与读取端是否声明 tags
  • 第三方库依赖 DOM:在 use client 文件中引用

生产部署清单

  • 环境变量与密钥仅在 Server 端使用
  • 对慢查询与外部 API 设定 timeout 与降级占位
  • 边缘与 Node 环境分开监控,错误率与耗时告警
  • 构建后启用 Bundle 分析,限制客户端包体与重复依赖
相关推荐
崔庆才丨静觅1 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60612 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了2 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅2 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅2 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅3 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment3 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅3 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊3 小时前
jwt介绍
前端
爱敲代码的小鱼3 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax