

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.jsuse client;可使用状态与事件,不能直接访问服务器资源app/components/RepoList.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>
)
}app/components/InteractiveCounter.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
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
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh">
<body>{children}</body>
</html>
)
}app/page.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>
)
}fetch:next: { revalidate } 控制增量静态化;cache: 'no-store' 关闭缓存cache(asyncFn):稳定函数层复用结果,避免重复 IOrevalidateTag/revalidatePath:配合标签或路径精细失效app/actions.ts
'use server'
import { revalidateTag } from 'next/cache'
export async function createItem(formData: FormData) {
// 写入数据库...
revalidateTag('items')
}Server 组件读取:
const res = await fetch('https://api.example.com/items', { next: { tags: ['items'], revalidate: 120 } })app/actions.ts
'use server'
export async function addTodo(prev: any, formData: FormData) {
const title = String(formData.get('title') ?? '')
// 写库...
return { ok: true, title }
}app/page.tsx
import { addTodo } from './actions'
export default function Page() {
return (
<form action={addTodo}>
<input name="title" />
<button type="submit">Add</button>
</form>
)
}app/api/items/route.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 })
}app/api/hello/route.ts
export const runtime = 'edge'
export async function GET() { return new Response('Hi from edge') }app/page.tsx
export const metadata = { title: 'RSC 指南', description: '下一代 SSR 与流式渲染' }@testing-library/reactrevalidate,减少后端压力lib/db.ts
import { PrismaClient } from '@prisma/client'
export const prisma = new PrismaClient()app/items/page.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
'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
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.tsxapp/items/client/AddForm.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>
)
}app/upload/actions.ts
'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 }
}app/error.tsx
'use client'
export default function Error({ error }: { error: Error }) { return <div>Error</div> }app/not-found.tsx
export default function NotFound() { return <div>Not Found</div> }next: { revalidate: N }next: { tags: ['t'] } 配合 revalidateTag('t')revalidatePath('/p')cache: 'no-store'useState/useEffectpages/,新页在 app/use client 并最小化客户端边界app/items/[id]/page.tsx
export const dynamic = 'force-static' // 或 'force-dynamic'
export const revalidate = 300 // 段级增量静态化
export const fetchCache = 'default-cache' // 控制 fetch 缓存middleware.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*'] }app/items/[id]/page.tsx
export async function generateMetadata({ params }: { params: { id: string } }) {
return { title: `Item #${params.id}`, description: 'RSC 动态元数据' }
}instrumentation.ts
export async function register() {
console.log('app start')
}@testing-library/reactuseState:改为客户端组件或上移为 propsrevalidateTag 与读取端是否声明 tagsuse client 文件中引用timeout 与降级占位