Vibe Coding 实战指南:如何用 AI 写出靠谱代码

8 阅读7分钟

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>
  );
}

这里就是胶水编程的典型示范:

  • DragDropContextDroppable@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 组件包裹每个条目,传入 draggableIdindex,再展开库要求的 props。UI 部分完全由我们自己控制。


四、从 Demo 看方法论的落地效果

对照这套方法论,回顾整个开发过程:

规划阶段做的决策,在代码阶段全部兑现:

  • 功能边界 → 代码没有越界添加持久化/筛选等功能
  • 模块拆分 → 3 个组件各司其职,总共不到 100 行
  • 数据结构定义 → 字段名自始至终一致,text 就是 text
  • handleDragEndsplice 重排数组只有 3 行 — 真正的业务逻辑非常薄

胶水编程的效果:

  • 拖拽功能依赖的是 @hello-pangea/dnd(周下载量百万级的成熟库),而不是自己手写的几百行坐标计算
  • 我们用库提供的 DragDropContext / Droppable / Draggable 三个组件,把自己的 UI 和数据流串起来
  • 粘合代码不到 30 行,bug 空间极小

五、核心原则速查

原则做什么不做什么
先规划后编码明确技术栈、功能边界、模块拆分、数据结构不要跳过规划直接让 AI 写代码
胶水编程选用成熟库,写衔接/调用/数据流转代码不手写底层逻辑,不从零造轮子
审核规划再实现人确认规划无误后,再让 AI 写代码不让 AI 自主发挥加功能
模块化拆分每个组件职责单一不写 500 行的巨石组件

六、总结

用 AI 写代码,本质上是在做工程管理,而不是敲键盘。

把 AI 当新同事:先带它熟悉环境(/init),给它明确的需求文档(规划),让它用成熟工具干活(胶水编程),审核它的产出。

这套方法论不挑语言、不挑框架。无论是写 React 组件还是写后端接口,先想清楚再动手,能用现成的就不自己写 — 这本来就是软件工程里最朴素的好习惯,AI 时代只是让它变得更加重要了。


Demo 源码:demo1/todolist