上周帮一个做独立项目的朋友看代码。他用 Claude Code + Cursor,三天做了一个完整的 React 管理后台——能登录,能 CRUD,能导出 Excel,页面 UI 还不错。
他说:"你帮我看看,准备上线了。"
我打开项目,npm start 跑起来,页面确实能用。然后我打开代码——看了十分钟,告诉他:"能跑,但别上线。至少改完这 5 个地方再说。"
这不是个例。Vibe Coding 现在最大的问题不是"写不出来",而是写出来的东西看起来很好,实际上全是定时炸弹。FT 最近有篇报道标题就是"Who cleans up after the vibe-coding party?"。Reddit 上也有人说:"The first 80% of vibe coding feels fast. The last 20% has broken me."
以下是我在这份代码里发现的 5 个典型问题。如果你也在用 AI 写代码,对照检查一下。
打开项目的状态管理,我看到一个巨大的AppContext:
// ❌ AI最爱干的事:把所有状态塞进一个Context
const AppContext = createContext(null);
function AppProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const [sidebarOpen, setSidebarOpen] = useState(true);
const [notifications, setNotifications] = useState([]);
const [tableData, setTableData] = useState([]);
const [filters, setFilters] = useState({});
const [selectedRows, setSelectedRows] = useState([]);
const [modalVisible, setModalVisible] = useState(false);
const [formData, setFormData] = useState({});
const [loading, setLoading] = useState(false);
// ...还有20多个
return (
<AppContext.Provider value={{
user, setUser, theme, setTheme,
sidebarOpen, setSidebarOpen,
// ...全部暴露出去
}}>
{children}
</AppContext.Provider>
);
}
40 多个 state,全塞在一个 Provider 里。结果就是:任何一个 state 变化,整棵组件树都重新渲染。
你在表格里勾选一行,selectedRows变了——侧边栏、导航栏、通知弹窗全部重新渲染。
AI 为什么这样写?因为你告诉它"加一个 XX 功能",它就在已有的 Context 里加一个 state。它不会主动说"这个 state 应该放到单独的 Context 里"。
// ✅ 按职责拆分Context
const AuthContext = createContext(null);
const UIContext = createContext(null);
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
return (
<AuthContext.Provider value={{ user, setUser }}>
{children}
</AuthContext.Provider>
);
}
function UIProvider({ children }) {
const [theme, setTheme] = useState('light');
const [sidebarOpen, setSidebarOpen] = useState(true);
return (
<UIContext.Provider value={{ theme, setTheme, sidebarOpen, setSidebarOpen }}>
{children}
</UIContext.Provider>
);
}
// 表格页面的状态留在表格组件里,根本不需要Context
function DataTable() {
const [selectedRows, setSelectedRows] = useState([]);
const [filters, setFilters] = useState({});
// ...
}
判断标准:这个 state 是不是只有一个页面/组件用? 只有一个地方用的 state,别往 Context 里放。
翻了一下数据请求的代码:
// ❌ AI写的典型请求代码:只管发,不管防
function UserList() {
const [users, setUsers] = useState([]);
useEffect(() => {
fetch('/api/users')
.then(res => res.json())
.then(data => setUsers(data));
}, []);
const handleDelete = async (id) => {
await fetch(`/api/users/${id}`, { method: 'DELETE' });
// 删完重新拉列表
const res = await fetch('/api/users');
const data = await res.json();
setUsers(data);
};
return (
<ul>
{users.map(u => (
<li key={u.id}>
{u.name}
<button onClick={() => handleDelete(u.id)}>删除</button>
</li>
))}
</ul>
);
}
问题清单:
// ✅ 生产级别的请求应该这样
function UserList() {
const queryClient = useQueryClient();
const { data: users, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(r => r.json()),
});
const deleteMutation = useMutation({
mutationFn: (id) => fetch(`/api/users/${id}`, { method: 'DELETE' }),
onMutate: async (id) => {
await queryClient.cancelQueries({ queryKey: ['users'] });
const prev = queryClient.getQueryData(['users']);
queryClient.setQueryData(['users'], old =>
old.filter(u => u.id !== id)
);
return { prev };
},
onError: (err, id, context) => {
queryClient.setQueryData(['users'], context.prev);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
if (isLoading) return <Skeleton />;
if (error) return <ErrorFallback error={error} />;
return (
<ul>
{users.map(u => (
<li key={u.id}>
{u.name}
<button
disabled={deleteMutation.isPending}
onClick={() => deleteMutation.mutate(u.id)}
>
删除
</button>
</li>
))}
</ul>
);
}
不是说每个请求都要写这么多。而是 AI 根本不会主动帮你考虑这些边界情况。它只实现了你描述的"正常流程",所有异常路径都不存在。
这是让我最紧张的一个:
// ❌ 真的在代码里看到了这个
const supabase = createClient(
'https://xxxxx.supabase.co',
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.xxxxx...'
);
// 另一个文件里
const STRIPE_KEY = 'sk_live_xxxxxxxxxxxxx';
Supabase 的 anon key 直接写在代码里。Stripe 的私钥直接写在前端代码里。
我问他:"这个 key 是从哪来的?"
他说:"我在 prompt 里告诉 Claude 我的 key,它就帮我配好了。"
AI 不会主动告诉你"这个 key 不能放在前端代码里"。它只管让代码跑起来。如果你在 prompt 里给了 key,它就原样写进去。
// ✅ 环境变量必须走.env
// .env.local(不提交到git)
// NEXT_PUBLIC_SUPABASE_URL=https://xxxxx.supabase.co
// NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGci...
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
);
// Stripe私钥绝不能出现在前端
// 必须放在服务端API route里
// pages/api/checkout.js
// const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
检查清单:
| 该检查什么 | 怎么查 |
|---|---|
| 代码里有没有硬编码的 key | 全局搜索 sk_、eyJ、key、secret、password
|
| .gitignore 有没有忽略.env | 打开.gitignore 看有没有 .env*
|
| 前端代码有没有后端密钥 | 前端代码里的 key 只能是 NEXT_PUBLIC_ 或 VITE_ 前缀的 |
| git 历史里有没有泄露过 | git log --all -p -- '*.env' |
看到一个"管理员才能看到的页面",打开路由:
// ❌ AI写的"权限控制"
function AdminRoute({ children }) {
const { user } = useAuth();
if (user?.role !== 'admin') {
return <Navigate to="/dashboard" />;
}
return children;
}
// 路由配置
<Route path="/admin/users" element={
<AdminRoute>
<UserManagement />
</AdminRoute>
} />
看起来没问题对吧?管理员才能进管理页面。
但是——打开UserManagement组件里的 API 调用:
// 这个接口任何人都能调
const handleDeleteUser = async (userId) => {
await fetch(`/api/admin/users/${userId}`, {
method: 'DELETE',
});
};
前端藏了按钮,但接口没有权限验证。任何人打开浏览器 DevTools,直接调/api/admin/users/123就能删用户。
这就是 OWASP Top 10 里的"Broken Access Control"——连续多年排第一的 Web 安全漏洞。
// ✅ 权限必须在后端验证(Next.js API Route示例)
export default async function handler(req, res) {
const session = await getServerSession(req, res, authOptions);
if (!session || session.user.role !== 'admin') {
return res.status(403).json({ error: 'Forbidden' });
}
if (req.method === 'DELETE') {
const { id } = req.query;
await db.user.delete({ where: { id } });
return res.status(200).json({ success: true });
}
res.status(405).end();
}
铁律:前端的权限检查是 UX 优化(不给用户看到没权限的按钮),后端的权限检查才是安全防线。两个都要有,但后端那个不能省。
这是 AI 写代码最显眼的特征——所有逻辑都塞在一个组件里:
// ❌ AI的经典作品:一个500行的"完整"组件
function OrderManagement() {
// 20个useState
const [orders, setOrders] = useState([]);
const [filters, setFilters] = useState({});
const [selectedOrder, setSelectedOrder] = useState(null);
const [isEditing, setIsEditing] = useState(false);
// ...
// 10个handler函数
const handleSearch = () => { /* 30行 */ };
const handleFilter = () => { /* 25行 */ };
const handleEdit = () => { /* 40行 */ };
const handleDelete = () => { /* 20行 */ };
const handleExport = () => { /* 50行 */ };
// ...
// 200行JSX
return (
<div>
{/* 搜索栏 */}
<div>{/* 50行搜索表单 */}</div>
{/* 筛选器 */}
<div>{/* 40行筛选条件 */}</div>
{/* 表格 */}
<table>{/* 80行表格渲染 */}</table>
{/* 编辑弹窗 */}
{isEditing && <div>{/* 60行编辑表单 */}</div>}
{/* 分页 */}
<div>{/* 30行分页器 */}</div>
</div>
);
}
为什么 AI 喜欢写成这样?因为你说"做一个订单管理页面",它就在一个文件里把所有东西都实现了。它的目标是"让你的需求跑起来",不是"让代码可维护"。
当你要改一个筛选器的 bug 时,你要在 500 行代码里找到那 20 行。 当你要给表格加一列时,你要理解这 500 行的所有状态依赖关系。
// ✅ 按职责拆分
// components/OrderFilters.jsx
function OrderFilters({ value, onChange }) {
return (/* 筛选器UI,只管筛选 */);
}
// components/OrderTable.jsx
function OrderTable({ data, onEdit, onDelete }) {
return (/* 表格UI,只管展示 */);
}
// components/OrderEditModal.jsx
function OrderEditModal({ order, onSave, onClose }) {
return (/* 编辑弹窗,只管编辑 */);
}
// hooks/useOrders.js
function useOrders(filters) {
return useQuery({
queryKey: ['orders', filters],
queryFn: () => fetchOrders(filters),
});
}
// pages/OrderManagement.jsx — 组装层,不超过50行
function OrderManagement() {
const [filters, setFilters] = useState({});
const [editingOrder, setEditingOrder] = useState(null);
const { data: orders, isLoading } = useOrders(filters);
return (
<div>
<OrderFilters value={filters} onChange={setFilters} />
<OrderTable
data={orders}
onEdit={setEditingOrder}
onDelete={handleDelete}
/>
{editingOrder && (
<OrderEditModal
order={editingOrder}
onSave={handleSave}
onClose={() => setEditingOrder(null)}
/>
)}
</div>
);
}
| 检查项 | 怎么查 | AI 代码常见问题 |
|---|---|---|
| 全局状态 | 搜 Context/Provider | 所有 state 塞一个 Context |
| 请求处理 | 搜 fetch/axios | 没有 loading/error/竞态处理 |
| 密钥泄露 | 搜 sk_/eyJ/secret/password | 硬编码在前端代码里 |
| 权限控制 | 看 API 路由有没有 auth 检查 | 前端藏按钮但接口裸奔 |
| 组件大小 | 看文件行数 | 单文件 500 行 +,逻辑全混在一起 |
我不反对 Vibe Coding。三天能跑起来一个完整的管理后台,这在两年前不可想象。
但AI 生成的代码和人写的代码需要同一套审查标准。你不会让一个实习生提交的代码不经过 review 就直接上线,AI 写的代码也不应该。
区别在于:实习生的代码你一看就知道哪里不对,AI 写的代码看起来很专业——命名规范,结构清晰,注释齐全。但那 5 个问题就藏在这些"看起来很对"的代码里。
如果你正在 Vibe Coding 一个准备上线的项目,至少跑一遍上面的速查表。
你 review 过 AI 写的代码吗?发现过什么让你冒冷汗的问题?