返回笔记列表
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-交互组件拆分/

一、这一节的目标

  1. 理解 App Router 的默认值:所有组件默认是服务端组件
  2. 精通 "use client" 边界:什么必须客户端、边界画在哪
  3. 掌握"服务端壳 + 客户端岛"的组合模式(地图页的核心模式)
  4. 明确服务端组件的三大限制

二、默认服务端,按需客户端

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 取数组件)。


三、服务端组件三大限制

  1. 不能用 hooks/事件/浏览器 API(它不在浏览器跑)
  2. 不能 async 传给客户端:客户端组件的 props 必须可序列化(不能传函数、class 实例、Date 之外的复杂对象按规则来)——日期建议先 toISOString 转字符串
  3. 不能把服务端函数当 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

步骤:

  1. 页面:文章列表(服务端)+ 每篇的点赞按钮(客户端岛)+ 一个全文 "use client" 的对照页
  2. 完成 5 个 TODO:点赞按钮持久化(localStorage——只有客户端能用,正好演示边界)、把 LikeButton 改成"服务端组件"观察报错(读懂报错原文)、传一个函数 props 给客户端岛观察报错、Date 传输实验(toISOString 前后)、把整个页面 "use client" 对照(体会失去 async 后的别扭)
  3. 在注释里画出本页的"服务端区/客户端岛"示意

通关标准:

  • 能说出三个"必须 use client"的信号
  • 亲眼看懂两个边界报错(async 客户端 / 函数 props)
  • 能画出"壳 + 岛"结构图

五、自测题

  1. App Router 默认组件跑在哪?好处三条?
  2. "use client" 的边界画在哪最好?
  3. 服务端 → 客户端传 props 的限制叫什么?
  4. 为什么 LikeButton 必须是客户端组件?
  5. 客户端组件能 import 服务端组件吗?

参考答案

  1. 服务器;零 JS 发送、可 await/密钥、SEO 直出。
  2. 越靠叶子越好——服务端大壳 + 客户端小岛。
  3. 可序列化(序列化协议限制:函数/类实例不可;Date 建议转字符串)。
  4. 它有 useState 与 onClick——浏览器交互能力,服务端没有这些运行时。
  5. 不能(箭头单向:服务端→客户端);客户端里需要服务端数据就走 Route Handler 或父级传 props。

六、下一步

边界划清 → 第 24 节:API Routes 与部署,补上后端一角并上线。

TAGSweb