Vibe Coding 实战指南:如何用 AI 写出靠谱代码
让 AI 写代码不难,难的是让它写出能跑、好改、不失控的代码。本文通过一个完整的 React 待办清单 demo,分享一套可复用的 AI 协作方法论。
一、问题:AI 写代码的两大陷阱
用 AI 写过代码的人大概率都踩过这两个坑:
幻觉代码 — 看起来像那么回事,一跑就报错。API 是编的,参数是猜的,import 的包根本不存在。
屎山代码 — 能跑,但结构混乱。一个组件 500 行,状态管理全靠全局变量,想改一个功能牵一发动全身。
本质原因就一条:我们把 AI 当工具用,而不是当同事带。
想象一下,一个新员工入职,你不给他看项目文档、不讲技术规范、不划定需求边界,直接扔一句"帮我写个登录页"——他写得出来,但大概率不是你想要的。AI 同理。
二、解法:从"下指令"变成"定规矩"
核心思路就两步:先规划,后编码。只做胶水,不造轮子。
第一步:规划就是一切
不要上来就让 AI 写代码。先让它读懂你的技术栈、项目结构、需求边界。
具体做法——在项目根目录跑 /init,让 AI 生成 CLAUDE.md 规划文件。之后每次对话,这份规划都会被自动带上,AI 始终知道自己在什么环境下干活。
然后,每次提需求时,先让 AI 输出一份纯文字规划,包含:
1. 技术栈确认(React 19 + Tailwind CSS + useState)
2. 功能边界(新增/删除/切换完成 — 不做持久化、不做筛选、不做拖拽)
3. 模块拆分(输入框组件、待办条目组件、列表容器组件)
4. 数据流定义(useState 管理 tasks 数组,数据结构:{id, text, completed})
5. 输出规划,等人确认,确认无误后再写代码
等你看过规划、确认没问题了,再让 AI 动笔。
为什么这一步省不掉?
- 提前划定功能边界,防止 AI 擅自加戏,无限膨胀
- 强制模块拆分,生成的代码天然好读好改
- 预先规定数据结构(字段叫
text不能叫title),从源头杜绝字段名幻觉 - 规划本身随项目演进持续更新,成为项目的"活文档"
第二步:胶水编程思维
能抄不写,能连不造。轮子别人造好,你只做胶水。
这句话是整个方法论的精髓。展开说:
| 做法 | 结果 | 风险 |
|---|---|---|
| 让 AI 从零手写拖拽逻辑 | 坐标监听、排序算法、边界 case 几百行 | 高幻觉、难维护 |
| 用 react-beautiful-dnd 包一下 | 只写 20 行粘合代码,把库和自己的数据结构接上 | 几乎不出错 |
胶水编程的核心原则:决不自己手写底层逻辑,优先选用社区长期验证过的成熟方案,然后只写衔接、调用、数据流转的粘合代码。
AI 生成的代码越少,幻觉概率越低;引用的成熟库越多,可靠性越高。
三、Demo 拆解:React 待办清单完整实现
下面通过一个完整 demo,展示"先规划 + 胶水编程"的全流程。
3.1 技术栈
| 层 | 选型 |
|---|---|
| 框架 | React 19 |
| 样式 | Tailwind CSS 3 |
| 构建 | Vite 6 |
| 拖拽 | @hello-pangea/dnd(react-beautiful-dnd 的维护 fork) |
3.2 规划阶段
在写第一行代码之前,先敲定:
功能边界:
- 新增待办、删除待办、切换完成状态
- 拖拽排序
- 不做:本地持久化、分类筛选、截止日期、远程同步
模块拆分(乐高组件):
App ← 唯一的状态容器(useState)
├── AddTodo ← 输入框 + 添加按钮
└── TodoList ← 拖拽上下文 + 列表容器
└── TodoItem ← 单条待办(可拖拽)
数据结构:
// tasks 数组,由 App 的 useState 管理
tasks = [
{ id: 1711699200000, text: '写规划文档', completed: true },
{ id: 1711699300000, text: '实现组件', completed: false },
]
数据流:
AddTodo ──onAdd(text)──→ App (tasks state)
├── onToggle(id) ─→ 切换 completed
├── onDelete(id) ─→ 删除
└── onDragEnd(result) ─→ 重排数组
3.3 代码实现(粘合代码)
App.jsx — 状态容器 + 业务逻辑
import { useState } from 'react';
import AddTodo from './components/AddTodo';
import TodoList from './components/TodoList';
export default function App() {
const [tasks, setTasks] = useState([]);
const handleAdd = (text) => {
setTasks([...tasks, { id: Date.now(), text, completed: false }]);
};
const handleToggle = (id) => {
setTasks(
tasks.map((t) => (t.id === id ? { ...t, completed: !t.completed } : t))
);
};
const handleDelete = (id) => {
setTasks(tasks.filter((t) => t.id !== id));
};
const handleDragEnd = (result) => {
if (!result.destination) return;
const reordered = [...tasks];
const [moved] = reordered.splice(result.source.index, 1);
reordered.splice(result.destination.index, 0, moved);
setTasks(reordered);
};
return (
<div className="max-w-lg mx-auto mt-10 p-6">
<h1 className="text-2xl font-bold text-center text-gray-800 mb-6">
待办清单
</h1>
<AddTodo onAdd={handleAdd} />
<div className="mt-4">
<TodoList
tasks={tasks}
onToggle={handleToggle}
onDelete={handleDelete}
onDragEnd={handleDragEnd}
/>
</div>
</div>
);
}
App 是一切的"大脑"——它持有唯一的 tasks 状态,所有子组件只负责 UI 和事件上报,完全不管理状态。
AddTodo.jsx — 输入组件
import { useState } from 'react';
export default function AddTodo({ onAdd }) {
const [text, setText] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (!text.trim()) return;
onAdd(text.trim());
setText('');
};
return (
<form onSubmit={handleSubmit} className="flex gap-2">
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="输入新的待办..."
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg
focus:outline-none focus:ring-2 focus:ring-blue-500
focus:border-transparent"
/>
<button
type="submit"
className="px-4 py-2 bg-blue-500 text-white rounded-lg
hover:bg-blue-600 transition-colors"
>
添加
</button>
</form>
);
}
纯展示组件,只管本地输入框状态和提交回调,不知道也不关心外部数据如何存储。
TodoList.jsx — 拖拽容器(关键的粘合代码)
import { DragDropContext, Droppable } from '@hello-pangea/dnd';
import TodoItem from './TodoItem';
export default function TodoList({ tasks, onToggle, onDelete, onDragEnd }) {
if (tasks.length === 0) {
return (
<p className="text-center text-gray-400 py-8">
暂无待办,添加一个吧
</p>
);
}
return (
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="todolist">
{(provided) => (
<ul
ref={provided.innerRef}
{...provided.droppableProps}
className="space-y-2"
>
{tasks.map((todo, index) => (
<TodoItem
key={todo.id}
todo={todo}
index={index}
onToggle={onToggle}
onDelete={onDelete}
/>
))}
{provided.placeholder}
</ul>
)}
</Droppable>
</DragDropContext>
);
}
这里就是胶水编程的典型示范:
DragDropContext和Droppable是@hello-pangea/dnd提供的成熟组件- 我们做的事情只是:提供
droppableId、绑定ref、展开droppableProps、传入placeholder - 拖拽的重排逻辑在
@hello-pangea/dnd内部完成,我们只通过onDragEnd回调拿到结果,然后把自己的tasks数组重排
我们没有写一行拖拽底层代码,只是把库和自己的数据流接在一起。
TodoItem.jsx — 可拖拽条目
import { Draggable } from '@hello-pangea/dnd';
export default function TodoItem({ todo, index, onToggle, onDelete }) {
return (
<Draggable draggableId={String(todo.id)} index={index}>
{(provided) => (
<li
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
className="flex items-center gap-3 p-3 bg-white rounded-lg shadow-sm"
>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
className="w-5 h-5 text-blue-500 rounded focus:ring-blue-500"
/>
<span className={`flex-1 ${
todo.completed ? 'line-through text-gray-400' : 'text-gray-800'
}`}>
{todo.text}
</span>
<button
onClick={() => onDelete(todo.id)}
className="px-3 py-1 text-sm text-red-500 hover:bg-red-50
rounded transition-colors"
>
删除
</button>
</li>
)}
</Draggable>
);
}
同样的胶水思路:用 Draggable 组件包裹每个条目,传入 draggableId 和 index,再展开库要求的 props。UI 部分完全由我们自己控制。
四、从 Demo 看方法论的落地效果
对照这套方法论,回顾整个开发过程:
规划阶段做的决策,在代码阶段全部兑现:
- 功能边界 → 代码没有越界添加持久化/筛选等功能
- 模块拆分 → 3 个组件各司其职,总共不到 100 行
- 数据结构定义 → 字段名自始至终一致,
text就是text handleDragEnd中splice重排数组只有 3 行 — 真正的业务逻辑非常薄
胶水编程的效果:
- 拖拽功能依赖的是
@hello-pangea/dnd(周下载量百万级的成熟库),而不是自己手写的几百行坐标计算 - 我们用库提供的
DragDropContext/Droppable/Draggable三个组件,把自己的 UI 和数据流串起来 - 粘合代码不到 30 行,bug 空间极小
五、核心原则速查
| 原则 | 做什么 | 不做什么 |
|---|---|---|
| 先规划后编码 | 明确技术栈、功能边界、模块拆分、数据结构 | 不要跳过规划直接让 AI 写代码 |
| 胶水编程 | 选用成熟库,写衔接/调用/数据流转代码 | 不手写底层逻辑,不从零造轮子 |
| 审核规划再实现 | 人确认规划无误后,再让 AI 写代码 | 不让 AI 自主发挥加功能 |
| 模块化拆分 | 每个组件职责单一 | 不写 500 行的巨石组件 |
六、总结
用 AI 写代码,本质上是在做工程管理,而不是敲键盘。
把 AI 当新同事:先带它熟悉环境(/init),给它明确的需求文档(规划),让它用成熟工具干活(胶水编程),审核它的产出。
这套方法论不挑语言、不挑框架。无论是写 React 组件还是写后端接口,先想清楚再动手,能用现成的就不自己写 — 这本来就是软件工程里最朴素的好习惯,AI 时代只是让它变得更加重要了。
Demo 源码:demo1/todolist