服务端与客户端组件
服务端与客户端组件 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
App Router 里每个组件都要回答一个问题:服务端还是客户端?读完你应该能按「要不要数据、要不要交互」划边界,验证秘密只留在服务端,并把客户端依赖压到最小。
问题场景
课程详情页:上半部分是课程信息和进度统计(来自数据库),下半部分是一个「收藏」按钮(点击有反馈)。两种偷懒写法都不对——整页 "use client",就得把数据获取搬到浏览器、把数据库代码暴露在请求链路上;全用服务端,收藏按钮点了没反应。
核心解释
App Router 的默认是服务端组件:只在服务器执行,产出 HTML。它的能力边界来自物理位置——能直接访问数据库、文件、密钥,没有网络往返;代价是不能用 state 和事件,用户看不到它「动」。
客户端组件(文件顶部 "use client")会被打包发给浏览器,能 useState、能监听事件;代价是代码和它引用的依赖全进 JS 包,而且不能再碰任何秘密。
划分边界的两条判据:
判据一:数据留在服务端。 数据库查询、API 密钥、环境变量只在服务端组件(或数据层)出现。服务端组件把渲染结果(HTML + 序列化的 props)发给浏览器,秘密不会跟着走——前提是你没把它当 props 传下去。
// app/courses/[slug]/page.tsx —— 服务端组件(默认)
import { getCourse } from "@/lib/queries";
import { FavoriteButton } from "@/components/favorite-button";
export default async function CoursePage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const course = await getCourse(slug); // 进程内查库,密钥不出服务端
return (
<main>
<h1>{course.title}</h1>
<p>已完成 {course.doneCount} 节</p>
<FavoriteButton initialCount={course.favorites} /> {/* 可序列化 props 下传 */}
</main>
);
}
判据二:交互下沉到客户端。 只有真正需要 state / 事件的部分才 "use client",并且做成叶子小组件:FavoriteButton 拿一个数字做初始值,自己管理本地状态。服务端外壳照常渲染,浏览器只多下载一个按钮的代码。
// components/favorite-button.tsx
"use client";
import { useState } from "react";
export function FavoriteButton({ initialCount }: { initialCount: number }) {
const [count, setCount] = useState(initialCount);
return <button onClick={() => setCount(count + 1)}>收藏({count})</button>;
}
反模式要认清:在客户端组件里 process.env.DB_PASSWORD 会得到 undefined 甚至泄漏风险;在服务端组件里写 onClick 是无效属性;「从客户端组件 import 服务端模块」直接报错——边界是单向的,服务端可以引客户端,反过来不行(但可以把服务端组件当 children 传给客户端组件)。
示例拆解
实验区是一个概念模拟页(纯 HTML/JS),验证「秘密不下山」:
- 左栏模拟服务端渲染:一个
renderOnServer()函数持有DB_PASSWORD,但只把课程列表拼进返回的 HTML; - 页面用脚本逐字检查返回结果:确认其中不含密码字符串——这就是「服务端组件产出不包含秘密」的直观版;
- 右栏模拟客户端组件:一个收藏按钮用 addEventListener 响应点击、更新计数——交互只能发生在浏览器;
- 对照结论:数据渲染在服务端完成、秘密从不进入输出;带交互的那一小块单独存在,相当于被
"use client"标记的叶子组件。
动手任务
完成三个小任务:
- 在项目里实现「服务端详情页 + 客户端收藏按钮」组合,props 只传 initialCount 这类可序列化值。
- 在收藏按钮所在文件顶部加
"use client",然后故意删掉它——记录浏览器控制台和终端的报错,理解报错信息在说边界什么规则。 - 跑完实验区的概念演示,用文字复述「为什么返回的 HTML 里查不到 DB_PASSWORD」。
常见错误
- 整页统一 "use client" 图省事:数据获取被迫搬到浏览器(多一次网络往返),数据库依赖进包,秘密防线后撤;按叶子组件下沉。
- 把敏感值当 props 传给客户端组件:props 会序列化进 HTML/数据流,浏览器看得到;敏感值只在服务端消费,输出只留结论。
- 在服务端组件里传函数 props:函数无法序列化,客户端组件只接受可序列化的 props;要传行为就传「数据 + 客户端自己处理」。
- 在客户端组件里 import 数据库模块:构建直接报错;需要数据时让服务端组件取好再传,或走 Server Actions(后续课程)。
检查与复盘
完成下方检查题确认理解。自查清单:
- 能按「数据 / 交互」两条判据给任意组件定边
- 秘密只在服务端出现,客户端组件的 props 全部可序列化且无敏感值
- 客户端依赖收敛在叶子组件,服务端外壳默认无 "use client"
参考资料
- Next.js 文档:服务端与客户端组件 —— 边界规则与组合模式的官方讲解
- Next.js 文档:use client 指令 —— 指令的作用范围与常见误区
读完这一课了?
标记完成后,工作台会安排接下来的学习。