图片起源:https://unsplash.com/
本文作者:董健华
1. 背景
云音乐 B 端业务场景十分多,B 端业务绝对于 C 端业务产品生命周期更长而且更重视场景的的梳理。很多时候开发 B 端业务都是拷贝之前的代码,这样减少了很多反复而且干燥的工作量。
中后盾零碎其实能够拆分成几个比拟通用的场景:表单、表格、图表,其中表单波及到联动、校验、布局等简单场景,常常是开发者的须要消耗精力去解决的点。
比照传统的 Ant Design 表单开发开发方式,咱们认为有以下问题:
- 首先代码无奈被序列化,而且对于一些非前端的开发者更习惯用
JSON
形式形容表单,因为足够简略 - 表单的校验并没有和校验状态做联合
onChange
实现的联动形式在简单的联动状况下代码会变得难以保护,容易产生很多链表式的逻辑- 表单有许多互斥的状态能够整顿,而且咱们也心愿用户能够很轻易的在这些状态间进行切换
- 对于一些比拟罕用而且通用的场景,例如:表单列表,也能够抽离出一套可行的计划
所以尽管传统的表单开发方式曾经足够的灵便,然而我也仍然认为表单还有优化的空间,在灵便与效率上做了些衡量。
外界也有比拟成熟的表单解决方案,例如:Formliy、FormRender。尽管解决了下面某几个点的问题,然而仍然不够全面,咱们须要有本人 style
的计划。
所以为了进步中后盾开发效率,让前端可能把工夫投入到更有意义的事件里,咱们总结了一套面向简单场景的表单解决方案。
2. 技术计划
在技术计划上至关重要的一环就是 Schema 设计,框架架构等工作都是围绕这一环去实现的,所以我会因循这个思路给大家做介绍。
2.1 Schema 设计
表单计划基于 Ant Design
开发,通过 JSON
形式配置 Schema,然而并非是 JSON Schema
,外界很多基于 JSON Schema
的配置计划,其实也有思考过,不过 JSON Schema
写起来有点麻烦,所以对 JSON Schema
的转换只作为一项附加的能力。
案例如上面代码所示,最简略的表单字段只有配置 key
、type
和 ui.label
就能够了:
const schema = [
{
"key": "name",
"type": "Input",
"ui": {"label": "姓名"}
},
{
"key": "age",
"type": "InputNumber",
"ui": {"label": "年龄"},
"props": {"placeholder": "请输出年龄"}
},
{
"key": "gender",
"type": "Radio",
"value": "male",
"ui": {"label": "性别"},
"options": [
{
"name": "男",
"value": "male"
},
{
"name": "女",
"value": "female"
}
]
}
];
export default function () {const formRef = useRef(null);
const onSubmit = () => {formRef.current.submit().then((data: any) => {console.log(data);
});
};
const onReset = () => {formRef.current.reset();
};
return (
<>
<XForm
ref={formRef}
schema={schema}
labelCol={{span: 6}}
wrapperCol={{span: 12}}
/>
<div>
<Button type="primary" onClick={onSubmit}> 提交 </Button>
<Button onClick={onReset}> 重置 </Button>
</div>
</>
);
}
因为计划是基于 Ant Design
的 Form
组件设计的,所以为了保留 Ant Design
的一些个性,设计了 ui
和 props
两个字段别离对应 Form.Item
的 props
和组件的 props
。即便后续 Ant Design
表单减少了某些性能或者个性,这套表单计划也能做到无缝反对。
2.1.1 校验形式
既然表单是基于 Ant Design
实现的,那么校验也沿用了它的校验类库 async-validator,这个类库曾经比拟成熟而且弱小,可能校验 Array
和 Object
等深层级的数据类型,满足简单校验的需要,所以咱们间接在这个库的根底上做调整。
通过 rules
字段进行配置,除了 async-validator
原本就就有的个性外,还额定减少了 status
(校验状态)和 trigger
(触发条件)枚举如下:
-
status:校验状态
- error(默认):谬误
- warning:正告
-
trigger:触发条件
- submit(默认):提交时候触发
- change:值变动时候触发判断
- blur:失去焦点时候触发判断
根本应用形式如下:
{
"key": "name",
"type": "Input",
"ui": {"label": "姓名"},
"rules": [
{
"required": true,
"message": "姓名必填",
"trigger": "blur",
"status": "error"
}
]
}
2.1.2 联动形式
除了校验,联动也是比拟罕用的性能,传统的联动通过组件 onChange
形式实现,当联动逻辑比较复杂的时候,看代码就像搜寻链表一样麻烦,所以这块设计了一种 反向监听
的形式,字段的所有变动都保护在字段配置自身,升高前期保护老本。
通过 listeners
字段进行配置,设计了 watch
(监听)、condition
(条件)、set
(设置)三个字段组合实现联动性能。
watch
记录须要监听的字段,当监听字段有任何变动的时候,会触发 condition
条件的判断,只有条件判断通过才会接着触发 set
设置。
[
{
"key": "name",
"type": "Input"
},
{
"key": "gender",
"type": "Radio",
"value": "male",
"options": [
{
"name": "男",
"value": "male"
},
{
"name": "女",
"value": "female"
}
],
"listeners": [
{"watch": [ "name"],
"condition": "name.value ==='Marry'","set": {"value":"female"}
}
]
}
]
上述例子当名字为 Marry 的时候,性别默认调整成女。
2.1.3 表单状态
咱们发现有些联动场景是为了对字段做暗藏和显示的操作,为了不便用户切换状态,将 4 种互斥表单状态整顿成一个 status
字段:
-
status:状态
- edit(默认):编辑
- disabled:禁用
- preview:预览
- hidden:暗藏
preview
状态并不是组件自身具备的,然而预览的需要蛮多的,于是咱们做了拓展,为所有根本的表单组件预置了预览的状态。即便自定义组件也会默认展现字段值,如果须要自行处理的话也提供了计划。
应用形式如下:
[
{
"key": "edit",
"type": "Input",
"status": "edit",
"value": "编辑",
"ui": {"label": "编辑"}
},
{
"key": "disabled",
"type": "Input",
"status": "disabled",
"value": "禁用",
"ui": {"label": "禁用"}
},
{
"key": "preview",
"type": "Input",
"status": "preview",
"value": "预览",
"ui": {"label": "预览"}
},
{
"key": "hidden",
"type": "Input",
"status": "hidden",
"value": "暗藏",
"ui": {"label": "暗藏"}
}
]
效果图如下:
2.1.4 Options 设置
许多抉择组件应用 options
字段设置选项,选项有时候通过异步接口获取。思考到异步接口的状况,设计了 4 套计划:
options
为Array
的状况
{
"key": "type",
"type": "Select",
"options": [
{
"name": "蔬菜",
"value": "vegetables"
},
{
"name": "水果",
"value": "fruit"
}
]
}
options
为string
的状况,即接口链接
{
"key": "type",
"type": "Select",
"options": "//api.test.com/getList"
}
options
为object
的状况,action
为接口链接,nameProperty
配置name
字段,valueProperty
配置value
字段,path
为获取选项门路,watch
配置监听字段
{
"key": "type",
"type": "Select",
"options": {"action": "//api.test.com/getList?name=${name.value}",
"nameProperty": "label",
"valueProperty": "value",
"path": "data.list",
"watch": ["name"]
}
}
action
为function
的状况
{
"key": "type",
"type": "Select",
"options": {"action": (field, form) => {return fetch('//api.test.com/getList')
.then(res => res.json());
},
"watch": ["name"]
}
}
2.1.5 表单列表
表单列表是一种组合类型的表单,通常有 Table
和 Card
两种场景,具备减少和删除性能。
这种类型的表单值是以 Array
的模式返回的,所以设计了 Array
组件,依据 props.type
对 Table
和 Card
状态进行切换(貌似这种状况不多),children
配置子表单,应用形式如下:
{
"key": "array",
"type": "Array",
"ui": {"label": "表单列表"},
"props": {"type": "Card"},
"children": [
{
"key": "name",
"type": "Input",
"ui": {"label": "姓名"}
},
{
"key": "age",
"type": "InputNumber",
"ui": {"label": "年龄"}
},
{
"key": "gender",
"type": "Radio",
"ui": {"label": "性别"},
"options": [
{
"name": "男",
"value": "male"
},
{
"name": "女",
"value": "female"
}
]
}
]
}
效果图如下:
2.2 框架架构
围绕 Schema 设计思路,咱们采纳了基于分布式治理计划,将核心层和渲染层拆散,字段信息保护在核心层,渲染层只负责渲染的工作,做到数据和界面代码的拆散构造。
核心层与渲染层之间通过 Sub/Pub
形式进行通信,渲染层通过监听核心层定义的一系列 Event
事件对界面作出调整。
这种数据状态的扭转驱动界面的变动曾经不是什么新鲜事了,在大多数框架中被宽泛应用,其中劣势有:
- 方面各个字段之间数据与状态共享
- 通过对事件的管制,可能正当的优化渲染次数,进步性能
- 可能适配多框架的状况,只需复用一套核心层代码
核心层次要由 Form
、Field
、ListenerManager
、Validator
、optionManager
几局部组成如下图所示:
其中 Form
是表单原型,上面承载了很多 Field
字段原型,由 ListenerManager
对立治理联动方面的性能,Field
下具备 Validator
和 OptionManager
别离治理校验和 options
选项性能
2.2.1 校验实现
次要还是通过 async-validator
类库实现,然而仍然无奈满足多校验状态和多触发条件的状况,所以在这个根底上做了些拓展,封装成一个 Validator
类。
Validator
只有一个 Validator.validate
办法,传递一个 trigger
参数,实例化 Validator
时候会去解析 rules
字段,依据 trigger
进行分类并创立对应的 async-validator
实例。
2.2.2 联动实现
ListenerManager
具备 ListenerManager.add
办法和 ListenerManager.trigger
办法,别离用于解析并增加 listeners
字段以及 Field
字段发生变化时触发联动成果。
具体流程是在初始化 Field
时,会将 listeners
字段通过 listenerManager.add
办法解析信息,依据 watch
中的 key
值进行分类并保留在其中,当 Field
信息发生变化的时候会通过 ListenerManager.trigger
触发联动,判断 condition
条件是否满足,如果满足即触发 set
内容。
2.2.3 表单列表实现
表单列表其实是由多个 XForm
实例形成,每一个自增项都是一个 XForm
实例,所以联动只能在同一行上进行,不能跨行联动。
当点击增加按钮的时候,会依据 children
提供的 Schema
模板创立一个 XForm
实例:
2.2.4 布局实现
除了 Ant Design
的 Form 提供的三种布局形式(horizontal、vertical、inline),还须要提供一种更灵便的布局形式来满足更加简单的状况。
布局真是一个很头疼的问题,特地是 Schema
在相似 JSON
的构造下实现简单的布局很容易导致 Schema
嵌套层级深,这种是咱们不违心看到的。
最后计划是通过网格布局实现,通过设置 Form
的 row.count
或者 col.count
参数计算出网格的行数和列数再对字段进行散布,这种形式只实用于每行列数都统一的状况,然而这种形式难以满足每行列数不统一的状况:
所以从新设计了一个 ui.groupname
的字段,同一个 groupname
的字段都会被一个 div
包裹住,并且 div
的 className
即 groupname
,用户要实现简单的布局能够本人写款式去实现,这样的计划尽管简陋,然而实用。
3. 细节设计
3.1 疏忽特定字段值
有些场景须要疏忽 status
为 hidden
的字段的值,所以设计了一个 ignoreValues
字段,字段配置有上面几种状况:
- hidden:疏忽状态为 hidden 的状况
- preview:疏忽状态为 preview 的状况
- disabled:疏忽状态为 disabled 的状况
- null:疏忽值为 null 的状况
- undefined:疏忽值为 undefined 的状况
- falseLike:疏忽值 == false 的状况
通过配置 ignoreValues
字段,提交后返回的 values
就会疏忽相应的字段:
<XForm schema={schema} ignoreValues={['hidden', 'null']}/>
3.2 字段解构与重组
字段解构是指把一个字段的值拆成多个字段,字段重组是指把多个字段组合成一个字段,这块的具体性能还未实现,然而曾经有了初步的想法。
字段解构例子如下,次要是通过 key
对字段进行拆分,最终返回 values
蕴含 startTime
和 endTime
两个字段:
{"key": "[startTime, endTime]",
"type": "RangePicker",
"ui": {"label": "工夫抉择"}
}
发现许多场景须要由多个字段组合成一个字段,这种状况大多须要写自定义组件不然就是前期须要对数据进行解决,为了简化这一过程所以设计了字段重组的性能。通过 Combine
组件将多个字段重组成一个字段:
{
"key": "time",
"type": "Combine",
"ui": {"label": "工夫抉择"},
"props": {"shape": "{startTime, endTime, type}"
},
"children": [
{
"key": "startTime",
"type": "DatePicker"
},
{
"key": "endTime",
"type": "DatePicker"
},
{
"key": "type",
"type": "Select",
"options": [
{
"name": "发行工夫",
"value": "publishTime"
},
{
"name": "上线工夫",
"value": "onlineTime"
}
]
}
]
}
4. 结尾
欠缺表单这款产品的过程也是一个博采众长的过程,咱们调研了业界竞品联合本身业务需要,开发出了这款产品。下面介绍了表单计划的思路和实现形式供大家参考,十分遗憾的是咱们产品还未开源,置信会在适合的时候跟大家见面。
5. 相干材料
- Formily
- FormRender
本文公布自 网易云音乐大前端团队,文章未经受权禁止任何模式的转载。咱们长年招收前端、iOS、Android,如果你筹备换工作,又恰好喜爱云音乐,那就退出咱们 grp.music-fe(at)corp.netease.com!