3.5 组件化开发——从混乱到规范,让 AI 写前端越做越快

4 阅读19分钟

模块三:产品设计与前端实战 | 第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.tsxapp/(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: anyconfig: 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 变体 + 你的约束文件

设计系统不是「下载一套皮肤」,而是三类约束:

  1. 语义色与间距 token(少写魔法数)
  2. 组件变体(Button 的 default/outline/ghost)
  3. 文档化规则(写在 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「样式体系与交互约束」)

  1. 先冻结技术栈(Tailwind + shadcn)
  2. 再冻结目录(components/ui 不手写业务)
  3. 再冻结命名(Note* 前缀)
  4. 最后才允许讨论「好不好看」

十、组件粒度:什么时候拆、什么时候不拆

拆的信号:单文件超过 ~200 行且职责超过两类;出现复制粘贴的 JSX;同一状态在多个分支重复判断。
不拆的信号:逻辑高度内聚、拆后 props 传递链过长、只是为了「显得专业」而拆。

VibeNote 的实战建议:MarkdownEditor 可以保留为一个 feature 组件,但要把 工具栏预览状态条拆出去。否则 AI 每次改工具栏都可能误伤预览区。


十一、Props drilling 的三种解法(按复杂度递增)

  1. 继续 drilling:层数 ≤3 时通常可接受
  2. 组合:通过 children 把中间层变成布局壳
  3. 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 组件时:

  1. 先提交干净工作区
  2. 只升级一个组件目录
  3. pnpm lint && pnpm build
  4. 记录变更到 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 APIapp/api/ai/writing全栈

二十三、复用 vs 过度抽象:VibeNote 的实战边界

值得抽象:三态列表、工具栏按钮组、Markdown 预览封装。
不值得抽象:只用一次的 20 行布局,除非它已经在三个页面复制粘贴。


二十四、命名:让 AI 「猜对」比「炫技」重要

统一前缀 Note*Ai*App*。不要混用 PostArticleDoc。命名稳定,glob 搜索与 @ 引用都更准。


二十五、错误边界:组件树也要兜底

在 feature 级加 error.tsx 或边界组件,避免编辑器异常拖死整站。边界组件里给「返回列表」链接,是专业 UX。


二十六、测试策略(轻量):从纯函数开始

lib/notes.ts 里的 sortNotestruncateTitle 先测;组件后测。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 不翻车」十条

  1. 技术栈冻结
  2. 目录冻结
  3. 组件命名冻结
  4. 禁止第二套 UI
  5. 异步必须三态
  6. 禁止客户端密钥
  7. props 必须类型化
  8. 大副作用进 hook
  9. 预览与编辑解耦
  10. 每次改动跑 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 从父传入。

先树后代码,成功率更高。


三十八、从混乱到规范的四个里程碑(可打卡)

  1. 冻结 ui 目录
  2. 抽出 EditorShell
  3. 抽出三态列表
  4. 抽出 AI hook

完成四步,你的项目就从「能写」变成「能规模化写」。


三十八补、组件接口演进:如何不破坏调用方

实战里最常见的痛是:你为了给编辑器加功能,改了 props,结果列表页、弹窗页全部编译报错。缓解策略:

  1. 新增 props 用可选 + 默认值,尽量避免改名与删除。
  2. 大改版用 v2 组件并行NoteEditorV2NoteEditor 共存一段时间。
  3. 在 PR 描述列出破坏性变更并全局搜索引用点。

这三条对 AI 协作尤其重要,因为模型有时会「顺手重命名」。


三十八补 2、从 Figma 到代码:组件化翻译表

如果你有设计稿,可以建一张映射表:Frame 名称 → 组件名 → 变体。不要期待 AI 自动猜 Figma 图层名。把映射表贴在 PRD,生成代码会稳定得多。


三十八补 3、组件化的成本:什么时候会变慢?

早期会慢一点,因为你要写类型、拆文件、设约定。但一旦超过阈值(通常 3~5 个相关页面),组件化会显著加速。VibeNote 这种会持续迭代的产品,几乎必然跨过阈值。


三十八补 4、代码评审时只看三件事(组件 PR)

  1. 边界是否干净(副作用位置)
  2. props 是否可理解(命名与类型)
  3. 是否破坏设计系统约束

避免在评审里争论「这行 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 分钟做一次「组件化体检」

  1. 列出项目中最大的 3 个文件。
  2. 标注每个文件的职责数量。
  3. 选最痛的一个,画出目标组件树。
  4. 让 AI 按树拆分,但要求「一次只提交一个组件」。
  5. lint/build 后合并。

这套练习比看十篇教程更有用。

你也可以把「组件化体检」变成每个迭代的固定仪式:不需要每次都大拆,只要持续阻止文件无节制增长,就能把技术债压在可控范围。对 solo 开发者,这份仪式相当于给自己当 Tech Lead:冷酷一点,长期更省时间。


三十八补 12、与 VibeNote V3 的明确衔接

第07讲会把 EditorShellNoteWorkspace、侧栏与编辑器组合成完整页面。你现在学的不是抽象理论,而是为实战预埋骨架。若你跳过本讲直接堆页面,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?若还没有,现在就做。

四十、思考题

  1. 你会把 AI 请求逻辑放在组件里还是 hook 里?边界是什么?
  2. 如何避免 components 目录变成垃圾场?
  3. 什么时候该拆 features/ 目录?
  4. NoteEditor 设计一版 props,使其同时支持「只读预览模式」与「编辑模式」,并说明如何不破坏旧调用方。
  5. 你会如何定义「组件完成」的 DoD(至少 5 条)?
  6. 如果 AI 生成了 400 行单文件,你的第一步拆分策略是什么?

四十一、下讲预告

当你把组件边界、目录约束、提示词模板固化下来,你会发现 AI 写前端不再是「抽卡」,而是「在轨道上加速」。下一讲进入响应式与交互细节:同样一套组件,如何在不同输入设备上保持专业体验——这不是简单加几个 md: 前缀就能糊弄过去的。

第06讲响应式与交互体验:Tailwind 断点、移动优先、触控与无障碍基础。


四十二、结语

组件化不是为了炫架构,而是为了让你在 Vibe Coding 中拥有可累积的工程资产。每一次清晰的边界,都会让下一次提示词更短、更准、更便宜。

当你开始用「组件契约」思考界面,你会自然厌恶「巨型页面文件」:那不是勤奋,而是把复杂性推迟到不可维护的未来。把复杂关进小盒子,不是限制创造力,而是让创造力重复可用。


参考:课程原文 课程内容/4.4 组件化开发如何越做越快?从混乱到规范的路径.md课程内容/4.5 让AI写前端不翻车:样式体系与交互约束模板.md

附:当你觉得「组件化好麻烦」,把它翻译成一句话——麻烦一次,省事一百次。Vibe Coding 的爽感来自速度,但速度必须建立在结构上,否则你只是更快地把仓库变成废墟;结构不是为了好看,是为了可逆与可协作。

把本讲与第04讲连起来:UI/UX 解决「用户觉得好不好用」,组件化解决「你改起来贵不贵」。两者叠加,才是专业前端。下一讲我们会把这些组件放进不同屏幕尺寸与输入设备里检验,你会发现很多「桌面完美」在手机上会瞬间露馅;提前在组件层预留响应式策略,比在发布前连夜改样式更体面,也更省钱。