实战 Vibe Coding:用 AI 从零搭一个 React 待办清单

想用 AI 写代码,结果不是「幻觉代码」------看着像那么回事,一跑就崩;就是「屎山代码」------能跑,但乱到想改都不知道从哪下手。

这篇不是教你写代码,而是教你怎么和 AI 协作,写出靠谱的代码

我们以「React + Tailwind 实现一个待办清单(TodoList)」为实战案例,完整走一遍流程。


🧭 一个核心认知:把 AI 当「同事」,而不是「代码生成器」

新手用 AI 写代码,最常见的姿势是:

💬 「帮我写一个 React 待办清单页面,支持新增、删除任务。」

然后 AI 啪一下吐出一大坨代码。你以为省事了,其实埋了一堆坑:

❓ 隐患 说明
🤔 要不要本地存储? AI 没问,可能擅自加了 localStorage
🤔 要不要远程 API? AI 可能凭空造了一个后端接口
🤔 要不要拖拽排序? AI 可能手写一套坐标监听逻辑
🤔 要不要优先级标签? 功能无限膨胀,直接变屎山

问题的根源 :你把 AI 当成了「只会机械输出的代码机器」,而它其实该是你的伙伴、同事、助手

就像你不会让一个刚入职的员工直接上手干活------得先让他读《员工手册》、熟悉业务流程。对 AI 也是同理。


📋 第一步:规划就是一切

不要让 AI 急着写代码,先强制它读懂你的技术栈和项目规划。

先规划,再编码 🎯

给 AI 下达的第一条指令,应该禁止它写任何代码,只输出规划:

text 复制代码
遵守胶水编程思维:优先使用成熟方案,避免凭空造逻辑。

第一个阶段:只做规划,禁止输出任何代码。

1. 确认技术栈:
   React 19 + Tailwind CSS + useState

2. 梳理功能边界:
   - 新增待办、删除待办、切换完成状态
   - 不做本地持久化、筛选、拖拽功能

3. 拆分模块(乐高组件):
   输入框组件、待办条目组件、列表容器组件

4. 定义数据流:
   useState 存储 task 数组,数据结构:
   { id, text, completed }

5. 输出这份完整规划,等待我确认无误后,再分段实现代码

这份规划在防什么?🛡️

规划动作 背后的逻辑
📐 划定边界 防止 AI 擅自多加功能,功能无限膨胀成屎山
🧱 强制模块拆分 代码好读、好维护,AI 生成的代码我们也要审核
🧩 引导复用独立组件 每个组件职责单一,像乐高积木一样拼装
📦 预定数据结构 从根源减少 AI 的「参数/字段幻觉」------字段叫 text 就是 text,不能让 AI 自己拍脑袋叫 title

一句话总结 :先生成规划 → 审核规划 → 更新规划。这份规划将伴随项目开发的整个生命周期


🧩 第二步:胶水编程思维

能抄不写,能连不造。

轮子别人造好,你只做胶水;胶水不生产零件,只联通零件。

胶水是什么?🔗

  • 能抄不写:去 GitHub 找那些经典、优质、被验证过千万次的代码。
  • 能连不造:A、B、C 组件之间,用胶水把它们黏合起来。
  • 绝不从零自研底层逻辑:手写底层 = 高幻觉 + 难维护。

胶水本身不创造零件 ,只负责把现成零件黏在一起。你只负责写衔接、调用、流转的粘合代码,把各个模块联通。AI 生成的代码量越少,产生幻觉和屎山的可能性就越低。

实战对比:拖拽排序 🖱️

待办清单做完基础功能后,你想加一个「拖拽排序」。两种做法,天差地别:

❌ 错误示范(从零造零件)

💬 「帮我写 React 待办清单的拖拽排序功能」

AI 很可能凭空手写一套拖拽逻辑:自行实现坐标监听、碰撞检测、排序算法......

问题

  • 🐛 手写拖拽的边界 case 极多(边界判断、跨元素位置、动画回弹......),极易出幻觉 bug
  • 💩 代码冗长复杂,难以维护
✅ 正确示范(胶水)
text 复制代码
遵守胶水编程原则:绝不从零自研底层逻辑,优先选择社区长期验证的成熟开源组件。

当前需求:给待办列表增加拖拽排序。

1. 先调研:React 生态成熟的拖拽库,优先选用 @dnd-kit(业内广泛使用)
2. 不要自己手写拖拽底层代码,只做粘合工作
3. 输出内容顺序:
   - 安装依赖命令
   - 把现有 TodoList 组件和 @dnd-kit 组件进行衔接
   - 只写模块之间适配、数据流转的粘合代码

结果 :AI 只需要把 @dnd-kit 这个「现成零件」接进你的组件里,写几行粘合代码就够了。底层拖拽逻辑交给社区验证过的库,稳。


🧠 第三步:用元方法论让 AI 自我进化

AI 不只会写代码,还能帮你不断优化提示词本身

  • 🔧 利用 cc / codex记忆模块harness 架构
  • 📈 它可以持续帮我们打磨提示词
  • 🔁 用「阿尔法提示词」定义怎么干活 → 生成结果 → 用「欧米茄提示词」去打分、判断 → 反过来优化提示词

就这样,AI 系统不断地自我进化------你越用,它越懂你


🛠️ 实战全景:TodoList 项目拆解

现在回到我们的待办清单。按照规划,项目结构和数据流如下:

text 复制代码
todolist/
├── index.html
├── package.json          # React 19 + Vite 6 + Tailwind 4 + @dnd-kit
├── vite.config.js
└── src/
    ├── main.jsx          # 入口
    ├── App.jsx           # 🧠 状态中枢:tasks 数组 + 增删改排
    ├── index.css         # @import "tailwindcss"
    └── components/
        ├── TodoInput.jsx # ⌨️ 输入框组件
        ├── TodoItem.jsx  # 📄 待办条目组件(含拖拽手柄)
        └── TodoList.jsx  # 📋 列表容器组件(拖拽上下文)

📦 数据结构(严格按规划)

js 复制代码
{ id, text, completed }  // 字段名早已定死,AI 不许乱改

🧠 App.jsx ------ 状态中枢

所有状态和业务逻辑都收拢在这里,子组件只负责「接收 props + 触发回调」:

jsx 复制代码
import { useState } from 'react'
import { arrayMove } from '@dnd-kit/sortable'
import TodoInput from './components/TodoInput'
import TodoList from './components/TodoList'

export default function App() {
  const [tasks, setTasks] = useState([])

  const addTask = (text) => {
    setTasks((prev) => [
      ...prev,
      { id: crypto.randomUUID(), text, completed: false },
    ])
  }

  const toggleTask = (id) => {
    setTasks((prev) =>
      prev.map((task) =>
        task.id === id ? { ...task, completed: !task.completed } : task,
      ),
    )
  }

  const deleteTask = (id) => {
    setTasks((prev) => prev.filter((task) => task.id !== id))
  }

  const reorderTask = (activeId, overId) => {
    setTasks((prev) => {
      const oldIndex = prev.findIndex((t) => t.id === activeId)
      const newIndex = prev.findIndex((t) => t.id === overId)
      return arrayMove(prev, oldIndex, newIndex)  // 🔗 胶水:复用 @dnd-kit 的排序函数
    })
  }

  return (
    <div className="mx-auto flex min-h-screen max-w-xl flex-col items-center gap-6 px-4 py-10">
      <h1 className="text-3xl font-bold text-gray-800">待办清单</h1>
      <TodoInput onAdd={addTask} />
      <TodoList
        tasks={tasks}
        onToggle={toggleTask}
        onDelete={deleteTask}
        onReorder={reorderTask}
      />
    </div>
  )
}

💡 亮点 :拖拽排序只用了 arrayMove(prev, oldIndex, newIndex) 一行胶水代码,底层坐标监听、碰撞检测全部交给 @dnd-kit

⌨️ TodoInput.jsx ------ 输入框组件

jsx 复制代码
import { useState } from 'react'

export default function TodoInput({ onAdd }) {
  const [value, setValue] = useState('')

  const handleSubmit = (e) => {
    e.preventDefault()
    const text = value.trim()
    if (!text) return          // 🛡️ 空输入直接拦截,不给脏数据
    onAdd(text)
    setValue('')
  }

  return (
    <form onSubmit={handleSubmit} className="flex w-full gap-2">
      <input
        type="text"
        value={value}
        onChange={(e) => setValue(e.target.value)}
        placeholder="输入待办事项..."
        className="flex-1 rounded-lg border border-gray-300 px-3 py-2 text-gray-800 outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-200"
      />
      <button
        type="submit"
        className="rounded-lg bg-blue-500 px-4 py-2 font-medium text-white transition hover:bg-blue-600"
      >
        添加
      </button>
    </form>
  )
}

📄 TodoItem.jsx ------ 待办条目组件(含拖拽手柄)

jsx 复制代码
import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'

export default function TodoItem({ task, onToggle, onDelete }) {
  const {
    attributes,
    listeners,
    setNodeRef,
    transform,
    transition,
    isDragging,
  } = useSortable({ id: task.id })   // 🔗 胶水:一行接入拖拽能力

  const style = {
    transform: CSS.Transform.toString(transform),
    transition,
  }

  return (
    <li
      ref={setNodeRef}
      style={style}
      className={`flex items-center gap-3 rounded-lg border border-gray-200 bg-white px-3 py-2 shadow-sm ${
        isDragging ? 'opacity-60 shadow-lg' : ''
      }`}
    >
      <button
        type="button"
        {...attributes}
        {...listeners}
        aria-label="拖动排序"
        className="cursor-grab touch-none text-gray-300 transition hover:text-gray-500"
      >
        ⋮⋮
      </button>
      <input
        type="checkbox"
        checked={task.completed}
        onChange={() => onToggle(task.id)}
        className="h-4 w-4 accent-blue-500"
      />
      <span
        className={`flex-1 text-gray-800 ${
          task.completed ? 'text-gray-400 line-through' : ''
        }`}
      >
        {task.text}
      </span>
      <button
        type="button"
        onClick={() => onDelete(task.id)}
        className="text-sm text-red-400 transition hover:text-red-600"
      >
        删除
      </button>
    </li>
  )
}

📋 TodoList.jsx ------ 列表容器组件(拖拽上下文)

jsx 复制代码
import {
  DndContext,
  PointerSensor,
  closestCenter,
  useSensor,
  useSensors,
} from '@dnd-kit/core'
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'
import TodoItem from './TodoItem'

export default function TodoList({ tasks, onToggle, onDelete, onReorder }) {
  const sensors = useSensors(
    useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
  )

  const handleDragEnd = (event) => {
    const { active, over } = event
    if (over && active.id !== over.id) {
      onReorder(active.id, over.id)   // 🔗 胶水:只把结果回传给 App,不碰底层
    }
  }

  if (tasks.length === 0) {
    return <p className="mt-8 text-center text-gray-400">暂无待办,添加一条吧</p>
  }

  return (
    <DndContext
      sensors={sensors}
      collisionDetection={closestCenter}
      onDragEnd={handleDragEnd}
    >
      <SortableContext
        items={tasks.map((t) => t.id)}
        strategy={verticalListSortingStrategy}
      >
        <ul className="flex w-full flex-col gap-2">
          {tasks.map((task) => (
            <TodoItem
              key={task.id}
              task={task}
              onToggle={onToggle}
              onDelete={onDelete}
            />
          ))}
        </ul>
      </SortableContext>
    </DndContext>
  )
}

🚀 启动与依赖

bash 复制代码
# 安装依赖
pnpm i

# 开发模式启动
pnpm dev

# 构建
pnpm build

依赖清单(全部是成熟方案,没有一个轮子是自己造的):

json 复制代码
{
  "dependencies": {
    "@dnd-kit/core": "^6.3.1",        // 🖱️ 拖拽核心
    "@dnd-kit/sortable": "^10.0.0",    // 🔀 拖拽排序
    "@dnd-kit/utilities": "^3.2.2",    // 🧰 工具函数
    "react": "^19.0.0",
    "react-dom": "^19.0.0"
  },
  "devDependencies": {
    "@tailwindcss/vite": "^4.0.0",     // 🎨 样式
    "@vitejs/plugin-react": "^4.3.4",
    "tailwindcss": "^4.0.0",
    "vite": "^6.0.0"
  }
}

🧱 组件职责一览

组件 职责 依赖的「胶水」
🧠 App.jsx 状态中枢,持有 tasks 数组 useStatearrayMove
⌨️ TodoInput 输入 + 新增,自带空值拦截 ---
📄 TodoItem 单条展示 + 勾选/删除 + 拖拽手柄 useSortable
📋 TodoList 列表容器 + 拖拽上下文 DndContextSortableContext

数据流单向清晰:子组件从不自己改数据 ,只通过 props 接收状态、通过回调把意图「上报」给 App。这就是「胶水」的哲学------零件各司其职,胶水负责联通。


🎯 总结:三步法回顾

步骤 关键词 一句话
📋 第一步 规划就是一切 先规划后编码,划定边界、拆模块、定结构
🧩 第二步 胶水编程 能抄不写、能连不造,绝不从零造轮子
🧠 第三步 元方法论 让 AI 打分、进化提示词,越用越懂你

✨ 记住这三句话

  1. 先规划,再编码 ------ 规划是给你的代码「上保险」。
  2. 胶水不生产零件,只联通零件 ------ 现成的轮子,拼就完了。
  3. 把 AI 当同事,不当工具 ------ 教它懂你,比命令它更快。

📌 本文基于 readme.mdtodolist 项目实战生成。所有代码均可直接运行,动手指令:pnpm i && pnpm dev

相关推荐
狂师1 小时前
AI 测试 | 把 UI 自动化测试执行固化成五步流程,这套AI Skill 思路可以直接抄
人工智能·agent·测试
调试到凌晨1 小时前
2026年配音工具避坑实测:免费额度、长文本稳定性与API集成能力全记录
人工智能·经验分享·实时音视频
cxr8281 小时前
deepseek harness能否指挥Claude code和codex来协同开发
人工智能·智能体
数据智研1 小时前
【数据分享】中国农产品价格调查年鉴(2004-2025)
大数据·人工智能·数据分析·可视化·数据可视化
阿图灵1 小时前
OpenCV 图像操作六件套:读取、裁剪、缩放、旋转、通道分割与保存
图像处理·人工智能·python·深度学习·opencv·计算机视觉
小小帅呀1 小时前
学习 VLA 第 1 天:VLA 概述与学习路线
人工智能
TechEdu2026061 小时前
[人工智能]Claude(Anthropic):模型能力、智能体与安全工程实践
人工智能·ai
pjj198541 小时前
opencv-图像透视转换
人工智能·opencv·计算机视觉
aneasystone本尊1 小时前
学习大模型推理的分词:从文本到 Token
人工智能