前端 Code Review 实战:识别 AI 写代码的 15 个常见坑

去年下半年我们做了 6 个月的 AI 编程试点,期间 Review 了 200 多个 PR。我整理出一组反直觉的数据:AI 代码 PR 的首次 Review 通过率比人工代码高 23% ,但 merge 后缺陷率只低 4%。

换句话说,AI 代码看起来"更对",但实际并没有更安全。

更扎心的是,有一次我自己 Review 了一个 AI 生成的 PR,里面有个 v-html 直接渲染用户评论。代码风格无可挑剔,三秒通过。merge 第二天,生产环境被注入了一段挖矿脚本。

从那次之后,我把所有 AI 生成的 PR 都当成"陌生人的代码"重新看。这篇文章整理了过去 6 个月里我反复遇到的 15 个真实陷阱,给所有 Reviewer 一份可立即上手的速查手册。


AI 代码最大的问题,是"看起来太对"

AI 生成的代码有一个特征:语法、风格、可读性往往都很"标准"。这恰恰让它危险------一个写得"漂亮"的危险代码,比一个写得"粗糙"的安全代码更难被发现。

我们调研了 6 支前端团队,列出了 AI 时代 Review 工作的 Top 5 痛点:

痛点 占比
Reviewer 看不懂 AI 为什么这么写 78%
AI 代码"看起来太对",不好意思挑刺 65%
找不出具体问题,但又觉得哪里不对 58%
AI 引入的 Bug 排查时间比人工代码长 2 倍 47%
团队成员开始依赖 AI Review,缺乏独立判断 41%

如果你的团队已经引入或正在考虑引入 AI 编程工具,这 5 个痛点大概率都会遇到。

在展开 15 个具体陷阱之前,先建立 5 个"心智模型"------AI 代码几乎所有问题,都可以归到这 5 个过度行为之一。

1. 过度防御 :到处是 if (!x) return null、try/catch、?? defaultValue。AI 见过太多防御性代码,机械模仿,但分不清"必要"和"过度"。

2. 过度抽象:抽出只调用一次的 util、提前组件化、引入设计模式。AI 倾向"做通用解",但缺乏对"现在是否需要"的判断。

3. 过度优化 :到处 computed、提前加缓存、加防抖节流。AI 知道"性能优化"是个好东西,但不知道"何时不该优化"。

4. 过度类型化 :类型体操、复杂泛型、过度 as、滥用 any。AI 训练语料里有大量 TypeScript "展示型"代码。

5. 过度设计:引入根本不需要的状态管理库、状态机、依赖注入。AI 倾向给出"工业级"解,但你的项目可能只是个内部工具。

记住这 5 个模式,胜过逐条对照 Checklist。


15 个陷阱(按严重程度排序)

🔴 P0 | 必须修复(涉及安全、数据、稳定性)


陷阱 1:XSS------直接渲染未转义内容

vue 复制代码
<!-- AI 写的:处理"富文本"展示 -->
<script setup lang="ts">
defineProps<{ content: string }>()
</script>

<template>
  <div v-html="content"></div>
</template>

用户输入、API 返回、Markdown 渲染结果都可能包含 <script> 或事件属性。AI 不知道你的内容来源是否可信------它只关心"完成渲染"。

修复:用 DOMPurify 处理。

vue 复制代码
<script setup lang="ts">
import DOMPurify from 'dompurify'
import { computed } from 'vue'

const props = defineProps<{ content: string }>()
const sanitized = computed(() => DOMPurify.sanitize(props.content))
</script>

<template>
  <div v-html="sanitized"></div>
</template>

识别信号 :搜索 v-html / bypassSecurityTrustHtml,看到默认假设不可信。


陷阱 2:错误处理缺失------fetch / JSON.parse 必崩

vue 复制代码
<script setup lang="ts">
import { ref, onMounted } from 'vue'

const props = defineProps<{ userId: string }>()
const user = ref(null)

onMounted(() => {
  fetch(`/api/users/${props.userId}`)
    .then(r => r.json())  // 非 JSON 会 throw
    .then(data => user.value = data)  // null 时崩溃
})
</script>

<template>
  <div>{{ user.name }}</div>
</template>

AI 默认假设"一切顺利"。但 fetch 失败、JSON 解析失败、user 为 null,任何一个发生都会让页面变白屏。

修复:完整三态 + 错误处理。

vue 复制代码
<script setup lang="ts">
import { ref, onMounted } from 'vue'

interface User { id: string; name: string }

const props = defineProps<{ userId: string }>()
const user = ref<User | null>(null)
const loading = ref(true)
const error = ref<Error | null>(null)

onMounted(() => {
  loading.value = true
  fetch(`/api/users/${props.userId}`)
    .then(r => r.json())
    .then(data => user.value = data)
    .catch(e => error.value = e)
    .finally(() => loading.value = false)
})
</script>

<template>
  <Skeleton v-if="loading" />
  <ErrorMessage v-else-if="error" :error="error" />
  <Empty v-else-if="!user" />
  <div v-else>{{ user.name }}</div>
</template>

外层用 app.config.errorHandler 做兜底。

ts 复制代码
// main.ts
app.config.errorHandler = (err) => {
  // 上报到监控 + 显示错误页
}

识别信号 :.then() 后没有 .catch()、JSON.parse 无 try/catch、模板里直接访问对象属性无空值保护。


陷阱 3:硬编码密钥 / 凭证

ts 复制代码
// AI 写的"快速对接"
const API_KEY = 'sk-xxxxxxxxxxxxxxxx'
const API_BASE = 'https://internal-api.company.com'

AI 不知道你的密钥策略。它可能直接复制训练数据里的"示例值"------但更多的情况是它会"贴心"地帮你写完整密钥路径,让你更倾向于直接使用。

修复:构建时注入或后端代理。

ts 复制代码
// Vite 项目:使用 import.meta.env
const API_KEY = import.meta.env.VITE_API_KEY
const API_BASE = import.meta.env.VITE_API_BASE

// 或:完全走代理,前端不接触密钥
const API_BASE = '/api/proxy'

识别信号 :字符串以 sk- / pk- / Bearer 开头;看到内网域名但不在环境变量里。


陷阱 4:localStorage 滥用存敏感数据

ts 复制代码
// AI 写的"登录态持久化"
function login(token: string) {
  localStorage.setItem('token', token)
  localStorage.setItem('user', JSON.stringify(userInfo))
}

token 长期存 localStorage 容易被 XSS 窃取。AI 不懂"token 应该走 httpOnly cookie"这条 web 安全基本规则。

修复:token 由后端 set-cookie 控制,前端只存非敏感信息。

ts 复制代码
function login(userInfo: UserInfo) {
  localStorage.setItem('theme', userInfo.preferences.theme)
}

识别信号 :localStorage.setItem('token';存身份证 / 银行卡 / 手机号。


陷阱 5:竞态条件------搜索结果错乱

vue 复制代码
<script setup lang="ts">
import { ref } from 'vue'

const query = ref('')
const results = ref<any[]>([])

const handleSearch = async (q: string) => {
  const data = await api.search(q)
  results.value = data  // 输入 "abc",但 "a" 的请求后返回 → 覆盖
}
</script>

<template>
  <input v-model="query" @input="handleSearch(query)" />
</template>

用户连续输入 "a" → "ab" → "abc","a" 的请求最后返回,覆盖了正确结果。AI 不会主动考虑这种"用户实际操作可能比代码异步逻辑快"的场景。

修复:watch + AbortController。

vue 复制代码
<script setup lang="ts">
import { ref, watch } from 'vue'

const query = ref('')
const results = ref<any[]>([])

watch(query, async (newQuery, _, onCleanup) => {
  const controller = new AbortController()
  onCleanup(() => controller.abort())
  const data = await api.search(newQuery, { signal: controller.signal })
  results.value = data
})
</script>

<template>
  <input v-model="query" />
</template>

识别信号:搜索框 / 自动补全组件;多个 await 后紧跟赋值;无 abort / cleanup 逻辑。


🟠 P1 | 应当修复(架构、性能、可维护性)


陷阱 6:watch / watchEffect 滥用

vue 复制代码
<script setup lang="ts">
import { ref, watch } from 'vue'
import { useRouter } from 'vue-router'

const router = useRouter()
const productId = ref('')

// 错误:把"点击跳转"放进 watch
watch(productId, (id) => {
  if (id) router.push(`/product/${id}`)
})
</script>

<template>
  <button @click="productId = '123'">查看</button>
</template>

watch 是"同步外部系统到状态",不是"事件处理"。事件应该直接在 @click 里完成。

修复:

vue 复制代码
<script setup lang="ts">
import { useRouter } from 'vue-router'

const router = useRouter()
</script>

<template>
  <button @click="router.push('/product/123')">查看</button>
</template>

识别信号 :watch 里只有 if (cond) doSomething();依赖里有 router / route。


陷阱 7:computed 滥用 / 过度响应式化

vue 复制代码
<script setup lang="ts">
import { computed } from 'vue'

const props = defineProps<{ product: { id: string; price: number } }>()

// 错误:基础计算根本不需要 computed
const formattedPrice = computed(() => `¥${props.product.price.toFixed(2)}`)

// 内联函数:Vue 没有 useCallback 的需求
const handleClick = () => console.log(props.product.id)
</script>

Vue 3 的响应式系统天然有依赖追踪,没有 React useMemo/useCallback 那种"每次都重算"的开销。AI 倾向"以防万一"加 computed,但忽略了 computed 自身也有依赖追踪成本。

修复:

vue 复制代码
<script setup lang="ts">
const props = defineProps<{ product: { id: string; price: number } }>()

const formattedPrice = `¥${props.product.price.toFixed(2)}`
const handleClick = () => console.log(props.product.id)
</script>

判断原则:computed 只在以下场景使用:

  1. 该值会作为 props 传给子组件
  2. 该值会被 watch / watchEffect 依赖
  3. 模板中多处使用,且依赖计算昂贵

识别信号 :computed 里只有一行基础运算;数据根本不会变化但包了 ref/reactive。


陷阱 8:滥用 any

ts 复制代码
async function loadUser(id: string) {
  const res = await fetch(`/api/users/${id}`)
  const user = (await res.json()) as any
  return user
}

any 让 TypeScript 失去价值。user.name 拼错不会报错,等同于写 JS。AI 遇到"类型对不上"时最常见的处理方式就是 as any。

修复:定义具体类型 + 错误检查。

ts 复制代码
interface User {
  id: string
  name: string
  email: string
}

async function loadUser(id: string): Promise<User> {
  const res = await fetch(`/api/users/${id}`)
  if (!res.ok) throw new Error('Failed to load user')
  return res.json() as Promise<User>
}

识别信号 :单文件 any > 5 次;as any / as unknown as;// @ts-ignore 无说明。


陷阱 9:列表 key 用 index

vue 复制代码
<!-- AI 写的:用 index 作 key -->
<script setup lang="ts">
import type { Todo } from './types'
defineProps<{ todos: Todo[] }>()
</script>

<template>
  <ul>
    <TodoItem
      v-for="(todo, index) in todos"
      :key="index"
      :todo="todo"
    />
  </ul>
</template>

删除中间项时,index 不变但内容变了,导致输入框光标错位、动画错乱、内部状态错位。

修复:

vue 复制代码
<template>
  <ul>
    <TodoItem
      v-for="todo in todos"
      :key="todo.id"
      :todo="todo"
    />
  </ul>
</template>

识别信号 :搜索 :key="index" / :key="i" / :key="idx";列表项有内部状态(输入框、勾选状态)。


陷阱 10:不处理 loading / error / empty 三态

vue 复制代码
<!-- AI 写的 -->
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import type { User } from './types'

const users = ref<User[]>([])

onMounted(() => {
  api.getUsers().then(data => users.value = data)
})
</script>

<template>
  <ul>
    <li v-for="u in users" :key="u.id">{{ u.name }}</li>
  </ul>
</template>

用户请求未完成时看到空白;失败时无提示;空数据时无反馈。AI 给的代码经常只处理"成功"这一种状态。

修复:

vue 复制代码
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import type { User } from './types'

const users = ref<User[]>([])
const loading = ref(true)
const error = ref<Error | null>(null)
const isEmpty = computed(() => !loading.value && !error.value && users.value.length === 0)

onMounted(() => {
  loading.value = true
  api.getUsers()
    .then(data => users.value = data)
    .catch(e => error.value = e)
    .finally(() => loading.value = false)
})
</script>

<template>
  <Skeleton v-if="loading" />
  <ErrorMessage v-else-if="error" :error="error" />
  <Empty v-else-if="isEmpty" />
  <ul v-else>
    <li v-for="u in users" :key="u.id">{{ u.name }}</li>
  </ul>
</template>

识别信号:组件调用了 async API 但无 loading 状态;空数据时显示空白页面。


陷阱 11:引入不必要的依赖

ts 复制代码
import _ from 'lodash'
const cloned = _.cloneDeep(obj)

import moment from 'moment'
const formatted = moment(date).format('YYYY-MM-DD')

AI 倾向给出"教科书式"答案。lodash、moment、jquery 这些"老牌"库在它的训练数据里出现频率极高,因此 AI 容易直接引入。但现代 JS / TS 原生 API 已经覆盖了 90% 场景。

修复:

ts 复制代码
// 深拷贝
const cloned = structuredClone(obj)

// 日期格式化
const formatted = new Intl.DateTimeFormat('zh-CN', {
  year: 'numeric', month: '2-digit', day: '2-digit'
}).format(date)

识别信号 :PR 中新增 lodash / moment / jquery;import * as _ 但只用了 1 个函数。


陷阱 12:静默吞错

ts 复制代码
async function saveData(data: any) {
  try {
    await api.save(data)
  } catch (e) {
    console.log('error', e)  // 仅打印
  }
}

用户点击保存,以为成功了,实际没存。AI 给的 catch 块经常是空的或者只有 console.log。

修复:

ts 复制代码
async function saveData(data: Data) {
  try {
    await api.save(data)
    showToast('保存成功')
  } catch (e) {
    showToast(`保存失败:${e.message}`, 'error')
  }
}

识别信号 :catch (e) {} 空块;用户关键操作(保存、提交、支付)相关代码无错误反馈。


🟢 P2 | 建议修复(代码质量)


陷阱 13:过度抽象------抽出只调用一次的 util

ts 复制代码
// 只有 ProductCard 调用了它
function formatDate(date: Date): string {
  return new Intl.DateTimeFormat('zh-CN').format(date)
}
vue 复制代码
<!-- ProductCard.vue -->
<script setup lang="ts">
import type { Product } from './types'

const props = defineProps<{ product: Product }>()
const dateText = formatDate(props.product.createdAt)
</script>

<template>
  <div>{{ dateText }}</div>
</template>

抽象的成本(理解、跳转、修改)大于收益。

修复:内联即可。

vue 复制代码
<!-- ProductCard.vue -->
<script setup lang="ts">
import type { Product } from './types'

const props = defineProps<{ product: Product }>()
const dateText = new Intl.DateTimeFormat('zh-CN').format(props.product.createdAt)
</script>

<template>
  <div>{{ dateText }}</div>
</template>

经验法则:同一逻辑出现 3 次以上再抽象。


陷阱 14:无意义的注释

ts 复制代码
function getUserName(user: User) {
  // 获取用户名字
  return user.name
}

注释应为"为什么"而非"是什么"。AI 倾向生成"做了什么"的注释,因为它见过的训练数据里这种注释占比很高。

修复:

ts 复制代码
function getUserName(user: User) {
  // 后端返回的 name 字段可能为空字符串(已注销用户)
  return user.name || '匿名用户'
}

陷阱 15:命名不一致

ts 复制代码
interface UserData {
  user_id: string
  userName: string
  uid: number
}

同一个概念三种命名。AI 不会主动遵守你的命名规范。

修复:与团队规范保持一致。

ts 复制代码
interface User {
  id: string
  name: string
}

识别信号 :搜索 user_id|userId|uid,同一 PR 出现 2 种以上说明 AI 没注意规范。


一份精简的 Review Checklist

把上面 15 个陷阱压缩成 11 个必查项,贴在工位或加到 PR Template 里。

🔴 P0(任意 1 项不过 = 拒绝 merge)

  • 无 v-html / bypassSecurityTrustHtml,或已用 DOMPurify
  • async 操作有 .catch() 或 try/catch
  • 渲染前有空值保护
  • 无硬编码密钥 / 内部 URL
  • 搜索 / 自动补全有 AbortController
  • 用户关键流程有 loading + error 反馈

🟠 P1(任意 2 项不过 = 拒绝 merge)

  • watch / watchEffect 数量合理,无滥用
  • 无 as any / // @ts-ignore
  • 列表 :key 使用稳定 ID,不用 index
  • loading / error / empty 三态完整
  • 无新增 lodash / moment 等大依赖

团队怎么落地

三件小事,比写十篇规范更有效。

1. PR Template 加一段 AI 声明

markdown 复制代码
## AI 代码声明
- [ ] 本 PR 包含 AI 生成的代码
- [ ] P0 检查项全部通过
- [ ] P1 检查项至少 80% 通过

2. 每月一次"AI 代码找 Bug 挑战赛"

从近期 AI 生成的 PR 里选 3-5 个,让团队成员盲测找出问题。亲测发现,这种方式比培训更有效------大家会主动建立"质疑 AI"的本能。

3. ESLint 加几条规则拦截明显问题

js 复制代码
// eslint.config.js
export default [
  {
    rules: {
      'vue/no-v-html': 'error',  // v-html 必须人工 review
      '@typescript-eslint/no-explicit-any': 'warn',  // 警告 any
    }
  }
]

工具只能发现约 30% 的 AI 代码问题,人工 Review 仍是核心。


几条带走的工程心法

  1. AI 写代码易如反掌,Review 才是真功夫。AI 把"写"的成本降到接近 0,Review 的重要性反而提升。

  2. AI 代码"看起来太对"是最大的危险。Review 时保持质疑感,不要被风格迷惑。

  3. 错误处理是 AI 代码的"重灾区"。过去 6 个月我们遇到的 AI 代码 Bug 里,70% 以上是错误处理缺陷。优先 Review 这一块。

  4. 安全代码永远不直接 merge。涉及 XSS、密钥、权限、资金的代码,AI 输出仅供参考。

  5. 把 AI 当作一个"非常熟练但判断力不稳定的初级工程师"------它的产出值得 review,但不值得盲信。


你团队里 Review AI 代码最大的阻力是什么?欢迎评论区聊聊。

如果本文对你有帮助,欢迎转发给你们的 Reviewer 同事------AI 时代的代码质量,靠的是团队一起把关。

相关推荐
拖孩2 小时前
全程 AI 写的小程序,刚接上会员虚拟支付,上线第二天就有人真金白银买单了|双签名、定价、iOS 的坑我全踩了一遍
前端·后端·微信小程序
小蚂蚁i2 小时前
React 基础
前端
ZoeLandia2 小时前
TypeScript 02:interface、type 与类型收窄
前端·typescript
Bode_20022 小时前
构建制造业知识库
前端·智能制造系统·业务流程再造
cindershade2 小时前
接口协作的交接面:让并行开发从约定走到发布
前端
JAVA老架构AI智能化2 小时前
多图叠加比对
前端·javascript·chrome
ShineWinsu2 小时前
对于HTML:HTML常见标签的解析—上
前端·面试·html·浏览器·标签·图片·超链接
行者全栈架构师2 小时前
【鸿蒙心迹】从手机到平板、折叠屏——多端适配自查清单与踩坑实录(HarmonyOS 7.x)
前端·算法·架构
百度一下吧3 小时前
HTML5 新增标签与 Web Components 使用指南
前端·html·html5