HOW - 设计和实现一个动态渲染不同表单类型组件的 DynamicFormItem 组件
目录
- 背景
- 方案设计
- 方案一:通过 `type` 字段判断渲染组件类型
- 思路
- 示例结构
- 优点
- 缺点
- 方案二:通过 itemRender 自定义组件渲染
- 思路
- 示例结构
- 优点
- 缺点
- 两者对比总结
- 综合建议
背景
我们已经有了 Select、Input、Radio 等基本表单组件。现在我们对于不同模块,如事件列表、属性列表、用户列表,它们都有一个通用筛选模块。这个筛选模块需要支持配置不同字段(包含 label、type、placeholder 等),对于事件列表,支持事件名称、事件类型、是否上线,对于属性列表,支持属性名称、属性类型…
方案设计
方案一:通过 type
字段判断渲染组件类型
思路
你定义一个统一的渲染组件,如 DynamicFormItem
,它根据配置项中传入的 type
(如 'input'
, 'select'
, 'radio'
等)自动渲染对应的 antd 组件。
示例结构
interface FieldConfig {
label: string;
name: string;
type: 'input' | 'select' | 'radio';
options?: { label: string; value: string }[];
placeholder?: string;
rules?: Rule[];
}
const DynamicFormItem: React.FC<{ field: FieldConfig }> = ({ field }) => {
const { type, label, name, options = [], ...rest } = field;
const renderFormItem = () => {
switch (type) {
case 'input':
return <Input placeholder={rest.placeholder} />;
case 'select':
return (
<Select placeholder={rest.placeholder}>
{options.map(opt => (
<Select.Option key={opt.value} value={opt.value}>
{opt.label}
</Select.Option>
))}
</Select>
);
case 'radio':
return (
<Radio.Group>
{options.map(opt => (
<Radio key={opt.value} value={opt.value}>
{opt.label}
</Radio>
))}
</Radio.Group>
);
default:
return null;
}
};
return (
<Form.Item label={label} name={name} rules={rest.rules}>
{renderFormItem()}
</Form.Item>
);
};
优点
- 结构清晰,易于配置,适合低代码或表单配置系统。
- 增加新类型只需扩展
switch
或配置 map。 - 适用于统一风格、标准化表单。
缺点
- 灵活性较差,组件渲染逻辑由内部掌控,外部不可定制 UI。
- 处理复杂交互逻辑(如依赖联动、动态插槽)较为困难。
方案二:通过 itemRender 自定义组件渲染
思路
每个配置项支持传入一个 itemRender
方法,用于返回自定义的表单项组件,这种方式允许你完全控制每一项的渲染逻辑。
示例结构
interface FieldConfig {
label: string;
name: string;
rules?: Rule[];
itemRender?: () => React.ReactNode;
}
const DynamicFormItem: React.FC<{ field: FieldConfig }> = ({ field }) => {
const { label, name, rules, itemRender } = field;
return (
<Form.Item label={label} name={name} rules={rules}>
{itemRender ? itemRender() : null}
</Form.Item>
);
};
优点
- 灵活性极高,支持所有 antd 控件、组合控件甚至第三方控件。
- 更适合高级场景(如复杂的嵌套表单、富文本、自定义行为等)。
- 渲染逻辑由使用者控制,组件更解耦、可组合性强。
缺点
- 配置方式更偏向开发者,门槛略高。
- 所有类型都需要手动写
itemRender
,可能重复。
两者对比总结
对比项 | 方案一:基于 type | 方案二:基于 itemRender |
---|---|---|
配置复杂度 | ✅ 低(适合配置表单) | ❌ 高(需要写渲染逻辑) |
灵活性 | ❌ 低 | ✅ 高 |
适用场景 | 表单类型有限、低代码平台 | 个性化表单、交互复杂 |
可维护性 | ✅ 可统一管理控件逻辑 | ❌ 分散在配置项中 |
扩展性 | 一定程度可扩展 | 完全可扩展 |
综合建议
你可以考虑 两者混用的折中方案:
interface FieldConfig {
label: string;
name: string;
type?: 'input' | 'select' | 'radio';
options?: any[];
itemRender?: () => React.ReactNode;
}
优先用 itemRender
,如果没有就按 type
渲染,这样同时具备标准性和灵活性,适合中大型业务系统。