模块三:产品设计与前端实战 | 第05讲:组件化开发——从混乱到规范,让 AI 写前端越做越快
项目:VibeNote 智能笔记(Next.js 14 + React + TypeScript + Tailwind + shadcn/ui)
本讲目标:把页面拆成稳定边界的组件树,用 props / state / composition 建立契约,让 AI 在固定骨架上增量演进,而不是每次重写一坨 JSX。读完本讲,你应该能解释:为什么同样的提示词,在「有组件契约的项目」里产出更稳定,在「一团乱麻的项目」里只能抽卡。
一、为什么「组件化」是 AI 时代的加速器
没有组件边界的项目,AI 每次生成都像在重写整页:类名漂移、状态散落、重复逻辑爆炸。你以为是模型不稳定,其实是架构没有给模型轨道。
组件化的本质不是「拆文件」,而是拆责任:
- 谁负责数据获取?
- 谁负责布局?
- 谁负责交互?
- 谁负责把 UI 与业务规则隔离?
当责任清晰,你的提示词可以从「做一个页面」缩小为「在 NoteEditor 内增加一个 prop 控制预览开关」——上下文更短、错误更可控。
你还可以把组件边界理解成信息隐藏:外层不需要知道内层用什么方式实现 Markdown 解析,只要约定输入输出。信息隐藏越好,AI 在局部重构时越不容易波及全局。反过来,如果所有状态都堆在页面顶层,AI 每次「小改」都会牵动大片 JSX,你看到的 diff 会像爆炸一样难以审查。
课程原文「4.4 组件化开发如何越做越快」强调:差距来自能否把业务拆成稳定步骤。本讲把它翻译成 React 组件层的工程语言。
flowchart TB
Page["Page 容器"] --> Layout["Layout 骨架"]
Layout --> Feature["Feature 组件"]
Feature --> UI["Presentational UI"]
Feature --> Hooks["hooks / lib"]
二、组件分层:Container vs Presentational(仍然好用)
容器组件:接数据、接路由参数、组合子组件、处理副作用。
展示组件:纯 UI,主要靠 props,不直接 fetch(除非极薄的数据钩子)。
对 VibeNote:
app/notes/page.tsx或app/(main)/layout.tsx偏容器components/note-list.tsx可以是展示(若数据由父传入)components/markdown-preview.tsx典型展示组件
这不是教条:当逻辑极简单,可以合并;当 AI 开始生成 400 行单文件,就必须拆。
flowchart LR
C["Container"] -->|props| P["Presentational"]
P -->|events| C
三、Props 设计:把组件当成「函数 API」
好的 props 名词清晰、默认值合理、可选性有理由。差 props:data: any、config: object。
建议:
- 用 TypeScript
interface写明每个字段 - 布尔 props 命名用
is/open/show一致前缀 - 回调用
onXxx命名 - 不要把「半个应用状态」塞进一个 mega prop
示例(节选):
export type NoteEditorProps = {
title: string;
initialBody: string;
saving?: boolean;
onChangeBody: (next: string) => void;
onSave: () => void;
};
四、State 上升与下降:唯一的难题,值得认真练
经验法则:
- 仅组件自用的 UI 状态(折叠、tab)放本地
- 跨组件共享的状态上升: lifting state up
- 跨页面共享用 URL / context / 外部 store(后续模块)
VibeNote 编辑区常见错误:把 body 同时存在父组件与子 textarea,导致双源真理。正确是:单一数据源,子组件受控或用最薄的封装。
再强调一个 AI 常见坑:模型喜欢在你没要求的情况下加 useEffect 去「同步」两个 state,这几乎总是坏味道。你要在规则里明确:禁止为了同步两个 React state 写 effect,除非处理外部系统(例如订阅)。把这条写进 AGENTS.md,你会少很多诡异 bug。
五、组合优于继承:用 children 与插槽模式
React 推荐组合:Sidebar + Main + children。对 AI 来说,组合模式比「继承一个 MegaComponent」更容易增量修改。
shadcn 的 CardHeader / CardContent 就是组合思想。
六、设计系统 = Tailwind token + shadcn 变体 + 你的约束文件
设计系统不是「下载一套皮肤」,而是三类约束:
- 语义色与间距 token(少写魔法数)
- 组件变体(Button 的 default/outline/ghost)
- 文档化规则(写在
AGENTS.md:禁止第二套 UI 库)
当你让 AI「按设计系统实现」,必须同时 @:
components/ui/*tailwind.config.*globals.css
设计系统还有一个隐藏收益:减少 code review 的品味争论。当间距只能用 gap-2/gap-4/gap-6 这一组阶梯,当圆角只能用 rounded-md 这类 token,评审者就不会陷入「我觉得 6px 更好」这种不可证伪讨论。讨论应集中在用户任务是否完成、状态是否覆盖、是否有无障碍回归——这些是可验证的。
对 VibeNote,建议你把「允许使用的间距刻度」写进约束:例如正文区域统一 p-4 md:p-6,列表项统一 py-3。刻度越少,界面越一致,AI 越不容易随机发挥。
七、AI 提示词模板:组件级(可直接复制)
提示词里务必写出「禁止事项」与「验收标准」,这与 PRD 是同一套思想:约束越明确,模型越不需要猜。下面模板可以复制后只替换组件名与 props 列表。
你是 React + TS 工程师。请实现 `components/note-toolbar.tsx`。
约束:
- 只用 shadcn Button + Tooltip + Separator
- props: onSave, onAiSummarize, disabled?: boolean
- 所有图标来自 lucide-react
- 导出类型 NoteToolbarProps
- 不要 fetch
验收:
- disabled 时按钮不可点
- Tooltip 有中文说明
八、可运行代码:小型组合组件 EditorShell
下面示例刻意保持「无业务逻辑」:它只做布局组合,方便你在多个页面复用。记住:布局组件越笨,越稳定;聪明的业务逻辑应放在 feature 组件或 hook 里,而不是藏在 EditorShell 的缝隙中。
// components/editor-shell.tsx
import type { ReactNode } from "react";
import { Separator } from "@/components/ui/separator";
export function EditorShell(props: {
sidebar: ReactNode;
main: ReactNode;
toolbar?: ReactNode;
}) {
return (
<div className="flex h-[calc(100vh-4rem)] w-full">
<aside className="hidden w-72 shrink-0 border-r md:block">{props.sidebar}</aside>
<div className="flex min-w-0 flex-1 flex-col">
{props.toolbar}
<Separator />
<div className="min-h-0 flex-1 overflow-hidden">{props.main}</div>
</div>
</div>
);
}
说明:
toolbar可传NoteToolbar;移动端侧栏可用Sheet包裹(第07讲实战补齐)。
九、从混乱到规范的路径(对照 4.5「样式体系与交互约束」)
- 先冻结技术栈(Tailwind + shadcn)
- 再冻结目录(
components/ui不手写业务) - 再冻结命名(
Note*前缀) - 最后才允许讨论「好不好看」
十、组件粒度:什么时候拆、什么时候不拆
拆的信号:单文件超过 ~200 行且职责超过两类;出现复制粘贴的 JSX;同一状态在多个分支重复判断。
不拆的信号:逻辑高度内聚、拆后 props 传递链过长、只是为了「显得专业」而拆。
VibeNote 的实战建议:MarkdownEditor 可以保留为一个 feature 组件,但要把 工具栏、预览、状态条拆出去。否则 AI 每次改工具栏都可能误伤预览区。
十一、Props drilling 的三种解法(按复杂度递增)
- 继续 drilling:层数 ≤3 时通常可接受
- 组合:通过
children把中间层变成布局壳 - Context:主题、用户信息、编辑器实例共享(谨慎,别放高频变更对象)
对 VibeNote,不要用 Context 存大文本 body(会导致无意义重渲染);body 仍建议由父层持有或使用状态管理库。
十二、自定义 Hooks:让 AI 生成「可测试的逻辑块」
把 useAiWriting 这类副作用封装为 hook:{ run, busy, error, reset }。组件只负责渲染,hook 负责与 /api/ai/writing 对话。这样你可以单独给 hook 写测试(未来模块),也能在 Storybook 里用 mock hook 渲染 UI。
flowchart LR
UI["Component"] --> H["useXxx Hook"]
H --> API["fetch / route"]
H --> UI
十三、文件与目录约定(强烈建议写进 AGENTS.md)
推荐结构(示例):
components/
ui/ # shadcn 生成,不手写业务
layout/ # shell、header
note/ # 领域组件 NoteList NoteEditor
ai/ # AIWritingToolbar 等
lib/
notes.ts # 纯函数、类型
hooks/
use-media-query.ts
规则:components/ui 只允许通过 CLI 更新;业务组件不得修改 ui 源码(除非升级 shadcn)。
十四、类型即文档:用 Zod 统一前后端形状(可选但强大)
在 AI 协作里,z.infer<typeof schema> 能显著减少「字段名不一致」。即便你暂时只在 client 用 zod 做表单校验,也值得把 Note schema 单独放 lib/schemas/note.ts。
十五、Storybook 不是必选项,但「组件沙箱」思维是必选项
没有 Storybook,也要能用最小页面路由 /dev/buttons 之类快速查看组件状态。否则你只能到真实页面里点点点,调试成本高,AI 也难对齐。
十六、组合模式实战:NoteWorkspace 把侧栏与编辑区拼起来
// components/note-workspace.tsx(示意)
import type { ReactNode } from "react";
import { EditorShell } from "@/components/editor-shell";
export function NoteWorkspace(props: {
list: ReactNode;
editor: ReactNode;
toolbar: ReactNode;
}) {
return (
<EditorShell sidebar={props.list} toolbar={props.toolbar} main={props.editor} />
);
}
意义:Page 层只负责取数与传参,NoteWorkspace 负责布局,NoteEditor 负责编辑体验——AI 改其中一层不易误伤其他层。
十七、避免「Props 爆炸」:用小型对象分组
当 props 超过 7 个,考虑分组:
type AiToolbarCallbacks = { onSummarize: () => void; onPolish: () => void };
比平铺 12 个回调更可读,也更利于 AI 增量修改。
十八、组件性能:memo 不是默认答案
React.memo 适合重渲染昂贵且 props 稳定的组件(大列表项)。乱用 memo 会增加心智负担。对 VibeNote,更优先:
useDeferredValue处理预览- 列表虚拟化(大笔记量时)
- 避免在 render 创建新对象当 props(用
useMemo)
十九、与 shadcn 协同:如何升级而不翻车
升级 shadcn 组件时:
- 先提交干净工作区
- 只升级一个组件目录
pnpm lint && pnpm build- 记录变更到 PR 描述
告诉 AI「升级 button」时,要明确 @components/ui/button.tsx 是唯一改动范围。
二十、AI 生成组件的 Review 清单
- props 类型完整吗?
- 有无隐式
any? - 是否把
fetch塞进了展示组件? - 是否遵循目录约定?
- 是否引入第二套 UI?
二十一、从 4.5 提炼的「样式与交互约束模板」(浓缩版)
贴进每次提示词:
样式:只用 Tailwind + shadcn token;禁止 inline style(除第三方必须)
交互:按钮请求期 disabled;错误 inline 或 toast;空状态有 CTA
布局:md 以下移动规则;md 以上桌面规则
组件:业务组件不得直接改 components/ui
二十二、组件契约与 PRD 的映射:一个表格搞定
| PRD 模块 | 组件落点 | 负责人 |
|---|---|---|
| 列表三态 | NoteListStates | 前端 |
| 编辑器 | NoteEditor | 前端 |
| AI 工具栏 | AIWritingToolbar | 前端 |
| AI API | app/api/ai/writing | 全栈 |
二十三、复用 vs 过度抽象:VibeNote 的实战边界
值得抽象:三态列表、工具栏按钮组、Markdown 预览封装。
不值得抽象:只用一次的 20 行布局,除非它已经在三个页面复制粘贴。
二十四、命名:让 AI 「猜对」比「炫技」重要
统一前缀 Note*、Ai*、App*。不要混用 Post、Article、Doc。命名稳定,glob 搜索与 @ 引用都更准。
二十五、错误边界:组件树也要兜底
在 feature 级加 error.tsx 或边界组件,避免编辑器异常拖死整站。边界组件里给「返回列表」链接,是专业 UX。
二十六、测试策略(轻量):从纯函数开始
lib/notes.ts 里的 sortNotes、truncateTitle 先测;组件后测。AI 生成纯函数更不容易翻车,你也更快建立信心。
二十七、与模块二的对齐:Workflow 在组件层的体现
Plan 输出组件树;Review 对照目录约定;Implement 按文件拆 PR;Verify 对照 Story/页面走查。
二十八、Mermaid:组件依赖图(示例)
graph TD
Page --> NoteWorkspace
NoteWorkspace --> EditorShell
NoteWorkspace --> NoteList
NoteWorkspace --> NoteToolbar
NoteWorkspace --> NoteEditor
NoteEditor --> MarkdownPreview
NoteToolbar --> useAiWriting
二十八补、从「文件拆分」到「边界拆分」:真正让 AI 加速的是后者
很多同学习惯把一个大文件拆成多个小文件,但 import 来 import 去,边界仍然是糊的:状态还是共享全局,副作用还是散落。更好的拆法是:先画组件边界与数据流,再决定文件。你可以用 10 分钟在白板上画箭头:数据从哪来、事件往哪去。画完再让 AI 生成文件,它会少很多「随机中间状态」。
二十九、可运行代码:useDebouncedValue(预览防抖)
// hooks/use-debounced-value.ts
import { useEffect, useState } from "react";
export function useDebouncedValue<T>(value: T, ms: number): T {
const [v, setV] = useState(value);
useEffect(() => {
const id = setTimeout(() => setV(value), ms);
return () => clearTimeout(id);
}, [value, ms]);
return v;
}
把防抖逻辑从组件里抽出来,AI 以后改预览策略时不会误触编辑器输入逻辑。
三十、Presentational 组件的「快照测试」思路(不依赖重工具)
即便你不写正式测试,也可以维护一份 fixtures/notes.json,在 dev 页面循环渲染列表项,肉眼看三态。这个习惯会逼迫你把 props 设计得更干净。
三十一、与 Tailwind 的协作:className 合并策略
使用 cn()(clsx + tailwind-merge)合并 class,避免 shadcn 变体与用户传入 className 冲突。告诉 AI:所有可复用组件末尾留 className?: string 并用 cn 合并。
三十二、组件文档:最小 viable doc(每个 feature 组件 5 行)
在组件文件顶部用注释写:
- 用途
- 关键 props
- 非目标(不处理什么)
这比另写长篇 Story 更可能真的被维护。
三十三、从 4.4/4.5 合并的「AI 不翻车」十条
- 技术栈冻结
- 目录冻结
- 组件命名冻结
- 禁止第二套 UI
- 异步必须三态
- 禁止客户端密钥
- props 必须类型化
- 大副作用进 hook
- 预览与编辑解耦
- 每次改动跑
lint/build
三十四、对抗「AI 复制粘贴式膨胀」:强制 diff 小步
要求 AI「一次 PR 只改一个组件文件 + 一个调用方」,否则你会得到 20 个文件同时变,review 形同虚设。
三十五、Server vs Client 组件边界(Next.js 14)
能在 Server Component 做的就不要 client:page.tsx 取数(若你数据层允许)→ 把数据作为 props 传给 client 编辑组件。把边界写进 PRD,AI 就不容易把 use client 撒得到处都是。
flowchart LR
S["Server Component"] -->|props| C["Client Component"]
C -->|events| API["Route Handler"]
三十六、组件化的组织心智:「领域优先」而非「技术优先」
优先按 note/、ai/ 分,而不是 buttons/、forms/。否则业务一复杂,你会不知道按钮属于哪个域。shadcn 的 ui/ 已经是技术分层了,业务层请领域化。
三十七、让 AI 生成组件树草稿:提示词
基于 PRD,输出 VibeNote 的组件树(缩进文本),标注 Server/Client。
要求:每个组件一句话职责;指出哪些 props 从父传入。
先树后代码,成功率更高。
三十八、从混乱到规范的四个里程碑(可打卡)
- 冻结 ui 目录
- 抽出 EditorShell
- 抽出三态列表
- 抽出 AI hook
完成四步,你的项目就从「能写」变成「能规模化写」。
三十八补、组件接口演进:如何不破坏调用方
实战里最常见的痛是:你为了给编辑器加功能,改了 props,结果列表页、弹窗页全部编译报错。缓解策略:
- 新增 props 用可选 + 默认值,尽量避免改名与删除。
- 大改版用 v2 组件并行:
NoteEditorV2与NoteEditor共存一段时间。 - 在 PR 描述列出破坏性变更并全局搜索引用点。
这三条对 AI 协作尤其重要,因为模型有时会「顺手重命名」。
三十八补 2、从 Figma 到代码:组件化翻译表
如果你有设计稿,可以建一张映射表:Frame 名称 → 组件名 → 变体。不要期待 AI 自动猜 Figma 图层名。把映射表贴在 PRD,生成代码会稳定得多。
三十八补 3、组件化的成本:什么时候会变慢?
早期会慢一点,因为你要写类型、拆文件、设约定。但一旦超过阈值(通常 3~5 个相关页面),组件化会显著加速。VibeNote 这种会持续迭代的产品,几乎必然跨过阈值。
三十八补 4、代码评审时只看三件事(组件 PR)
- 边界是否干净(副作用位置)
- props 是否可理解(命名与类型)
- 是否破坏设计系统约束
避免在评审里争论「这行 class 要不要换」,那是低杠杆争论。
三十八补 5、给 AI 的「禁止清单」示例(强烈建议放进规则)
- 不允许在
components/ui内写业务 if/else - 不允许引入
antd/mui(若你选定 shadcn) - 不允许把
OPENAI_API_KEY写进 client - 不允许创建第二个
Button组件(除非明确重构)
禁止清单比愿望清单更能降低随机性。
三十八补 6、forwardRef 与可访问性:别忽略焦点管理
当你封装 Textarea 或组合输入框时,可能需要 forwardRef 以便父层聚焦。AI 有时会漏掉,导致快捷键聚焦失败。把「需要 ref 的组件」写进组件注释,能显著减少遗漏。
三十八补 7、组件化的终局:特征文件夹(feature folder)
当 components/note 里超过 6 个文件,可以升级为:
features/note/
components/
hooks/
lib/
Next.js 并不强制,但这是可维护性的自然演进。
三十八补 8、和设计师/产品的沟通语言:用组件名而不是文件名
评审时说「改 NoteToolbar」比说「改第二个文件」明确。组件名成为沟通协议后,AI 输出也会更对齐。
三十八补 9、总结一句:组件化是「把不确定性局部化」
软件工程里很多问题都是全局不确定性导致的。组件化就是把不确定性关进小盒子里:盒子有接口,接口可测试,测试可回归。AI 再强,也需要这种工程结构来承接。
三十八补 10、FAQ:组件化最常见的 8 个问答
Q:组件是不是越小越好?
A:不是。过小会导致 props 传递链与组合成本上升;以「职责单一」为准,不以行数为准。
Q:什么时候用 context?
A:跨多层共享且变更不频繁的配置型数据;不要塞高频文本内容。
Q:Tailwind class 太长怎么办?
A:提取小组件或 cva 变体;不要把长 class 复制十次。
Q:shadcn 组件能改吗?
A:可以,但要有纪律:改动要可追踪,且不要混入业务分支。
Q:AI 总喜欢在 page 里写一堆逻辑怎么办?
A:在提示词里强制「page 只做组合,逻辑下沉 hook/feature」。
Q:要不要上 zustand?
A:MVP 未必需要;当出现多个页面共享编辑器草稿时再引入。
Q:组件文件名用 PascalCase 还是 kebab-case?
A:React 社区常见 PascalCase.tsx;关键是统一。
Q:多久重构一次组件树?
A:当你连续两次需求都「改同一段巨型 JSX」时,就是重构信号。
三十八补 11、实战练习:用 30 分钟做一次「组件化体检」
- 列出项目中最大的 3 个文件。
- 标注每个文件的职责数量。
- 选最痛的一个,画出目标组件树。
- 让 AI 按树拆分,但要求「一次只提交一个组件」。
- 跑
lint/build后合并。
这套练习比看十篇教程更有用。
你也可以把「组件化体检」变成每个迭代的固定仪式:不需要每次都大拆,只要持续阻止文件无节制增长,就能把技术债压在可控范围。对 solo 开发者,这份仪式相当于给自己当 Tech Lead:冷酷一点,长期更省时间。
三十八补 12、与 VibeNote V3 的明确衔接
第07讲会把 EditorShell、NoteWorkspace、侧栏与编辑器组合成完整页面。你现在学的不是抽象理论,而是为实战预埋骨架。若你跳过本讲直接堆页面,V3 很容易变成「一次性代码山」。
三十八补 13、组件化与可观测性:让日志也遵循边界
建议在 hook 层统一记录 AI 调用日志(含 mode、耗时、失败原因码),不要在每个按钮里随手 console.log。日志边界清晰后,排障会快很多,也更方便你把事件映射到产品指标(第02讲的学习指标)。
三十八补 14、从「复制 AI 代码」到「合并 AI 代码」:PR 描述模板
组件:NoteToolbar
变更:新增 disabled 聚合;抽取 useAiWriting
风险:无破坏性;调用方 +2
验证:pnpm lint && pnpm build;手工点 AI 三按钮
把模板贴进 PULL_REQUEST_TEMPLATE.md,你的仓库会自动变专业。
三十九、复盘清单
- 你能画出 VibeNote 的组件树吗?
- 你的
components/ui是否保持「无污染」? - 你是否能给 AI 一段固定约束模板?
- 你是否能指出项目里最需要被拆分的「巨型组件」并说明理由?
- 你是否已经把目录约定同步到
AGENTS.md?若还没有,现在就做。
四十、思考题
- 你会把 AI 请求逻辑放在组件里还是 hook 里?边界是什么?
- 如何避免
components目录变成垃圾场? - 什么时候该拆
features/目录? - 给
NoteEditor设计一版 props,使其同时支持「只读预览模式」与「编辑模式」,并说明如何不破坏旧调用方。 - 你会如何定义「组件完成」的 DoD(至少 5 条)?
- 如果 AI 生成了 400 行单文件,你的第一步拆分策略是什么?
四十一、下讲预告
当你把组件边界、目录约束、提示词模板固化下来,你会发现 AI 写前端不再是「抽卡」,而是「在轨道上加速」。下一讲进入响应式与交互细节:同样一套组件,如何在不同输入设备上保持专业体验——这不是简单加几个 md: 前缀就能糊弄过去的。
第06讲讲 响应式与交互体验:Tailwind 断点、移动优先、触控与无障碍基础。
四十二、结语
组件化不是为了炫架构,而是为了让你在 Vibe Coding 中拥有可累积的工程资产。每一次清晰的边界,都会让下一次提示词更短、更准、更便宜。
当你开始用「组件契约」思考界面,你会自然厌恶「巨型页面文件」:那不是勤奋,而是把复杂性推迟到不可维护的未来。把复杂关进小盒子,不是限制创造力,而是让创造力重复可用。
参考:课程原文 课程内容/4.4 组件化开发如何越做越快?从混乱到规范的路径.md、课程内容/4.5 让AI写前端不翻车:样式体系与交互约束模板.md。
附:当你觉得「组件化好麻烦」,把它翻译成一句话——麻烦一次,省事一百次。Vibe Coding 的爽感来自速度,但速度必须建立在结构上,否则你只是更快地把仓库变成废墟;结构不是为了好看,是为了可逆与可协作。
把本讲与第04讲连起来:UI/UX 解决「用户觉得好不好用」,组件化解决「你改起来贵不贵」。两者叠加,才是专业前端。下一讲我们会把这些组件放进不同屏幕尺寸与输入设备里检验,你会发现很多「桌面完美」在手机上会瞬间露馅;提前在组件层预留响应式策略,比在发布前连夜改样式更体面,也更省钱。