一聚教程网:一个值得你收藏的教程网站

最新下载

热门教程

AI 编码提速却让组件失控?用工程闭环阻止重复造轮子

时间:2026-09-17 11:12:01 编辑:袖梨 来源:一聚教程网

当 AI 开始批量生成页面和组件,交付速度提升的同时,项目里也可能悄悄出现大量职责相同、命名不同的实现。仅靠提示词或代码评审很难持续拦住这类重复。要真正控制组件膨胀,需要把复用要求嵌入工程流程,让 AI 在动手前看见库存、编码时查询真实接口,并在提交前接受自动校验。

前言

Hello~大家好,我是秋天的一阵风

AI 现在已经是前端开发的标配。让它写个 Vue 组件、TSX 页面,几秒钟就能出代码,迭代快得肉眼可见。

但快是有代价的。AI 只负责"快速产出",不关心你项目里已经有什么——设计系统、组件规范、存量组件,它一概不知。

最常见的画面:你让它写个提交按钮,它现编一个;隔壁同事让它写"保存并提交",又现编一个。

两个组件长得几乎一样,只是文件名和入参不同。等代码进了 PR,评审才发现:又重复了。

一次两次还好,日积月累,业务目录里堆满同质化组件。等哪天要全局换主题色、适配无障碍、统一交互逻辑,就得人肉全网排查,漏改错改是家常便饭,维护成本直接失控。

一、AI为什么总爱"从零造轮子"

把 AI 想象成一个能力很强、但完全不了解你项目的临时工。

你让它写个基础提交按钮,它按网上最常见的写法随手写一个,根本不知道你们项目里早就有封装好的 SubmitAction,也不知道配色、圆角、间距都要用统一的 CSS 变量。

把这个问题捋一捋,真正的痛点其实就三个:

  1. AI 看不见存量:写代码前不会主动去读组件目录、导出清单,项目里有什么可复用的,它一无所知;
  2. AI 默认新写:没有存量参考时,它总是选"现场编一套"这条最省事的路,而不是先查查能不能复用;
  3. 事后补救太贵:代码已经落盘、进了 diff,再回头做复用优化,AI 生成的成本白花了,还得搭上人工清理、重构、对齐规范的时间。

所以治本的路子不是调提示词、训 AI"乖一点",而是改造工程工具链:写代码前让它知道项目有哪些组件,写的时候按项目真实 API 来,合入主干前过一遍复用校验。

二、关键词搜索根本防不住"换皮组件"

很多人第一反应是:用 IDE 全局搜索不就行了?Ctrl+Shift+F 搜个"Button",看看有没有现成的。

这个办法只能拦住"长得一模一样"的代码。真正麻烦的是那些换皮、异形、同源的隐性重复,关键词根本搜不出来。看几个典型场景:

漏检场景核心原因
命名体系分裂项目里新旧两套命名并存,老组件叫 FfBtn,新组件统一叫 ActionTrigger,关键词对不上,搜索完全落空
实现形态不同同一个功能的组件,一个用 Vue 单文件写,一个用 render 函数工厂写,文本差异巨大,关键词检索识别不了同源
DOM 结构不同、职责一致同样是可点击操作,一个用原生 button,一个用带按钮角色的 div,语义和结构都不同,但业务功能完全重合

所以靠谱的治理得两条腿走路:

  1. 确定性清单校验:把组件导出表、Storybook 文档、样式变量规范整理好,在 AI 写代码前主动喂进它的上下文;
  2. 智能近似匹配:用 AST 结构指纹、向量检索,在代码生成后和 CI 阶段精准揪出"换皮不换功能"的重复组件。

三、生成前:先让AI"看见"项目库存

这一步是整个方案的地基:强制 AI 在调用写文件、改文件工具之前,先加载项目的可复用组件清单

不管钩子叫 PreToolUse 还是 before_submit,核心都一样——前置拦截,把存量约束塞进去,从源头拦住盲目新建。

(一)自动扫描组件,生成库存清单

靠人工口头叮嘱"项目有哪些通用组件"完全不靠谱,也跟不上迭代。写个脚本自动扫组件目录,把名称、路径、props、用到的全局样式变量全解析出来,生成一份标准 JSON 库存,一劳永逸。

// scripts/inventory-components.ts
import fg from 'fast-glob'
import { parse } from '@vue/compiler-sfc'
import fs from 'node:fs/promises'
import path from 'node:path'

export type ComponentCard = {
  name: string
  file: string
  exportName: string
  props: string[]
  tokensUsed: string[]
}

// 匹配CSS变量
const TOKEN_RE = /var(--([a-z0-9-]+))/gi

// 构建项目组件库存清单
export async function buildInventory(root = 'packages'): Promise<ComponentCard[]> {
  // 遍历目标目录下的组件文件,过滤无效文件
  const files = await fg(['**/components/**/*.{vue,tsx,ts}'], {
    cwd: root,
    absolute: true,
    ignore: ['**/node_modules/**', '**/dist/**', '**/*.test.*'],
  })

  const cards: ComponentCard[] = []

  for (const file of files) {
    const source = await fs.readFile(file, 'utf8')
    const base = path.basename(file, path.extname(file))
    // 提取当前组件使用的所有全局样式变量
    const tokensUsed = [...source.matchAll(TOKEN_RE)].map((m) => `--${m[1]}`)

    // 解析Vue单文件组件
    if (file.endsWith('.vue')) {
      const { descriptor } = parse(source)
      // 提取组件props定义
      const props =
        descriptor.scriptSetup?.content.match(/defineProps<s*{([^}]+)}/s)?.[1]
          ?.split('n')
          .map((l) => l.trim().split(/[?:]/)[0])
          .filter(Boolean) ?? []

      cards.push({
        name: base,
        file: path.relative(process.cwd(), file),
        exportName: base,
        props,
        tokensUsed: [...new Set(tokensUsed)],
      })
      continue
    }

    // 解析TS/TSX组件导出
    const exports = [...source.matchAll(/exports+(?:function|const)s+([A-Z]w+)/g)].map(
      (m) => m[1],
    )

    for (const exportName of exports) {
      cards.push({
        name: exportName,
        file: path.relative(process.cwd(), file),
        exportName,
        props: [],
        tokensUsed: [...new Set(tokensUsed)],
      })
    }
  }

  return cards
}

// 执行脚本,生成库存JSON文件
if (import.meta.url === `file://${process.argv[1]}`) {
  const inventory = await buildInventory()
  await fs.writeFile('component-inventory.json', JSON.stringify(inventory, null, 2))
  console.log(`组件库存扫描完成,共${inventory.length}个可复用组件`)
}

生成出来的库存文件长这样,信息一次到位,直接喂给 AI:

[
  {
    "name": "SubmitAction",
    "file": "packages/ui/src/components/SubmitAction.vue",
    "exportName": "SubmitAction",
    "props": ["pending", "tone", "disabled"],
    "tokensUsed": ["--color-action", "--radius-control"]
  }
]

(二)写文件前注入复用约束

AI 每次执行写入、编辑、补丁操作前,自动在它的上下文里注入组件复用规范和库存清单,逼它优先用现成的,禁止顺手内联实现同类控件。

// agent-hooks/before-write.ts
import inventory from '../component-inventory.json'

export function beforeWriteTool(toolName: string, input: { prompt?: string }) {
  // 仅拦截文件写入、编辑、补丁操作
  if (!['Write', 'Edit', 'ApplyPatch'].includes(toolName)) return input

  // 截取核心组件清单注入上下文
  const digest = inventory
    .slice(0, 40)
    .map(
      (c) =>
        `- ${c.exportName} @ ${c.file} | props: ${c.props.join(', ') || '-'} | tokens: ${c.tokensUsed.join(', ') || '-'}`,
    )
    .join('n')

  // 落地强制约束规则
  const guard = `
【代码落盘强制约束】
1. 优先导入使用下列已有组件,禁止在业务页面内联实现同职责控件
2. 页面颜色、圆角、间距等样式,仅使用项目CSS变量,禁止手写固定色值、尺寸
3. 清单无适配组件时,需先说明组件缺口、提出新增建议,再进行代码修改
可复用组件清单(节选):
${digest}
`

  return {
    ...input,
    prompt: `${guard}nn${input.prompt ?? ''}`,
  }
}

这套钩子解决的问题,其实还有更轻的做法:把同样的约束直接写进项目的 AGENTS.md。

AGENTS.md 是给 AI 看的"项目说明书",AI 每次启动都会自动读一遍,Claude CodeCursorCopilot 这些工具都认它。

"优先复用已有组件、禁止内联实现同类控件" 写进它的编码规范模块,再把 component-inventory.json 的路径写进文档索引,AI 从一开始就知道项目里有什么、去哪儿找,不用等钩子临时注入。

Claude Code 自带的 Hooks 就是这类钩子的现成实现,可以直接用。

钩子负责"写之前强制注入",AGENTS.md 负责"启动时就知道",两层叠一起,效果更稳。

AGENTS.md 具体怎么编写、怎么使用,我在另一篇《AGENTS.md:你的AI代码助手,需要一份"项目说明书"》里拆过完整模板,从工具适配到进阶技巧都有,需要的话可以直接看:AGENTS.md:你的AI代码助手,需要一份"项目说明书"。

(三)代码落盘后自动拦截重复

光有前置约束还不够。AI 写完代码后,再自动把新文件和库存组件做结构相似度比对,重合度高的直接报错阻断,让它老老实实回去复用。

// agent-hooks/after-write.ts
import { jaccardAstSimilarity } from '../lib/ast-similarity'
import inventory from '../component-inventory.json'

export async function afterWriteTool(createdFiles: string[]) {
  // 遍历所有新增文件,逐一比对库存组件
  for (const file of createdFiles) {
    for (const item of inventory) {
      const score = await jaccardAstSimilarity(file, item.file)
      // 相似度阈值0.85,可根据团队规范调整
      if (score >= 0.85) {
        throw new Error(
          `${file} 与已有组件 ${item.file} 结构高度重合(相似度${score.toFixed(2)}),请直接复用 ${item.exportName} 组件,禁止重复实现`,
        )
      }
    }
  }
}

前置约束定方向,后置校验卡结果,这套组合拳比人肉评审盯 PR 靠谱得多。

四、生成中:让AI查着"说明书"写代码

库存清单解决了"项目里有什么",解决不了"该怎么用"。

组件 API 一直在变,光靠清单,AI 很容易凭旧印象写出过期 props、错误用法,照样出 bug。

最省事的办法是搭一个最小组件知识库——MCP 工具,强制 AI 写 UI 前先查一下组件真实文档和最新 API,一切以查询结果为准。

组件知识库MCP服务

// mcp-component-kb/server.ts
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { z } from 'zod'
import inventory from '../component-inventory.json'
import docs from './component-docs.json'

const server = new McpServer({ name: 'component-kb', version: '0.1.0' })

// 注册组件查询工具
server.tool(
  'lookup_component',
  '按组件名称、业务职责查询项目内组件的完整API和使用示例',
  {
    query: z.string().describe('查询关键词,例如「提交按钮」「危险操作确认」'),
  },
  async ({ query }) => {
    const q = query.toLowerCase()
    // 模糊匹配符合需求的组件
    const hits = inventory.filter(
      (c) =>
        c.name.toLowerCase().includes(q) ||
        c.file.toLowerCase().includes(q) ||
        (docs[c.name]?.summary ?? '').toLowerCase().includes(q),
    )

    // 返回组件完整信息(API+示例)
    const payload = hits.slice(0, 5).map((c) => ({
      ...c,
      api: docs[c.name]?.api ?? null,
      example: docs[c.name]?.example ?? null,
    }))

    return {
      content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
    }
  },
)

再把调用逻辑写进团队编码规范(规范清单),强制 AI 遵守:

  1. 修改、新增界面组件前,必须先调用 lookup_component 查询存量组件;
  2. 组件的 props、事件名以查询返回的最新 API 为准,禁止自定义编造;
  3. 存量组件能满足需求就默认复用;确实要新增,先说清楚理由和差异化场景。

不用堆超长系统提示词,把"查询校验"变成固定动作,API 误用和组件瞎编自然就少了。

五、生成后:扫描兜底,揪出隐性重复

前后钩子拦得住显性问题,但改几个变量、调一点逻辑的隐性换皮组件,还是可能漏网。

这步用可重复执行的扫描脚本补位,专治结构重复、样式变量漂移、样式写死。

AST结构相似度查重

文本 diff 对"改名换皮、微调逻辑"完全无效。AST 比对不一样——它忽略变量名、注释这些无关信息,只比核心节点结构,同源重复一眼就能认出来。

// lib/ast-similarity.ts
import { parse } from '@babel/parser'
import traverse from '@babel/traverse'
import fs from 'node:fs/promises'

// 提取代码核心结构Token,忽略无关细节
function structuralTokens(code: string): Set<string> {
  const ast = parse(code, { sourceType: 'module', plugins: ['jsx', 'typescript'] })
  const tokens = new Set<string>()

  traverse(ast, {
    enter(path) {
      // 记录节点类型
      tokens.add(path.node.type)
      // 记录JSX标签名
      if (path.isJSXIdentifier()) tokens.add(`jsx:${path.node.name}`)
      // 记录自定义组件标识符
      if (path.isIdentifier() && /^[A-Z]/.test(path.node.name)) {
        tokens.add(`id:${path.node.name}`)
      }
    },
  })

  return tokens
}

// 计算两个文件的杰卡德结构相似度
export async function jaccardAstSimilarity(aPath: string, bPath: string): Promise<number> {
  const [a, b] = await Promise.all([fs.readFile(aPath, 'utf8'), fs.readFile(bPath, 'utf8')])
  const A = structuralTokens(a)
  const B = structuralTokens(b)

  // 计算交集、并集
  let inter = 0
  for (const t of A) if (B.has(t)) inter++
  const union = A.size + B.size - inter

  return union === 0 ? 0 : inter / union
}

遇上结构微调、语义仍重复的复杂场景,可以再叠一层向量嵌入(Embedding),把去标识后的 AST 序列向量化比对,双保鲜。配一个 pnpm scan:near-dup 命令,一键扫全项目。

禁止业务代码写死色值

AI 特别喜欢随手写死十六进制色值、固定尺寸,组件一写死就脱离设计系统,全局迭代直接报废。用一条自定义 ESLint 规则把它摁住:

// 自定义ESLint规则:禁止硬编码颜色值
module.exports = {
  meta: { type: 'problem', docs: { description: '禁止业务代码写死十六进制色值,统一使用全局样式变量' } },
  create(context) {
    // 匹配所有十六进制颜色值
    const HEX = /#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})b/
    return {
      // 校验普通字符串色值
      Literal(node) {
        if (typeof node.value === 'string' && HEX.test(node.value)) {
          context.report({
            node,
            message: `禁止写死颜色值 ${node.value},请使用项目CSS变量(例:var(--color-action))`,
          })
        }
      },
      // 校验模板字符串内色值
      TemplateElement(node) {
        if (HEX.test(node.value.raw)) {
          context.report({
            node,
            message: '模板字符串中禁止写死颜色值,请替换为全局样式变量',
          })
        }
      },
    }
  },
}

效果对比:

<!-- AI容易生成的不规范代码 -->
<button style="background:#2563EB">提交</button>

<!-- 符合设计系统的规范代码 -->
<SubmitAction tone="primary">提交</SubmitAction>

这条扫描管道还能继续扩展:无效导入、废弃组件引用、只改展示名的复制组件,都能查。规范固化成工具,就不靠口头自觉了。

这里有个判断标准值得记住:像写死色值这种 linter 能自动抓的错,不用写进 AGENTS.md;

真正值得写进去的,是"复用组件"这种 linter 抓不住、只能靠 AST 扫描发现的硬约束。

AGENTS.md 只写 AI 自己推断不出来的规则,其他交给工具链。

六、CI门禁:PR合并前最后一道闸

本地钩子、本地扫描都能被人为跳过,但 CI 不行。代码要合入主干,就必须过复用校验。

# .github/workflows/reuse-gate.yml
name: component-reuse-gate
on: [pull_request]
jobs:
  gate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - run: pnpm i
      # 重新扫描生成最新组件库存
      - run: pnpm exec tsx scripts/inventory-components.ts
      # 执行组件重复相似度扫描
      - run: pnpm exec tsx scripts/scan-near-duplicates.ts --threshold 0.85
      # 校验样式变量规范,禁止硬编码样式
      - run: pnpm lint:design-tokens

只要扫出高相似度重复组件或违规硬编码样式,CI 直接失败、阻断合并。冗余代码入库这条路,从工程上就堵死了。

总结

让 AI 写代码很容易,难的是让它贴着你项目的规范、设计系统和存量能力来写。

组件库存让 AI 知道"项目里有什么",AGENTS.md 让它启动就懂规矩,MCP 知识库让它搞懂"该怎么用",扫描校验和 CI 门禁守住"不能乱改"的底线,闭环迭代让这套体系越用越聪明。

把这套链路跑通之后,代码评审不用再人肉查重、人肉纠错,AI 才真正从"运维负担"变成"提效工具"。

热门栏目