4.9 组件化开发:像搭积木一样拼页面
一、本节你会做出什么
你会把练习项目拆成一套组件,并且拿到一条判断标准:什么时候该抽组件、什么时候不该。
📷 效果位:左边 components/ 目录树,右边瘦了一半的 page.tsx
4.6 讲了组件是什么(返回 JSX 的函数)。这一节讲怎么组织——因为新手在这件事上有两个相反的翻车方式:
| 翻车方式 | 症状 |
|---|---|
| 抽得太少 | 一个 page.tsx 写到 600 行,想改个东西翻半天找不到 |
| 抽得太碎 | 20 个组件文件,每个 5 行,改一处要跳 4 个文件 |
这一节就是帮你找到中间那条线。
二、开始前你需要
三、跟着做
第 1 步:什么时候该抽组件——三条判断标准
满足任意一条就该抽。
| # | 标准 | 为什么 |
|---|---|---|
| 1 | 同样的东西出现 2 次以上 | 4.6 第 1 步那三个痛点:改一处要改 N 处 |
| 2 | 这个文件超过 150 行,读起来找不到东西 | 你的注意力有限,AI 的也有限 |
| 3 | 这一块有独立的职责,能用一个名字概括 | 能起出 AttractionCard、SearchBar 这种名字,说明它是个独立的东西 |
反过来,这些情况不要抽:
| 不要抽的情况 | 原因 |
|---|---|
| 只用了一次,而且就三五行 | 抽出去反而要跳文件看 |
抽出来起不出好名字(只能叫 Block1、Wrapper) |
起不出名字说明职责不清,抽了也是乱的 |
| 纯粹为了“让文件短一点” | 短不是目的,好找才是目的 |
🔑 最实用的那条是标准 3: 先问自己“这块东西叫什么”。叫得出名字就抽,叫不出就别抽。
第 2 步:三类组件,分开想就不乱了
真实项目里的组件大致分三类。认清类别,你就知道该抽什么、放哪里。
| 类型 | 职责 | 例子 | 特点 |
|---|---|---|---|
| ① 布局组件 | 页面的框架 | Header Footer Sidebar |
每页都有,通常放在 layout 里 |
| ② 展示组件 | 把数据显示出来 | AttractionCard PriceTag Avatar |
接 props,自己不管数据从哪来 |
| ③ 交互组件 | 响应用户操作 | Button SearchBar Modal |
需要 "use client"(4.7 第 5 步) |
💡 第 ② 类是重点。好的展示组件有一个特征:它不关心数据从哪来。
AttractionCard只管“给我名字和价格,我负责显示得好看”。数据是写死的、从接口来的、从数据库来的,它都一样工作。这叫“组件不关心数据来源” —— 第 5 章用写死的数据,第 9 章换成数据库,卡片组件一行都不用改。
第 3 步:组件放哪里、怎么命名
约定(不是强制,但照着做能省很多沟通):
项目根目录/├── app/│ ├── layout.tsx│ ├── page.tsx│ └── attractions/│ └── page.tsx└── components/ ← 👈 组件统一放这里 ├── Header.tsx ├── Footer.tsx ├── AttractionCard.tsx └── ui/ ← 更基础的小零件(可选) └── Button.tsx| 约定 | 说明 |
|---|---|
目录叫 components/ |
放项目根目录,和 app/ 平级(有 src/ 的话放 src/components/) |
| 文件名用大驼峰 | AttractionCard.tsx,和组件名一致——好找 |
| 一个文件一个组件 | 一眼知道去哪个文件改 |
| 组件名要具体 | AttractionCard ✅ / Card 🤔 / Component1 ❌ |
🔑 把这几条写进你的规则文件(3.5 讲的
AGENTS.md):- 组件放 components/ 目录,一个组件一个文件,文件名用大驼峰且与组件名一致- 组件名要能说清它是什么,不要用 Card / Box / Wrapper 这类泛名这样 AI 每次抽组件都会放对地方,你不用每次重复交代。
第 4 步:props 怎么设计——传什么,不传什么
这是组件化最需要判断力的地方。
原则:组件需要什么就传什么,不多传。
// 🤔 不太好:把整个对象扔进去<AttractionCard attraction={item} />
// ✅ 更清晰:明确需要哪几个字段<AttractionCard name={item.name} price={item.price} image={item.image} />| 写法 | 好处 | 坏处 |
|---|---|---|
| 传整个对象 | 写起来省事,字段多时不用列一长串 | 看组件的调用处,不知道它用了哪些字段 |
| 逐个传字段 | 一眼看出这个组件需要什么数据 | 字段多了很啰嗦 |
💡 实用建议:字段少于 5 个就逐个传,多了就传对象。但不管哪种,都要在组件定义处把需要的字段列清楚(4.6 讲的解构写法
({ name, price })就是在干这件事)。
children —— 包裹型组件
还有一种特殊的 props,叫 children。它让组件能“包住别的东西”:
// 定义一个卡片外壳function Panel({ title, children }) { return ( <section className="rounded-xl border p-6"> <h2 className="text-xl font-bold mb-3">{title}</h2> {children} {/* ← 被包住的内容出现在这里 */} </section> );}
// 使用:标签中间的东西就是 children<Panel title="特色美食"> <p>火锅、串串、钟水饺。</p> <p>建议避开饭点去排队。</p></Panel>💡 4.7 讲的
layout.tsx里那个{children}就是这个东西——layout 本质上就是一个包裹型组件,页面内容是它的 children。现在这两件事连起来了。
第 5 步:动手——把练习项目组件化
先存档(2.3 规矩一):
git add -A && git commit -m "组件化改造前的存档点"① 抽布局组件(Header / Footer)
4.8 里你把导航写在了 layout.tsx 里。现在把它抽出来:
跟 AI 说(按 3.5 的方式,先要方案):
【做什么】把 app/layout.tsx 里的顶部导航抽成 Header 组件,并新增一个 Footer 组件
【放在哪】components/Header.tsx 和 components/Footer.tsx
【什么样】- Header:内容和现在完全一样(站点名 + 三个导航链接)- Footer:显示「© 2026 我的练习站」,灰色小字,居中- layout.tsx 里变成 <Header /> {children} <Footer />
【不要做什么】- 页面视觉效果不要有任何变化(除了新增的 Footer)- 不要新增依赖包
【怎么算对】每个页面顶部有导航、底部有页脚,pnpm build 通过
先告诉我你打算建哪些文件、layout.tsx 会变成什么样,我确认后再动手。② 抽展示组件(AttractionCard)
把 app/attractions/page.tsx 里的列表项抽成 AttractionCard 组件:
1. 放在 components/AttractionCard.tsx;2. 通过 props 接收 id、name、price;3. 组件内部负责显示名字、价格(0 元显示"免费")、以及跳转到详情页的链接;4. 列表页里用 map 渲染,记得加 key;5. 顺手把卡片做得好看一点:白底、圆角、浅阴影、悬停时阴影加深;6. 页面功能不变——点击仍然能进详情页。
先给方案。③ 抽交互组件(一个要 "use client" 的)
新增一个 LikeButton 组件:1. 放在 components/LikeButton.tsx;2. 显示一个「想去 N」的按钮,点击 N 加一;3. 用 useState 记住数字,**记得这个组件需要 "use client"**;4. 把它放进 AttractionCard,每张卡片都有自己独立的计数。
注意:AttractionCard 本身如果不需要交互,**保持它是服务端组件**,只让 LikeButton 这一小块是客户端组件。做完告诉我你是怎么处理的。🔑 最后那个要求值得留意(4.7 提示词 ③ 也提过): 只把真正需要交互的那一小块做成客户端组件,页面其余部分保持服务端渲染。 这是 Next.js 的最佳实践——组件化在这里不只是“代码整齐”,而是有实际性能意义。
验收
git diff --stat # ③ 看改动范围pnpm build # 4.7 新增的那道关浏览器里点一遍:导航能跳、卡片能进详情、点赞按钮各自独立计数、控制台无红字。
📷 截图位 1:改造后的 components/ 目录 + 页面效果
git add -A && git commit -m "抽出 Header / Footer / AttractionCard / LikeButton 组件"第 6 步:别从零造轮子——现成组件库
做按钮、弹窗、下拉菜单、日期选择器这些东西,有大量现成的。
| 类型 | 说明 |
|---|---|
| 组件库 | 一整套做好的组件(按钮、表单、弹窗、表格……),装上就能用 |
| 常见的 | shadcn/ui、Material UI、Ant Design 等 |
| 本课立场 | 第 5 章用不着(内容型网站主要是布局和卡片);第 9 章做后台管理页时值得用 |
⚠️ 一条提醒:别一上来就让 AI 装组件库。 3.5 讲过「不要擅自新增依赖包」——组件库是比较重的依赖,而且会给项目带来一套你不熟悉的约定。
判断标准:你需要的是“卡片、布局”这类东西 → 自己写(Tailwind 几行就够);你需要的是“带键盘操作和无障碍支持的下拉菜单、可访问的弹窗” → 用现成的,这些自己写很容易出问题。
第 7 步:组件化的收益,自己验证一次
提一个需求,感受组件化到底值不值:
给所有景点卡片加一个"标签"显示(比如"必去""免费""文化"),标签是小圆角色块,显示在景点名下方。数据里加上 tag 字段。观察 git diff --stat:
- 组件化之前:要改列表页里三段重复代码
- 组件化之后:改
AttractionCard.tsx一个文件(加上数据里补字段)
🔑 这就是组件化的全部意义: 改一处,所有用到的地方都变。
4.6 让你体会过一次(加按钮),这里再验证一次——这两次体感会让你在第 5 章主动要求 AI 抽组件,而不是等它写出 600 行的 page.tsx。
四、完成的标志是
- 我知道该抽组件的三条标准(重复 2 次以上 / 文件超 150 行 / 能起出好名字)
- 我知道起不出名字就不该抽
- 我说得出三类组件:布局 / 展示 / 交互
- 我知道好的展示组件不关心数据从哪来
- 我知道组件放
components/、文件名用大驼峰、一个文件一个组件 - 我把这几条命名约定写进了规则文件
- 我知道 props 该传具体字段还是整个对象的权衡
- 我知道
children是“被包住的内容”,并且明白layout.tsx就是个包裹型组件 - 我抽出了 Header / Footer(布局类)
- 我抽出了 AttractionCard(展示类),页面功能不变
- 我抽出了 LikeButton(交互类),只有它加了
"use client" -
pnpm build通过 - 我用“加标签”的需求验证了一次组件化的收益(只改一个文件)
- 我知道什么时候该用现成组件库、什么时候自己写
五、卡住了看这里
Q:抽完组件,页面报错 Element type is invalid。
八成是导入/导出没对上。组件文件用 export default function Header(),引用处就该 import Header from "@/components/Header";如果用的是 export function Header()(没有 default),引用时要写 import { Header } from ...。把报错给 AI,一句话能修。
Q:报错说找不到模块 @/components/Header。
@/ 是指向项目根(或 src/)的别名。检查:① 文件路径和大小写是否完全一致(Mac 不区分大小写、部署的 Linux 区分——这是“本地好的上线就坏”的一个经典原因);② 文件扩展名是 .tsx 而不是 .ts。
Q:抽完组件,样式全没了。
Tailwind 的 class 可能在复制过程中漏了,或者外层包裹结构变了导致布局失效。用 git diff 对比,看那个元素的 class 是不是变短了。拿不准就 git restore . 重来一次,并在需求里强调「视觉效果必须完全不变」。
Q:LikeButton 加了 "use client",但报错说父组件也要加。
通常是你在服务端组件里把函数当 props 传给了客户端组件。两个解法:① 让那个交互组件自己管自己的状态,不要从父组件传函数进去(第 5 步的做法);② 父组件也加 "use client"(但会牺牲服务端渲染)。优先选 ①。
Q:我不知道该把某一块抽成组件还是留在页面里。 用标准 3:试着给它起个名字。 起得出就抽,起不出就留着。还拿不准就用本节提示词 ①,让 AI 给建议——但最终决定权在你,因为你知道以后要往哪个方向改。
Q:AI 一次抽了 15 个组件,我看不过来。
这是“抽得太碎”。回滚(git restore .),重新提需求时加上边界:「只抽出重复出现 2 次以上的部分,单次使用且少于 20 行的不要抽」。3.5 讲的「说清不要做什么」在这里特别有效。
六、本节提示词
① 组件化方案评估(动手前先问):
这是我的页面文件:【@文件名】
请帮我评估组件化方案:1. 哪些部分**应该**抽成组件?按「重复出现 / 文件过长 / 职责独立」说明理由;2. 哪些部分**不该**抽?为什么;3. 每个建议抽出的组件,给出:组件名、文件路径、需要哪些 props、属于布局/展示/交互哪一类;4. 哪些组件需要 `"use client"`,哪些不需要;5. 建议的改造顺序。
**先不要动代码**,我确认方案后再说。② 执行抽组件(确认方案后用):
按刚才确认的方案抽组件。要求:
1. 组件放 components/ 目录,一个组件一个文件,文件名大驼峰且与组件名一致;2. **页面视觉效果和功能必须完全不变**;3. props 用解构写法,让人一眼看出这个组件需要什么数据;4. 只有真正需要交互的组件加 `"use client"`,其余保持服务端组件;5. map 渲染的列表记得加 key;6. 不要新增任何依赖包。
改完告诉我:新建了哪些文件、原文件行数的变化、以及我该怎么验证效果没变。③ 组件代码体检(项目做大之后定期跑一次):
请检查我项目里 components/ 目录下的组件:
1. 有没有命名不清的组件(叫 Card / Box / Wrapper 这类泛名)?建议改成什么;2. 有没有组件接收了用不到的 props?3. 有没有组件过大、应该再拆分的?4. 有没有重复的组件(两个组件干了几乎一样的事)?5. 有没有不必要地加了 `"use client"` 的组件(其实不需要交互)?
逐条给结果,指出具体文件。**先不要改**,我确认后再动手。上一节 ← 4.8 Next.js 核心:路由与页面跳转 下一节 → 4.10 浏览器开发者工具:定位问题的第一现场