最近在做 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 决定是过滤、排序还是转换数据
}
handleData
的功能不明确,开发者需查阅文档才能理解其支持的操作。- 新用户可能误以为这是一个通用方法,但实际需要复杂的参数配置。
- 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() | 组件将要卸载 |
- 阶段明确:函数名通过 Mount(挂载)、Update(更新)、Unmount(卸载)直接体现所处阶段。
- 时序清晰:Will 和 Did 后缀区分动作发生的时间点,这种命名方式让开发者无需查看文档即可推测函数的执行时机。
- 功能直白: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
属性拆分成 clickType
和 backgroundColor
两个属性,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 编程中有深刻的体会,组件文档至少要包含以下关键信息:
- 概述:简短描述组件的主要功能和使用场景
- 安装方式
- 基本用法:几个最高频的使用示例(可借助 AI 自动生成)
- Props/属性列表:所需的信息基本就是组件的 TS 描述,可根据 AI 自动生成
- 名称
- 类型
- 默认值
- 是否必填
- 描述(用途和影响)