倒反天罡:AI 友好的前端组件设计

最近在做 AI 前端代码生成,之前用 Antd 时候发现生成的代码质量不错,但因为内部沉淀了大量更符合业务规范的组件,于是借助 MCP 把内部组件信息提供给大模型。

结果虽然使用了内部组件,但即使用了 Claude 3.7 后生成的代码质量仍旧很低,综合分析了一下发现我们内部的组件对 AI 实在不友好,对照 Antd 后总结了几条 AI 友好的前端组件设计规则

使用原子化 CSS

TailwindCSS 究竟适合大型项目还是个人项目,使用 TailwindCSS 之后代码可维护性究竟是变好还是变坏,这些目前业内还有争议,但这些争论是以人来编写、维护代码为前提展开的,如果聚焦到 AI Code TailwindCSS 肯定是更友好的

一段简单的 HTML,开发者看到之后需要结合 CSS 文件才能了解其样式,如果需要对 container 样式做调整,考虑到全局影响大部分开发者是不敢轻易动 .container内容的

html 复制代码
<div class="container">
  <button class="primary-btn">提交</button>
</div>
  • container 的样式可能涉及布局、背景色、边距等,AI 需要结合 CSS 文件才能理解具体效果。
  • primary-btn 的颜色可能依赖父级类(如 .dark .primary-btn),AI 难以直接推断样式规则。

而使用原子化的 CSS 后情况会有所改变,AI 会更擅长

html 复制代码
<div class="container mx-auto p-4">
  <button class="bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded">提交</button>
</div>
  • 原子化 CSS 类名遵循单一职责原则,每个类名仅代表一种样式属性,修改只会影响当前 DOM,没有额外副作用。
  • 原子化 CSS 没有业务属性(尤其是人命名的还可能有歧义),降低了 AI Code 上下文依赖,提高了理解代码的效率。
  • 原子化 CSS 类名自身包含了足够的信息,AI 无需查看其它代码就能理解其样式效果,显著降低 token 数。
  • 由于原子化 CSS 类名有固定的命名规则,AI 可以依据这些规则来预测和推断新的样式的效果。

通用属性命名保持一致

如果我们提供了一个组件库,那么通用属性需要使用统一命名,降低开发者学习和 AI 训练成本

常规组件

属性名称 描述 适用组件 备注
disabled 设置组件为禁用状态 Button, Input, Select, Checkbox, Radio, Switch 等 通常用于控制用户交互,防止操作
loading 显示加载状态 Button, Spin, Table 等 用于指示正在进行的操作或加载过程
visible 控制组件的可见性 Modal, Tooltip, Popover, Drawer 等 用于显示或隐藏弹出层组件
onClick 点击事件的回调函数 Button, Icon, Card, Dropdown 等 处理用户的点击操作
style 组件的行内样式 几乎所有组件 用于自定义组件的样式
className 组件的自定义类名 几乎所有组件 用于添加自定义 CSS 类
size 设置组件大小 Button, Input, Select, DatePicker, Avatar 等 常见值如 small, middle, large
prefix 输入框的前缀图标或内容 Input 在输入框前添加图标或文本
suffix 输入框的后缀图标或内容 Input 在输入框后添加图标或文本
mode 设置组件的模式或类型 Menu, Cascader, Select 等 控制组件的显示模式,如多选、标签模式等
theme 设置组件的主题样式 Button, Dropdown, Menu 等 常见值如 light, dark
bordered 是否显示边框 Input, Select, Table 等 控制组件是否显示边框
tooltip 设置组件的工具提示信息 Button, Icon, Avatar 等 为组件添加悬停时显示的提示信息
placement 设置弹出内容的位置 Tooltip, Popover, Dropdown 等 控制弹出层相对于目标元素的位置
borderRadius 设置组件的圆角半径 Button, Card, Avatar 等 自定义组件的圆角样式
ghost 透明背景样式 Button, Card 等 通常用于在深色背景上显示浅色组件
type 设置组件的类型或风格 Button, Input, Alert, Tag 等 不同组件有不同的类型选项,如按钮类型 primary, default

表单相关

属性名称 描述 适用组件 备注
checked 设置选中状态 Checkbox, Radio, Switch 等 用于表示选中或激活状态
defaultValue 组件的默认值 Input, Select, DatePicker, Cascader 等 设置组件的初始值
value 组件的当前值 Input, Select, DatePicker, Cascader 等 用于受控组件,值由父组件管理
onChange 值变化时的回调函数 Input, Select, Checkbox, Radio, Switch, DatePicker 等 处理用户输入或选择的变化
placeholder 输入框的占位提示 Input, Select, DatePicker 等 提示用户输入内容
allowClear 是否允许清除内容 Input, Select, DatePicker 等 显示清除按钮,允许用户一键清空输入
autoFocus 组件挂载后自动聚焦 Input, Select, Modal 光标默认聚焦

API 语义化

在组件 API 设计中,开发者常面临简洁性与语义清晰性的平衡难题。为避免代码冗长或保持开放性,部分 API 会采用含义宽泛的名称(如 handle()process()),这种设计虽提升了灵活性,却可能牺牲语义明确性,导致新手难以快速理解功能逻辑,甚至让 AI 分析工具产生歧义

javascript 复制代码
function handleData(data, options) {
  // 根据 options 决定是过滤、排序还是转换数据
}
  1. handleData 的功能不明确,开发者需查阅文档才能理解其支持的操作。
  2. 新用户可能误以为这是一个通用方法,但实际需要复杂的参数配置。
  3. AI 可能无法推断其具体行为,导致代码分析或智能提示不准确。

在保持简洁性的同时,通过语义化命名和文档注释增强可读性,同时通过扩展性设计(如参数、选项)兼顾开放性

javascript 复制代码
// 语义明确的函数名,直接体现功能
function addUser(user) { /* ... */ }
function removeUser(userId) { /* ... */ }
function updateUser(userId, newData) { /* ... */ }

// 通过参数扩展开放性,而非宽泛命名
function findUsers(filter = {}, sort = {}) {
  // 支持灵活的过滤和排序参数
}

因此在进行组件设计时语义 > 简洁性,名称应直接反映核心功能(如 validateEmail() 优于 check()),通过参数或选项支持灵活配置,而非依赖宽泛的函数名。虽然现在大部分项目已经不再使用 React 15,可以感受下 React 15 的生命周期函数命名

函数名 含义
constructor() 构造函数,组件初始化
componentWillMount() 将要挂载组件
render() 渲染组件,生成 UI
componentDidMount() 组件已挂载
componentWillReceiveProps() 接收新 props前的准备
shouldComponentUpdate() 是否应该更新,决定是否跳过后续渲染
componentWillUpdate() 组件将要更新
componentDidUpdate() 组件已更新
componentWillUnmount() 组件将要卸载
  1. 阶段明确:函数名通过 Mount(挂载)、Update(更新)、Unmount(卸载)直接体现所处阶段。
  2. 时序清晰:Will 和 Did 后缀区分动作发生的时间点,这种命名方式让开发者无需查看文档即可推测函数的执行时机。
  3. 功能直白:render()、shouldComponentUpdate() 等名称直接描述功能,无需额外解释。

这样的 API 设计即使是新手也很容易指导该使用哪个

属性单一职责

单一职责原则是面向对象设计中的五大原则之一,主张一个模块、类或函数应该仅有一个引起其变化的原因,在前端开发中,这一原则同样适用于组件的属性设计。

示例中按钮组件的custom 属性,既用于控制按钮的样式,又用于决定按钮的点击事件逻辑。当开发者看到这个属性时,很难快速理解它的具体作用,而且在修改按钮样式或点击事件时,可能会影响到其它方面的功能,增加了代码的维护难度。

javascript 复制代码
import React from 'react';

const CustomButton = (props) => {
  const { custom } = props;
  
  const handleClick = () => {
    if (custom === 'type1') {
      // 执行特定的点击逻辑
      console.log('Type 1 click');
    } else if (custom === 'type2') {
      // 执行另一种点击逻辑
      console.log('Type 2 click');
    }
  };

  const buttonStyle = custom === 'type1'
   ? { backgroundColor: 'red' }
    : { backgroundColor: 'blue' };

  return (
    <button style={buttonStyle} onClick={handleClick}>
      Custom Button
    </button>
  );
};

export default CustomButton;

当每个属性只负责一个明确的功能时,开发者和 AI 可以更容易地理解代码的意图。同时如果某个功能需要调整,只需要修改对应的属性即可,不会影响到其它不相关的功能,代码的修改和维护更加容易。

把示例中的 custom 属性拆分成 clickTypebackgroundColor 两个属性,clickType 专门控制点击逻辑,backgroundColor 专门控制按钮的背景颜色,代码会清晰非常多

javascript 复制代码
import React from 'react';

const CustomButton = ({ clickType, backgroundColor }) => {
    const handleClick = () => {
        if (clickType === 'type1') {
            // 执行特定的点击逻辑
            console.log('Type 1 click');
        } else if (clickType === 'type2') {
            // 执行另一种点击逻辑
            console.log('Type 2 click');
        }
    };

    const buttonStyle = { backgroundColor };

    return (
        <button style={buttonStyle} onClick={handleClick}>
            Custom Button
        </button>
    );
};

export default CustomButton;    

属性值可枚举

如果属性值可枚举,尽量设计成 TypeScript 联合类型,方便开发者和 AI 学习使用,Antd Button 组件的多个属性都符合这一设计规范

shape 设置按钮形状 default circle round
size 设置按钮大小 large middle small

属性值可拓展

属性可拓展性是指组件的属性允许开发者根据不同的需求传入不同类型的值,以实现多样化的功能和定制化效果。Antd Button 组件的 loading 属性就是一个很好的体现属性可拓展性的例子,它支持传入 boolean 类型或 React Node 类型的值。

当默认 loading 效果满足诉求时候传入 boolean 类型值即可

javascript 复制代码
<Button type="primary" loading={true}>
  提交
</Button>

当需要自定义 loading 效果时候可以传入自定义的 Icon

javascript 复制代码
<Button type="primary" loading={{ icon: <SyncOutlined spin /> }}>
  提交
</Button>

这样既保证了组件的业务拓展性,又没违背单一职责原则

组件可组合

随着个性化定制诉求的增多,复杂组件的属性可能会无限膨胀,组件的学习、维护成本显著提升。举个例子,组件中 header 部分的标题只能控制内容文案和对齐方式,如果希望标题前面有个 Icon 就需要升级组件,后续如果业务有修改标题颜色的诉求,需要继续升级组件

javascript 复制代码
const Modal = (props) => {
  const {
    title = '',
    style = {},
    visible = false,
    onClose,
    titleAlign = 'left',
    children,
  } = props;

  function close(e) {
    e.stopPropagation();
    onClose();
  }
  
  if (!visible) return null;
  
  return (
    <div className={`modal`} style={style}>
      <div className="mask" onClick={close} />
      <div
        className="modal"
      >
        <div className="header">
          <div className={`title ${titleAlign}`}>
            {title}
            <div className="cancel-icon" onClick={close} />
          </div>
        </div>
        <div className="content">{children}</div>
      </div>
    </div>
  );
};
export default Modal;

传统的开放封闭原则和组合优先于继承理念同样可以给前端复杂组件设计指导

  • 组件无法覆盖变化时候,通过容器组件与子组件的组合实现功能扩展。
  • 使用 children 插槽让用户自定义内容,而非拓展 props。

上文中提到的 Antd 对自定义 loading 处理其实就使用了这一设计理念,Form 更是典型应用,通过提供 Form.Item 避免所有配置集中于 Form 组件,Form.Item 支持 children 更是提升了其内容的灵活性

javascript 复制代码
<Form onFinish={handleFinish}>
  <Form.Item label="用户名" name="username" rules={[{ required: true }]}>
    <Input />
  </Form.Item>
</Form>

可以通过更多示例感受下我们在进行组件设计时候开放、封闭的粒度

过度依赖属性传递内容

javascript 复制代码
<Page
  header={<Header />}
  sidebar={<Sidebar />}
  content={<Content />}
  footer={<Footer />}
/>

通过 children 表达布局

javascript 复制代码
<Page>
  <Header />
  <Sidebar />
  <Content />
  <Footer />
</Page>

将子组件强制转为属性

javascript 复制代码
<Tabs
  items={[
    { key: "1", label: "Tab 1", children: <div>Content 1</div> },
    { key: "2", label: "Tab 2", children: <div>Content 2</div> },
  ]}
/>

通过 children 嵌套 TabPane

javascript 复制代码
<Tabs>
  <Tabs.TabPane tab="Tab 1" key="1">
    <div>Content 1</div>
  </Tabs.TabPane>
  <Tabs.TabPane tab="Tab 2" key="2">
    <div>Content 2</div>
  </Tabs.TabPane>
</Tabs>

拓展、组合开放粒度

当然并不是鼓励开发者无限制的开放组件的拓展性,毕竟拓展越多组件独立完成的功能就越少,开发者享受组件的提效优势会降低,组件的拓展开放粒度要适应业务实际变化的诉求,如果业务设计规范要求、统一了 Modal 标题的样式为基本文案,那么 Title 属性类型可以直接从 ReactNode 改为 String

组件文档规范

一个组件的 README 会优先被 AI 学习,这一点在用 Cursor 编程中有深刻的体会,组件文档至少要包含以下关键信息:

  1. 概述:简短描述组件的主要功能和使用场景
  2. 安装方式
  3. 基本用法:几个最高频的使用示例(可借助 AI 自动生成)
  4. Props/属性列表:所需的信息基本就是组件的 TS 描述,可根据 AI 自动生成
    • 名称
    • 类型
    • 默认值
    • 是否必填
    • 描述(用途和影响)
相关推荐
关山3 小时前
MCP实战
python·ai编程·mcp
大模型教程4 小时前
Cursor 快速入门指南:从安装到核心功能
程序员·llm·cursor
jifei5 小时前
有了Cursor,为什么还要买摸着Cursor过河的Trae?
cursor·trae
bug菌5 小时前
Trae如何快速辅助Java开发者进场AI编程?打破传统编程思维!
aigc·ai编程·trae
量子位6 小时前
一周六连发!昆仑万维将多模态AI卷到了新高度
ai编程
量子位6 小时前
16岁炒马斯克鱿鱼,SpaceX天才转投北大数学校友赵鹏麾下
ai编程
用户4099322502126 小时前
如何用Prometheus和FastAPI打造任务监控的“火眼金睛”?
后端·ai编程·trae
bug菌6 小时前
Java开发者还在被Python“碾压“?用Trae反击,让智能化应用开发快到飞起!
aigc·ai编程·trae
bug菌7 小时前
当AI遇上编程,传统IDE还能守住最后一道防线吗?Trae告诉你答案!
aigc·ai编程·trae
信码由缰10 小时前
软件开发中的 8 个伦理问题示例
ai编程