TL;DR
- AI 写前端的瓶颈不是模型能力,是没有反馈回路
- 前端复杂性在数据/状态层。分离状态逻辑,AI 就能自我验证
- Store TDD 两阶段:AI 写行为契约 + 场景测试 → AI 实现 Store,人只审契约
- 不是技术创新,是前端测试动机的转折点:从"应该做"变成"必须做"
AI 写前端 = 盲写
AI 生成了一个漂亮的组件,JSX 没毛病,样式也对——但一跑起来,提交没校验、loading 不同步、错误后重试发两次请求。你让它改,它改出新 bug。再改,另一个地方又坏了。
问题不在 AI 的能力,在于没有反馈回路。 它不知道自己写对了没有,因为没有东西能告诉它。
这篇文章不讲更好的 prompt,讲更好的架构——让 AI 能自我验证前端代码。
这些场景你一定经历过
- "看起来对但逻辑全错"——JSX 结构没问题,但校验没做、状态不同步、重试发两次请求、切 tab 状态丢了
- "改一个 bug,出三个新 bug"——逻辑散落在 useState + useEffect + handler 各处,AI 改一个地方不知道会影响哪里
- "500 行组件 review 不过来"——状态、业务、UI 混在一起,得在脑子里模拟整个渲染流程
- "AI 没有浏览器,怎么知道写对了"——没有页面、没有点击、没有交互,本质上就是盲写
- "换个对话窗口全忘了"——又开始写 useState,又在组件里混逻辑,之前的模式全丢
- "复杂功能一口气写完,质量惨不忍睹"——多步表单、权限审批、实时协作,一口气出来就是一团乱码
根因就一个:AI 没有自我纠错能力。
前端的复杂性在数据层,不在 UI 层
真正复杂的前端功能——多步表单、实时同步、权限联动、乐观更新——全是数据层问题。
动画、布局、手势?要么纯 CSS 不涉及状态,要么最终落到状态变更上。UI 迭代改进从来都是最简单的部分。
把状态逻辑从 UI 中分离出来,AI 就能在无 DOM 环境下完成"写 → 测 → 改"的闭环。
状态逻辑(Store)→ AI 写 + AI 测 + AI 修正 ← 闭环,质量可保证
UI 层(组件) → AI/人写,人看效果 ← 开环,但只需要看视觉
人的注意力从"检查所有东西"缩小到"只看视觉和交互体验"。
但"分离状态逻辑"不是拆成一堆零散的 atom 和 slice。React 社区这几年越来越倾向原子化——状态碎片化到各个独立的 store,计算散落在 useMemo 和 selector 里,行为散落在 useEffect 和 handler 里。对人来说,这只是"跳几个文件"的事;对 AI 来说,碎片化就是致命的——它不知道这些碎片之间的关系,不知道改一个 slice 会不会影响另一个 hook 里的派生逻辑。
我们需要的是领域 Store——一个功能的 State、Computed、Action 收敛到一个地方:
领域 Store = 数据(State)+ 派生(Computed)+ 行为(Action)
一个领域 Store 就是一个完整的业务单元。AI 读一个文件就能理解整个功能的数据流,什么是 action、什么是 observable、什么是内部实现——全在一个地方写清楚了。new Store() + 调 action + 断言 getter,不依赖 React,不需要 mock DOM。
领域 Store 不只是代码组织方式,它是 AI 能理解的最小业务单元。 下面的 Store TDD 之所以能交给 AI,正是因为领域 Store 把"一个功能该做什么"表达成了 AI 可以读懂、可以测试、可以实现的形式。
Store TDD:两阶段行为优先开发
经过迭代,最终定型为行为优先 + 场景测试,两个阶段都交给 AI:
阶段 1:行为契约 + 场景测试(AI 写)
这一步整个交给 AI。输入可以是 PRD、需求文档、甚至口头描述。AI 做两件事:
- 提炼行为契约——梳理出用户操作(Action)和可观察输出(Observable)
- 按用户旅程写场景测试——每个
it()是一条完整操作路径
关键原则:不提前定义 State 结构。 State 是实现细节,测试断言 observable(getter),不断言 store.state.xxx。Action 是用户操作,不是 setter:
// ❌ 暴露裸 setter(测试它没意义)
setSelectedId(id) {}
setLoading(loading) {}
// ✅ 暴露用户操作(测试完整后果链)
selectGame(appCode) {} // 切换选中 + 拉取 checklist + 重算派生值
loadPage() {} // 拉取列表 + 初始化状态
submitStep(id, data) {} // 校验 + 调 API + 处理结果
AI 生成的测试按用户旅程组织,不按方法名:
// ❌ 按接口测试:你在测 Immer
describe('setSelectedId', () => {
it('should update selectedId', () => {
store.setSelectedId('abc')
expect(store.state.selectedId).toBe('abc')
})
})
// ✅ 按场景测试:你在测功能
describe('用户选择游戏', () => {
beforeEach(async () => {
await store.loadPage() // 前置条件通过 action 建立
})
it('选中后应更新选中状态并拉取 checklist', async () => {
await store.selectGame('game-a')
expect(store.selectedGame?.name).toBe('Game A')
expect(store.checklist).toHaveLength(3)
expect(store.canSubmit).toBe(false)
})
it('切换游戏时应重置已填写的步骤', async () => {
await store.selectGame('game-a')
await store.submitStep('step-1', { data: 'done' })
await store.selectGame('game-b')
expect(store.completedSteps).toHaveLength(0)
})
})
测试的"单元"是功能,不是类。用户不关心你有几个 Store,只关心"我点了切换游戏,checklist 和消息都刷新了"。
注意看这些测试用例:loadPage() → selectGame('game-a') → submitStep(...) → selectGame('game-b')——它就是用户的操作路径。每一个 it() 都是一条完整的用户旅程,每一个 expect() 都是用户能观察到的结果。
这意味着场景测试天然就是最精确的需求文档。给 AI 一份 PRD,它可能理解偏了;给它测试用例,操作路径和预期结果写得明明白白,没有歧义空间。测试用例既是验证工具,也是最好的 prompt。
阶段 2:实现 Store(AI 写)
测试有了,State 结构自然涌现——AI 读完测试里的 getter 断言(store.selectedGame、store.canSubmit、store.completedSteps),反推出需要哪些字段、什么类型。State 不是设计出来的,是被测试逼出来的:
// State 在实现时才定义,测试不关心它长什么样
interface GameFlowState {
games: Game[]
selectedAppCode: string | null
checklist: ChecklistItem[]
completedSteps: Record<string, StepData>
loading: boolean
}
class GameFlowStore extends ZenithStore<GameFlowState> {
constructor(private api: GameAPI) {
super({
games: [],
selectedAppCode: null,
checklist: [],
completedSteps: {},
loading: false,
})
}
// Observable:测试断言这些 getter,不关心内部结构
@memo((s) => [s.state.games, s.state.selectedAppCode])
get selectedGame() {
return this.state.games.find(g => g.appCode === this.state.selectedAppCode) ?? null
}
@memo((s) => [s.state.checklist, s.state.completedSteps])
get canSubmit() {
return this.state.checklist.length > 0
&& this.state.checklist.every(item => item.id in this.state.completedSteps)
}
// Action:用户操作,测试验证完整后果链
async selectGame(appCode: string) {
this.produce(draft => {
draft.selectedAppCode = appCode
draft.completedSteps = {}
})
const checklist = await this.api.fetchChecklist(appCode)
this.produce(draft => { draft.checklist = checklist })
}
async loadPage() {
this.produce(draft => { draft.loading = true })
const games = await this.api.fetchGames()
this.produce(draft => {
draft.games = games
draft.loading = false
})
}
async submitStep(id: string, data: StepData) {
this.produce(draft => { draft.completedSteps[id] = data })
await this.api.submitStep(id, data)
}
}
红了就改,绿了就对。
角色分工
| 步骤 | 谁做 | 输入 | 输出 |
|---|---|---|---|
| 行为契约 + 场景测试 | AI | PRD / 需求描述 / 已有测试用例 | store.spec.ts |
| 实现 Store | AI | 测试 | store.ts(测试全绿) |
| Review | 人 | 测试 + Store | 确认契约合理、测试覆盖完整 |
| UI 接线 | 人/AI | Store API | 组件代码 |
人的工作从"写契约"变成了"审契约"。 AI 根据 PRD 生成行为契约和测试,人审一遍就行。审什么?几个要点:
- action 列表是否覆盖了所有用户操作?有没有漏掉的入口?
- 场景是否包含异常路径?加载失败、并发冲突、空数据、权限不足
- getter 断言的是用户可感知的结果(
selectedGame.name)还是内部字段(state.selectedAppCode)? - 前置条件是否通过 action 建立,而不是直接塞 state?
通过了,AI 就对着测试写实现——第一次有了自我纠错能力。
自由发挥 vs Store TDD
没有约束时,AI 优化的是"看起来完成了"的速度:
- 状态管理走捷径——useState + useEffect 搞定,"看起来完成了"
- 异步只写 happy path——不处理失败、重试、并发取消
- 边界情况直接跳过——"先跳过后面再补"(然后就不补了)
- 计算属性偷懒内联——该抽 getter 的派生逻辑,直接在 JSX 里
.filter().map()
AI 不是不知道该写错误处理,是没有东西逼它写。测试就是那个东西。 场景测试里有"加载失败"、"重试恢复",不实现就不绿。
自由发挥优化"看起来完成了"的速度。Store TDD 优化"实际正确"的概率。
为什么不用 React Testing Library
组件测试能做,但成本高:DOM 模拟要配置维护,act() / waitFor() 仪式感重,改个 DOM 结构测试就挂,AI 写 RTL 测试经常和实现细节耦合。
Store 测试?new Store() + 调 action + 断言 getter。没有 React,没有 DOM,没有渲染。 跑完不到一秒。
适用边界
这套模式不是万能的:
| 场景 | 适合吗 | 原因 |
|---|---|---|
| 多步表单、权限联动、乐观更新 | ✅ 非常适合 | 复杂状态逻辑,Store TDD 收益最大 |
| 异步数据流、实时同步 | ✅ 适合 | 异步和并发正是测试最能保护的地方 |
| 纯展示页面、静态内容 | ❌ 不需要 | 没有复杂状态,直接写组件更快 |
| 几个字段的简单表单 | ❌ 不值得 | Store 化成本高于直接写组件 |
| 复杂手势/动画交互 | ⚠️ 部分适用 | 状态变更部分适用,视觉效果仍需人看 |
两个前提:
- 需求要清晰——行为契约质量决定一切,模糊需求产出的契约会跑偏
- Store 设计仍需人参与——哪些是 action、粒度多大、状态怎么分,这是设计判断
重构韧性
这套模式还有一个隐性收益:测试不绑定内部实现,重构随便来。
把内部变量名从 msgList 改成 messages,只要 getter 没变,测试一个不挂。把两个 Store 合并成一个,只要 action 和 observable 没变,测试一个不挂。
这在 AI 辅助开发里尤其重要——你可以放心让 AI 重构内部实现,测试会替你兜底。
如果你熟悉测试理论,会发现这套模式并不新鲜:Store 是可测试对象,React 组件是只做传声筒的 Humble Object;测试断言状态结果而非交互过程,是典型的底特律派 TDD;从外部行为契约出发再推导内部实现,是 Outside-In 设计。每个元素都有几十年的积累,只是在 AI 辅助开发的语境下,它们恰好组合成了一个闭环。
不是里程碑,是转折点
TDD 几十年了,MVC/MVVM 也几十年了。每一个元素都不新。
新的是测试的目的。
传统测试:帮人捉 bug。这套模式:给 AI 一个反馈回路——AI 写代码,AI 跑测试,AI 自己知道对不对。
结语
前端测试推了这么多年推不动,根本原因是成本收益不划算——写测试的人和受益的人是同一个人,投入产出算不过来。
AI 改变了这个等式。写测试的成本没变,但受益方多了一个:AI 本身。有测试,AI 就能自我纠错;没有测试,AI 每写一行你都得盯着看。测试从"应该做"变成了"必须做"。
越来越多用 AI 写前端的团队会发现同一件事:必须把状态逻辑从 UI 中分离出来。不是架构洁癖,是不分离就没法让 AI 高效工作。
AI 会倒逼前端架构向更可测试的方向演进。 这不是预测,是正在发生的事。
代码示例基于 Zenith。核心思路只有一个前提:状态逻辑能脱离 UI 独立测试。