01 · NOTES / WebGIS 开发
第 23 节 · 服务端/客户端组件
2026 年 9 月 1 日
第 23 节 · 服务端/客户端组件
📌 版本信息:Next.js 15.x / React 19.x(2026-08-29 核对) 📚 来源:Next.js · Server and Client Components ▶️ 配套练习:
examples/23-交互组件拆分/
一、这一节的目标
- 理解 App Router 的默认值:所有组件默认是服务端组件
- 精通
"use client"边界:什么必须客户端、边界画在哪 - 掌握"服务端壳 + 客户端岛"的组合模式(地图页的核心模式)
- 明确服务端组件的三大限制
二、默认服务端,按需客户端
App Router 里你不写任何标记的组件,全部在服务器上渲染(只把 HTML/序列化结果发给浏览器)——省 JS、能 await、能用密钥。需要以下能力时才 "use client":
| 必须 "use client" 的能力 | 例子 |
|---|---|
| state / 生命周期 / Effect | useState、useEffect、useRef 交互 |
| 浏览器 API | localStorage、window、geolocation |
| 事件处理器 | onClick、onChange |
| 第三方"浏览器专属"库 | Leaflet、ECharts(它们要 window) |
// app/page.js(服务端,默认)—— 壳:取数 + 布局
import LikeButton from './LikeButton'; // 客户端岛
export default async function Page() {
const posts = await getPosts();
return (
<div>
<h1>文章(服务端渲染)</h1>
{/* 服务端组件可以"包住"客户端组件,并把数据当 props 传下去——标准组合 */}
{posts.map((p) => (
<div key={p.id}>
<h3>{p.title}</h3>
<LikeButton postId={p.id} /> {/* 交互集中在小岛上 */}
</div>
))}
</div>
);
}
// LikeButton.js —— "use client" 岛:只有它进浏览器包
'use client';
import { useState } from 'react';
export default function LikeButton({ postId }) {
const [liked, setLiked] = useState(false);
return <button onClick={() => setLiked(!liked)}>{liked ? '❤️' : '🤍'} 点赞</button>;
}
边界原则:边界线画得越靠叶子越好——整个页面 "use client" 是新手常见过度(那等于放弃了服务端的一切优势)。理想形态:服务端大壳 + 少量客户端小岛。
⚠️ 方向性:import 箭头只能从服务端 → 客户端(传可序列化 props),客户端组件不能 import 服务端组件(不能在浏览器里 await 取数组件)。
三、服务端组件三大限制
- 不能用 hooks/事件/浏览器 API(它不在浏览器跑)
- 不能 async 传给客户端:客户端组件的 props 必须可序列化(不能传函数、class 实例、Date 之外的复杂对象按规则来)——日期建议先 toISOString 转字符串
- 不能把服务端函数当 props 传(要用则走 Server Actions,进阶,知道即可)
地图页实战模式(第 27 节展开):
// 服务端壳:取数据 + 元数据
export default async function ExplorePage() {
const pois = await getPois(); // 密钥安全、零白屏
return <MapClient pois={pois} />; // 序列化后传给客户端岛
}
// MapClient.js:"use client" + 动态导入 Leaflet(第 27 节)
四、动手跟练:23 · 交互组件拆分
配套文件夹:23-交互组件拆分/(npm i && npm run dev)
步骤:
- 页面:文章列表(服务端)+ 每篇的点赞按钮(客户端岛)+ 一个全文 "use client" 的对照页
- 完成 5 个 TODO:点赞按钮持久化(localStorage——只有客户端能用,正好演示边界)、把 LikeButton 改成"服务端组件"观察报错(读懂报错原文)、传一个函数 props 给客户端岛观察报错、Date 传输实验(toISOString 前后)、把整个页面 "use client" 对照(体会失去 async 后的别扭)
- 在注释里画出本页的"服务端区/客户端岛"示意
通关标准:
- 能说出三个"必须 use client"的信号
- 亲眼看懂两个边界报错(async 客户端 / 函数 props)
- 能画出"壳 + 岛"结构图
五、自测题
- App Router 默认组件跑在哪?好处三条?
- "use client" 的边界画在哪最好?
- 服务端 → 客户端传 props 的限制叫什么?
- 为什么 LikeButton 必须是客户端组件?
- 客户端组件能 import 服务端组件吗?
参考答案
- 服务器;零 JS 发送、可 await/密钥、SEO 直出。
- 越靠叶子越好——服务端大壳 + 客户端小岛。
- 可序列化(序列化协议限制:函数/类实例不可;Date 建议转字符串)。
- 它有 useState 与 onClick——浏览器交互能力,服务端没有这些运行时。
- 不能(箭头单向:服务端→客户端);客户端里需要服务端数据就走 Route Handler 或父级传 props。
六、下一步
边界划清 → 第 24 节:API Routes 与部署,补上后端一角并上线。
TAGSweb