大家好,我是electrolux,在之前做项目的时候,大多数遇到这样一个问题,重复的表单配置,或者是需要给其他不太懂代码的人去配置表单(低代码场景),那么这个时候我们可能会想到formity
这类库,但是这玩意太庞大了,在没有那么复杂的场景下,咱们可以自己实现一套基础的版本,仅仅需要100行代码就可以实现
咱们这个版本支持三个功能
- 功能1:json 渲染表单
- 功能2:表单插件扩展
- 功能3:动态设置与增删表单
代码仓库如下: github.com/electroluxc…
demo: electroluxcode.github.io/mini-form-g…
最后成品如下,
咱们会用最简单的代码进行实现,更多的逻辑可以看到我的仓库。
对了,标题的dsl指的是Domain Specific Language
,
在咱们这里指的就是咱们的json渲染成表单的一种特定的结构吧
功能1:json 渲染表单
这一步是最基础的一步,如何通过json数据渲染成表单呢,答案是递归解析,那么在递归解析之前我们还要做的一个事情就是定义插件逻辑
咱们先来一个不带定义插件逻辑的版本,30行代码就可以解决
import { isFunction } from "lodash-es";
import { CompProps, RecursiveCompProps } from "./type";
// 递归组件
const RecursiveComp: React.FC<RecursiveCompProps> | React.ElementType = ({ item }: RecursiveCompProps) => {
const { Component, children, data, ...rest } = item;
if (!Component) {
return null;
}
// 渲染 children 的递归调用
const childElements = children ? (
children.map((child) => (
isFunction(RecursiveComp) ?
RecursiveComp({ item: child, }) :
<RecursiveComp item={child} />
))
) : null;
return (
<Component {...data} {...rest}>
{childElements}
</Component>
);
};
// 主组件
const Comp: React.FC<CompProps> = ({ data }) => {
return (
<>
{data.map((item, index) => (
<RecursiveComp key={index} item={item} />
))}
</>
);
};
export default Comp;
这个是要求是要 直接输入react的组件才行,加上插件后主要就是在 RecursiveComp
这个函数就行拦截,例如我定义一个type属性,当type等于timepicker这个字符串时,那么我就解析成这个字符串对应的组件,然后插件可以定义成一个数组的形式
类似于下面的代码, 这里不多说了,本质是解析到对应的string
在 组件中 转化成对应key的 组件就好了。然后使用呢,就是直接 <Comp data={你的json数据}>
就可以直接解析成你想要的结构了
import { Collapse,CollapsePanelScheme, } from "./Collapse";
import Form from "./FormSetting"
import Cascader from "./CascaderSetting"
import CheckBox from "./CheckBoxSetting"
import ColorPicker from "./ColorPickSetting"
import InputNumber from "./InputNumberSetting"
import Input from "./InputSetting"
import Radio from "./RadioSetting"
import Select from "./SelectSetting"
import Switch from "./SwitchSetting"
import Slider from "./SliderSetting"
import DatePicker from "./DatePickerSetting"
import TimePicker from "./TimePickSetting"
import Operate from "./OperateSetting"
import Rate from "./RateSetting";
import PreviewSetting from "./PreviewSetting";
export const createSchemaField = (prop)=>{
return prop
}
export const SchemaFieldComp = createSchemaField({
components:{
Collapse,
CollapsePanelScheme,
Form,
Cascader,
ColorPicker,
InputNumber,
Input,
Rate,
Radio,
Select,
Switch,
Slider,
DatePicker,
CheckBox,
TimePicker,Operate,PreviewSetting
}
})
功能2:表单插件扩展
有了上面的 递归主组件奠定基调,那么扩展插件就显得很容易了,我们只需要在 <Comp>
这个函数中添加 一个自定义的 插件数组,并且在 这个函数中定义插件数组和本来的 插件数据合并即可。插件数据定义如下
const TestComp = ()=>{
return <div style={{marginBottom: "16px"}} key={"959"}>
我是自定义组件:传入customComponent数组可自定义你自己的元素
</div>
}
let customComponent = {
components: {
"test": TestComp,
}
}
内部的融合逻辑也比较直观,直接合并就可以了
let registerComp
if(customComponent){
registerComp = {
...SchemaFieldComp["components"],
...customComponent["components"],
}
}else{
registerComp = {
...SchemaFieldComp["components"],
}
}
最后使用就传入data
和 customComponent
就可以了。
类似于
<Comp data={ParamContextInstance } customComponent={customComponent} />,
功能3:动态设置与增删表单
这一步的难点在于怎么将表单的值传递给各个组件并且能够设置整个表单的值
首先我们可以介绍有限 有限状态机xstate的一种设计思路,也就是 guard
守卫的思想
guard守卫
- 给予每一个子项目能够获取全局数据的能力,和目前更改的数据和获取自身属性的能力
- 并且像他的名字一样作为守卫,如果是false的话,那么这个表单的子项会被隐藏。并且数据结构需要很好的扩展性,这部分可以借鉴formily 字符串形式的函数
- 在表单编辑的时候动态的进行上述的操作
第一个问题是我们怎么让他获取全局数据。首先可以用到antd内部的useForm方法,这个方法可以拿到表单实例,然后通过ref层层递归传递即可,这部分不需要插件做任何事情,由咱们定义即可。
第二个问题是关于guard数据结构设计的问题,可以定义这个数据结构是字符串形式的函数,这个函数实例如下,item
表示当前表单子项的属性,value
表示目前正在改变的值, form
表示咱们第一步层层传递的 form 实例
下面是示例
{
guard: `({item,value,form})=>
{
const itemContainer = form.getFieldsValue()
}`
}
接下来是第三个问题,如何在 在表单编辑的时候动态的进行上述的操作呢,我们可以做一个发布订阅者模式,监听form表单的onchange行为,然后动态的在 递归的每一个组件中 接收form表单onchange的广播,组件中实施 guard
的所有行为
好,行文至此这个前端dsl的思路也就大概完成了,这个思路被用于我公司的一些项目中,找了个时间将核心提炼了出来,痛快