前言
开发这个组件的目的是能够更方便的通过配置来实现字段查询模块,如果你的功能有些复杂,比如说涉及到一些字段的联动逻辑。
其实通过配置也能做,但需要考虑的是,如果把一些复杂的工单做在组件里面,就会导致配置也变得复杂困难,那这个时候倒不如直接写代码了。
具体场景中,是用配置来写,还是通过开发代码来写,首先需要你花几分钟来思考一下。
schema
我们先来看看需要做的是什么,大致就是下面的这样一个组件,相信做过后台管理系统的同学都不会陌生。
那我们会怎么去使用这个组件呢,可以看一下下面的一个使用 demo
:
import { useRef, useState } from "react";
import Filter from "../../components/Filter";
import { COMPONENT_TYPE } from "../../components/Filter/constant";
const Config = () => {
const ref = useRef(null);
const [disabled, setDisabled] = useState(false);
const fieldsConfig = [
{
name: "name",
type: COMPONENT_TYPE.INPUT,
label: "姓名",
initialValue: "123,",
componentProps: {
disabled,
placeholder: "请输入姓名",
},
},
{
name: "number",
type: COMPONENT_TYPE.INPUT_NUMBER,
label: "数字输入框",
},
{
name: "date",
type: COMPONENT_TYPE.DATE_PICKER,
label: "日期",
},
{
name: "status",
type: COMPONENT_TYPE.SELECT,
label: "状态",
initialValue: 1,
componentProps: {
placeholder: "请选择状态",
allowClear: true,
},
onChange: (e) => {
console.log("e", e);
setDisabled(e === 2);
},
data: [
{
label: "开启",
value: 1,
},
{
label: "关闭",
value: 2,
},
],
},
];
return <Filter ref={ref} fieldsConfig={fieldsConfig} />;
};
export default Config;
希望是可以通过配置化的形式去调用该组件,对于设计一个组件来说,定义好它的属性以及方法是很有必要的,所以我们先要定义一份 schema
。
fieldsConfig:[
{
// 表单项唯一key
name:"",
// 表单项label
label:"",
// 表单项组件类型:输入框、下拉、时间选择等
type:"",
// 表单项初始值
initialValue:"",
// 表单字段值变化时的回调,可以用做字段联动
onChange:() => { },
// 表单组件的候选值,比如select的下拉列表
data:[],
// 表单项的props,参考Form.Item组件
formItemConfig: { },
// 组件的props,参考具体的组件
componentProps:{ }
}
]
// 表单的props,参考Form组件
formConfig:{
}
// 一些其他的配置
extraConfig:{
// 一行有多少个表单项
columnCount:"",
// 操作——查询、重置等
actions:()=>{
},
}
antd5封装
我们会分别对 antd3
以及 antd5
封装一份组件,但是对于使用方来说 schema
是相同的,所以会在封装过程中磨平这个差异。
首先我这里通过判断 Form
组件是否有 useForm
这个 hook
来判断 antd
的版本,这是一个运行时的判断方法,最后会介绍一种更通用的判断某个库是什么版本的方法。
然后实现一个 Filter
组件,作为入口:
先来看基于 antd5
的封装,
首先是整个表单的布局,会读取传入的配置中的这两个属性,分别表示一行多少个表单项、以及项目之间的间距是多少
然后分割成一个二维数组,通过 Row
与 Col
这两个组件来实现布局
最后来到 renderField
中,这里就是主要渲染组件的逻辑,根据传入的不同 type
,来渲染不同的组件:
const renderField = (field) => {
let Component = <></>;
const {
type,
name,
formItemConfig,
label,
data,
componentProps = {},
} = field;
switch (type) {
case COMPONENT_TYPE.INPUT:
Component = Input5;
break;
case COMPONENT_TYPE.SELECT:
Component = Select5;
break;
case COMPONENT_TYPE.INPUT_NUMBER:
Component = InputNumber;
break;
case COMPONENT_TYPE.DATE_PICKER:
Component = DatePicker;
break;
default:
break;
}
return (
<Form.Item {...formItemConfig} name={name} label={label}>
<Component {...{ ...componentProps, data }} />
</Form.Item>
);
};
有一些组件需要分成 antd5
与 antd3
版本的实现,有一些则不用,比如 input
,它是不用分开的
但比如 select
,它5与3版本之间有一个核心 api
差距比较大,所以需要分开,如果是其他透传的 props
,则无需分开,在组件调用的时候注意即可。
这个初始值的处理会是一个小技巧:
其实就是把一个数组转化成一个 map
,在开发过程中经常会遇到这个场景,如果不用 reduce
的话,常常会这样处理:
const func = (list) => {
const obj = {}
list.map(item=>{
obj[item.id] = item.label
})
return obj
}
用 reduce
则会优雅一些,一行代码可以搞定。
然后,会通过 useImperativeHandle
这个 api
,把 form
对象暴露给父组件调用
最后就是一个字段联动,对于 antd5
来说,主要是监听 onValuesChange
事件,然后调用 schema
中传入的对应字段的 onChange
方法。
const handleValueChange = (fields) => {
Object.keys(fields).forEach((key) => {
const onChange = fieldsConfig.find(
(config) => config.name === key
)?.onChange;
if (onChange) {
onChange(fields[key]);
}
});
};
antd3封装
接下来来看 antd3
的封装,主要差异在于 form
表单组件的封装。一些 api
实现会与 antd5
版本差距比较大:
- 通过
getFieldDecorator
绑定字段 - 没有
onValuesChange api
,可以把schema
中的onChange
传入具体组件中来监听变更
const renderField = (field) => {
let Component = <></>;
const {
type,
name,
formItemConfig,
label,
data,
initialValue,
onChange,
componentProps = {},
} = field;
switch (type) {
case COMPONENT_TYPE.INPUT:
Component = Input;
break;
case COMPONENT_TYPE.SELECT:
Component = Select3;
break;
case COMPONENT_TYPE.INPUT_NUMBER:
Component = InputNumber;
break;
case COMPONENT_TYPE.DATE_PICKER:
Component = DatePicker;
break;
default:
break;
}
return (
<Form.Item
{...{
labelCol: { span: 6 },
wrapperCol: { span: 18 },
}}
{...formItemConfig}
label={label}
>
{form.getFieldDecorator(name, {
...formItemConfig,
initialValue,
})(<Component {...{ ...componentProps, data, onChange }} />)}
</Form.Item>
);
};
对于调用方来说, antd3
版本的需要这样拿到 form
实例,有一点差别,但是感觉不大,就不打算抹平这个差异了。
更优雅的判断antd版本
const fs = require('fs');
const path = require('path');
// 定义宿主环境package.json路径
const packageJsonPath = path.resolve(process.cwd(), 'package.json');
// 读取并解析package.json文件
fs.readFile(packageJsonPath, 'utf8', (err, data) => {
if (err) {
console.error('Error reading package.json:', err);
process.exit(1);
}
try {
const packageJson = JSON.parse(data);
if (packageJson.dependencies && packageJson.dependencies['antd']) {
// 拿到antd版本
console.log(packageJson.dependencies['antd'])
}
} catch (e) {
console.error('Error parsing package.json:', e);
process.exit(1);
}
});
{
"name": "your-npm-package",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"install": "node install.js"
},
"keywords": [],
"author": "",
"license": "ISC"
}
拿到 antd
版本之后,再往包里写入一些东西,来标识使用哪一个版本的组件。在这个场景下,如果做成 npm
包,下载的时候 npm
包不应该被编译,而是跟着宿主项目一起编译打包。
最后
为什么要重复造轮子而不是使用现成的一个库?看了一下 formRender
跟 formily
的依赖,都是需要 antd4
及以上,而我们有几个项目都是 antd3
,而且版本不会再升级。所以就自己造了一个。
但是,这种类似“低码”的都是都有一个通病——要么不够灵活,要么配置成本又很高,所以自己内心应该有一杆秤,什么时候手写,什么时候用这个配置化的东西,做需求之前花两分钟思考一下。
最后,封装这个东西的初衷也是为了方便我自己,至于它能不能造福别人,就看缘分了哈哈哈。