AI 驱动的组件 API 设计建议:基于使用频率与命名一致性的智能推荐
一、当 Table 组件有 47 个 Props——API 膨胀的无声灾难
去年重构公司的通用表格组件时,我打开 Props 定义文件,看到 47 个属性。onRowClick、onRowSelect、onClickRow、handleRowClick——一个"点击行"的行为有四种命名方式,分别是四位工程师在不同年份留下的遗产。
这不是个例。组件库 API 的熵增是天然趋势:每个新需求都可能引入一个新 Prop,每个新加入的工程师都可能带入自己的命名习惯。AI 在这里的价值不是告诉你"这个组件该怎么设计",而是基于全量代码仓库的使用数据,给你一份量化报告:
- 哪些 Props 承担了 90% 的使用,哪些是"僵尸属性"(定义了但从未被使用)
- 命名不一致的 Props 集群(
onClickvshandleClickvsclickHandler) - 应该拆分但被强行合并的 Props(一个
options对象里藏了 15 个语义独立的配置项)
二、API 分析引擎的架构设计
系统的核心是Props 使用频率矩阵——一张二维表,行是组件名,列是 Props 名,值是"该 Props 在该组件中被使用的次数"。这张表是后续所有分析(僵尸检测、聚类、组合模式挖掘)的基础。
三、生产级实现
// analyzer/props-analyzer.ts // 组件 API 分析引擎 // 遍历项目中所有 tsx/jsx 文件,提取组件使用模式 import * as ts from 'typescript'; import * as fs from 'fs'; import * as path from 'path'; import { globSync } from 'glob'; interface PropUsage { componentName: string; propName: string; usageCount: number; // 该 Prop 的使用示例(代码片段) examples: string[]; // 该 Prop 的值类型分布:{ 'string': 120, 'boolean': 45, 'enum': 30 } valueTypes: Record<string, number>; } interface APIReport { // 僵尸属性:定义了但从未被使用的 Props zombieProps: Array<{ component: string; prop: string; definedIn: string }>; // 命名不一致集群:功能相同但命名不同的 Props 组 inconsistentNaming: Array<{ pattern: string; // 语义模式,如 '行点击事件' variants: Array<{ component: string; prop: string; count: number }>; recommendation: string; // AI 推荐的标准命名 }>; // 高内聚 Props 组:应该拆分为子组件的 Props 集合 cohesiveGroups: Array<{ component: string; props: string[]; // 总是一起出现的 Props 组 cooccurrenceRate: number; // 共现率,0~1 suggestion: string; }>; } /** * 主分析入口 * @param projectRoot 项目根目录,递归搜索所有 .tsx 文件 */ function analyzeComponentAPI(projectRoot: string): APIReport { const tsxFiles = globSync(`${projectRoot}/**/*.tsx`, { ignore: ['**/node_modules/**', '**/dist/**'] }); const allUsages: PropUsage[] = []; for (const file of tsxFiles) { const source = fs.readFileSync(file, 'utf-8'); const sourceFile = ts.createSourceFile( file, source, ts.ScriptTarget.Latest, true ); // 遍历 AST,收集所有 JSX 元素的 Props 使用 collectJSXProps(sourceFile, source, allUsages); } // 合并同一组件的同一 Prop 的使用数据 const merged = mergeUsages(allUsages); return { zombieProps: detectZombieProps(merged), inconsistentNaming: detectNamingInconsistency(merged), cohesiveGroups: detectCohesiveGroups(merged) }; } /** * AST 遍历:从 JSX 元素中提取组件名和 Props * 设计意图:不仅收集 Props 名称,还收集值类型, * 为后续的类型不一致检测提供数据基础 */ function collectJSXProps( node: ts.Node, source: string, usages: PropUsage[] ): void { // JSX 自闭合标签:<Button type="primary" /> // JSX 开闭标签:<Table data={list}><Column title="名称" /></Table> if (ts.isJsxSelfClosingElement(node) || ts.isJsxOpeningElement(node)) { const tagName = node.tagName.getText(source); // 跳过原生 HTML 标签(div, span, input...),只分析自定义组件 if (isNativeHTML(tagName)) return; // 遍历该 JSX 元素的所有属性 node.attributes.properties.forEach((attr) => { if (ts.isJsxAttribute(attr)) { const propName = attr.name.getText(source); // 推断属性值的类型 const valueType = inferValueType(attr.initializer, source); usages.push({ componentName: tagName, propName, usageCount: 1, examples: [getNodeText(attr, source)], valueTypes: { [valueType]: 1 } }); } }); } // 递归遍历子节点 ts.forEachChild(node, (child) => collectJSXProps(child, source, usages)); } /** * 检测命名不一致 * 核心思路:将 Props 名称按语义模式聚类 * * 例如:onRowClick、handleRowClick、rowClickHandler * 都匹配模式 /row.*click|click.*row/i → 它们应该统一命名 */ function detectNamingInconsistency(usages: PropUsage[]) { // 语义模式映射表——将相似命名的 Props 归为同一"语义意图" const patterns = [ { regex: /(on|handle)?\w*click\w*/i, semantic: '点击事件', convention: 'on{Target}Click' }, { regex: /(on|handle)?\w*change\w*/i, semantic: '变更事件', convention: 'on{Target}Change' }, { regex: /(is|has|should|can)\w+/i, semantic: '布尔状态', convention: '{prefix}{Noun}' }, { regex: /render\w*/i, semantic: '自定义渲染', convention: 'render{Noun}' }, ]; const results = []; for (const { regex, semantic, convention } of patterns) { // 找到所有匹配该语义的 Props const matched = usages.filter(u => regex.test(u.propName)); if (matched.length > 1) { // 按组件分组 const byComponent = groupBy(matched, 'componentName'); const variants = Object.entries(byComponent).map(([comp, props]) => ({ component: comp, prop: props[0].propName, count: sum(props, 'usageCount') })); // 如果同一语义在不同组件中有不同的命名,标记为不一致 const uniqueNames = new Set(variants.map(v => v.prop)); if (uniqueNames.size > 1) { results.push({ pattern: semantic, variants, recommendation: convention }); } } } return results; } /** * 检测僵尸属性 * 逻辑很简单:在组件定义文件中解析 Props 接口,然后检查全量使用数据中是否有匹配 */ function detectZombieProps(usages: PropUsage[]) { // 1. 从组件定义文件中提取所有声明的 Props const definedProps = extractAllDefinedProps(); // 2. 找出定义过但从未出现在 usage 中的 Props const usedProps = new Set(usages.map(u => `${u.componentName}.${u.propName}`)); return definedProps .filter(dp => !usedProps.has(`${dp.component}.${dp.prop}`)) .map(dp => ({ component: dp.component, prop: dp.prop, definedIn: dp.sourceFile })); } // 注:extractAllDefinedProps 需解析组件源文件的 TypeScript 类型定义,篇幅所限略去实现细节四、信任边界与误判控制
AI 分析最大的风险是误报——把一个正常使用的 Props 标记为僵尸属性,或者把一个有意的命名差异(例如onApply和onConfirm确实是不同语义)标记为不一致。
降低误报的三条规则:
必须有足够样本量。组件使用少于 5 次的,不参与任何分析(统计噪声太大)。
语义聚类必须有人工 Review 环节。onRowClick和onCellClick虽然都匹配 click 模式,但它们是不同的语义意图。AI 只能提"疑似不一致",最终判定权留给组件 Owner。
僵尸属性不等同于"立刻删除"。它可能被外部的消费方(不在当前仓库中)使用。处理策略应该是"标记 @deprecated,观察一个版本周期"。
五、总结
组件 API 的设计质量不是一次 Code Review 能解决的,它是一个持续熵增过程。AI 的价值是把这种缓慢退化的趋势量化、可视化、可干预。
三个核心输出:
- 使用频率矩阵——告诉组件维护者"你的用户在怎么用你的 API"
- 命名一致性报告——把分散在不同组件中的命名分歧聚合到一张表里
- 共现关系图——揭示 Props 之间的隐式依赖关系,为拆分/合并决策提供数据依据
工具的本质是把"我觉得"变成"数据显示"。当 47 个 Props 中只有 11 个承担了 95% 的使用量,重构的优先级自然就清楚了。