Next.js App Router 实战:用 RSC 与 Server Actions 构建全栈应用

Next.js App Router 是当下最主流的 React 全栈框架方案。它用 React Server Components(RSC)把组件默认放到服务端渲染,用 Server Actions 让函数直接跑在服务器上处理表单与数据写入,前后端第一次被统一进同一个工程。本文从目录约定、数据获取、Server Actions 到缓存与部署,手把手带你落地一个可上线的 Next.js 全栈应用。

一、为什么是 App Router:RSC 带来的范式变化

在 Pages Router 时代,一个页面就是一次客户端请求加一次数据预取,前端要写 getServerSideProps,再在组件里用 useEffect 拉数据,前后端边界很清晰但也很割裂。App Router 的核心改变有三点:默认服务端渲染、组件即路由、服务端可以直接写数据。

  • 零客户端包体:RSC 在服务端执行,不会被打包进浏览器 JS,首屏体积显著下降。
  • 流式渲染:Suspense 边界可以把慢数据拆成独立的加载片段,用户先看到骨架再看到内容。
  • 渐进增强:Server Actions 让普通 <form> 不依赖 JS 也能提交,SEO 与弱网都更稳。

二、项目初始化与目录约定

2.1 用 app 目录承载文件路由

App Router 用 app/ 目录下的文件路径直接映射 URL。新建项目后,目录结构大致如下:

# 初始化(Node 18.18+)
npx create-next-app@latest my-app --ts --app --tailwind --eslint
cd my-app

# 典型 app 目录结构
app/
  layout.tsx        # 根布局,包裹所有页面
  page.tsx          # 对应路由 /
  loading.tsx       # 路由级 Suspense 兜底 UI
  error.tsx         # 路由级错误边界
  posts/
    page.tsx        # 对应路由 /posts
    [id]/
      page.tsx      # 动态路由 /posts/123

2.2 layout 与模板:共享外壳

layout.tsx 在服务端渲染一次并缓存,适合放全局导航、字体与元数据;template.tsx 则会在每次导航重新挂载,适合需要动画或重置状态的场景。两者都接收 children 作为插槽。

// app/layout.tsx
export const metadata = {
  title: "我的全栈应用",
  description: "基于 Next.js App Router 构建",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="zh-CN">
      <body>{children}</body>
    </html>
  );
}

三、数据获取:在服务端直接读库

3.1 async Server Component 直连数据库

Server Component 可以是 async 函数,直接在组件里 await 数据库或接口,无需额外 API 层。下面以 PostgreSQL 为例:

// app/posts/page.tsx(服务端组件)
import { db } from "@/lib/db";

export default async function PostsPage() {
  const posts = await db.query(
    "SELECT id, title, created_at FROM posts ORDER BY created_at DESC LIMIT 20"
  );

  return (
    <ul>
      {posts.map((p) => (
        <li key={p.id}>{p.title}</li>
      ))}
    </ul>
  );
}

3.2 流式渲染与 loading.tsx

把某个区块用 Suspense 包起来,再配合 loading.tsx,Next.js 会先返回外壳 HTML,数据就绪后通过流式补充。对慢查询页面体验提升明显。

四、Server Actions:告别手写 API

4.1 定义与调用

在文件顶部加 "use server",函数就能被客户端直接调用,省去定义路由、写 fetch、处理 CORS 的全过程。

// app/actions.ts
"use server";

export async function createPost(formData: FormData) {
  const title = formData.get("title");
  if (!title) throw new Error("标题不能为空");
  await db.query("INSERT INTO posts(title) VALUES($1)", [title]);
  // 重新验证列表路由的缓存
  revalidatePath("/posts");
}

4.2 表单与渐进增强

把 Action 直接传给 <form action={...}>,即使浏览器禁用 JS,表单也能正常提交,这就是渐进增强。配合 useFormStatus 还能拿到提交态。

// app/posts/new/page.tsx
import { createPost } from "@/app/actions";

export default function NewPost() {
  return (
    <form action={createPost}>
      <input name="title" placeholder="文章标题" />
      <button type="submit">发布</button>
    </form>
  );
}

4.3 错误处理:useFormState 与 error.tsx

用 useFormState 捕获 Action 返回值做字段级提示,配合路由级 error.tsx 兜底意外异常,整体容错就完整了。

五、缓存策略:四种粒度怎么选

Next.js 的缓存是新手最容易踩坑的地方。下表总结了常见缓存手段的适用场景,建议先理解再使用,避免”改了数据页面不更新”的尴尬。

手段作用范围何时失效典型用途
fetch 的 cache 选项单个请求按 next: { revalidate }列表/详情数据
revalidatePath某个路由Action 内主动调用写操作后刷新页面
revalidateTag带 tag 的请求按 tag 失效多页面共享的数据源
Segment 配置 dynamic整个路由段每次请求/构建强实时页面

六、与现有后端和基础设施协作

Next.js 不是要取代你已有的后端,而是补齐”前端 + BFF”这一层。真实项目里它通常这样落位:

下面是一个最小可用的多阶段 Dockerfile,先把依赖装好再构建,最后用轻量运行镜像:

# 构建阶段
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# 运行阶段
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
EXPOSE 3000
CMD ["npm", "start"]

七、常见坑与排障清单

  • 客户端用了服务端变量:含 process.env.SECRET 的组件若被标记为 "use client",密钥会泄露,敏感逻辑务必留在服务端。
  • 页面不更新:多半是缓存没失效,检查 revalidatePath/revalidateTag 是否覆盖到对应路由。
  • hydration 不匹配:服务端和客户端渲染的日期、随机数不同会报错,用 suppressHydrationWarning 仅作临时止血,根因要统一数据源。
  • 包体回涨:在 Server Component 里误导入了仅客户端可用的库,会被打进客户端包,留意控制台警告。

八、服务端组件与客户端组件的边界怎么划

8.1 何时必须声明 “use client”

只有用到浏览器 API、事件监听、useState/useEffect 等交互能力的组件才需要加 "use client"。一个常见误区是把整个页面都标成客户端组件,结果 RSC 的服务端优势全部丢失。正确做法是把交互收口在最小的叶子组件里,父级仍然保持服务端组件,这样既拿到状态能力,又不污染整棵树的包体。

8.2 避免服务端依赖被打进客户端

在客户端组件里 import 了一个只在服务端存在的库(比如数据库驱动、文件系统模块),打包器会尝试把它拉进浏览器包,轻则体积暴涨,重则构建失败。把这类逻辑下沉到 Server Actions 或独立的服务端函数,客户端只调用结果,是更稳妥的拆法。

九、Server Actions 进阶:事务、鉴权与限流

真正的写操作不能只做插入,还要考虑原子性、权限和防刷。下面这段把鉴权、事务与限流串起来,更接近生产可用的写法:

// app/actions.ts
"use server";
import { getSession } from "@/lib/auth";
import { ratelimit } from "@/lib/ratelimit";

export async function createPost(formData: FormData) {
  const user = await getSession();
  if (!user) throw new Error("未登录");

  const ok = await ratelimit.check(user.id, "create-post");
  if (!ok) throw new Error("操作过于频繁,请稍后再试");

  const title = String(formData.get("title") ?? "").trim();
  if (!title) throw new Error("标题不能为空");

  await db.transaction(async (tx) => {
    await tx.query("INSERT INTO posts(title, author) VALUES($1,$2)", [title, user.id]);
  });

  revalidatePath("/posts");
}

把限流和鉴权放在 Action 入口,好处是所有调用方(页面表单、其他 Server Component)都自动受益,不必在每个入口重复写一遍。

十、App Router 与 Pages Router 该怎么选

老项目不必急着迁移,但新项目建议直接选 App Router。下表给出决策参考:

维度App RouterPages Router
默认渲染服务端组件客户端组件
数据写入Server Actions 内联手写 API 路由
包体体积更小(按需客户端化)偏大
学习成本较高(心智模型新)较低(熟悉)
适合场景新项目 / 重 SEO / 全栈存量项目渐进维护

十一、小结与迁移建议

App Router 的学习曲线集中在”服务端心智模型”的转变:组件默认在服务端、数据在那里取、写入用 Server Actions。新项目建议直接上 App Router;老项目不必推倒重来,可先在边缘路由(如管理后台、表单页)试点,再逐步把 getServerSideProps 改写成 RSC。当你把”读”交给 Server Component、”写”交给 Server Action、”缓存”交给 revalidate 体系,一个干净的全栈应用骨架就立起来了。

上一篇 Argo Workflows 实战:K8s 原生工作流编排
下一篇 《etcd 实战:Kubernetes 分布式键值存储与高可用运维》
最新评论
Docker 网络模式详解:bridge/host 生产选型 - FSData
Docker 网络模式详解:bridge/host 生产选型 - FSData
8月20日
[…] 注意最后那条:把端口绑定到 127.0.0.1,再由宿主机上的 Nginx 反代进来,比直接 -p 80:80 暴露安全得多,也方便统一做
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
8月20日
[…] Pinia 的工程化用法,可以参考我们之前的实战文章 《Vue3 组合式 API + Pinia:状态管理从入门到实战》,那里覆盖了 Sto
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
8月19日
[…] 状态,点一下就能起停容器,配合我们讲过的 Docker Compose 编排 […]
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
8月19日
[…] 缓存三大坑的本质都是”缓存失效导致请求穿透到数据库”。记住口诀:穿透用布隆、击穿用锁、雪崩用错峰。配合合理的过期策略
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
8月19日
[…] Docker 镜像部署。结合我们之前写的 Docker Compose 一键编排实战,可以在 CI […]