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 分析,限制客户端包体与重复依赖
相关推荐
Howie Zphile37 分钟前
NEXTJS/REACT有哪些主流的UI可选
前端·react.js·ui
hxmmm38 分钟前
preconnect、dns-prefetch、prerender、preload、prefetch
javascript
郑州光合科技余经理39 分钟前
PHP技术栈:上门系统海外版开发与源码解析
java·开发语言·javascript·git·uni-app·php·uniapp
lichong95139 分钟前
harmonyos 大屏设备怎么弹出 u 盘
前端·macos·华为·typescript·android studio·harmonyos·大前端
irises39 分钟前
从零实现2D绘图引擎:5.5.简单图表demo
前端·数据可视化
irises39 分钟前
从零实现2D绘图引擎:5.鼠标悬停事件
前端·数据可视化
青莲84340 分钟前
Android Lifecycle 完全指南:从设计原理到生产实践
android·前端
汤姆Tom40 分钟前
前端转战后端:JavaScript 与 Java 对照学习指南(第三篇 —— Map 对象)
java·javascript·全栈
irises41 分钟前
从零实现2D绘图引擎:4.矩形与文本的实现
前端·数据可视化