1. 动态表单是什么
在开发表单时常见的有两种做法:一种是通过配置代码的方式,而另一种则是通过组件组合的方式。通过配置代码生成的表单,我们称之为动态表单(Dynamic Form) ,而通过组件组合生成的表单,我们称之为普通表单。
我们先来看下普通表单 的开发流程,如下图所示:
而动态表单 则是一个表单对应一个JSON,所有表单由一份代码(动态表单引擎)进行加载和渲染,开发流程如下图:
2. 动态表单与传统表单比较
在项目中,如果存在大量表单,采用组件组合的方式来实现时,开发者可能会遭遇到UI与业务逻辑的高度耦合。这种现象会使得后续的代码维护变得艰难,影响可拓展性和可读性。同时,开发者很可能会编写大量冗长且重复的代码,这不仅显著延长了开发周期,还增加了后续的维护成本。当需要构建大量相似表单时,手动编写和维护所需的工作量和时间往往显得极为庞大。尤其是在业务和监管需求快速变化的情况下,表单需要随之调整,因而维护的成本愈发高昂。
动态表单对开发者而言,实现了业务逻辑与UI渲染逻辑的有效分离。开发者只需关注数据配置,省去了编写和调整DOM样式的繁琐工作,显著减少了开发时间,提升了开发体验。对于动态表单,我们只需设定一套统一的配置模板,后续新增表单或输入项时,仅需修改配置文件,过程简单而便捷。与传统的组合方式相比,动态表单显著减少了重复的HTML模板代码,使得开发更加高效流畅。
在存在大量表单的项目中,动态表单的优势如下:
- 分离关注点 :
- 动态表单将业务逻辑与UI渲染分离,使得开发者可以专注于数据配置,而不必担心具体的DOM结构和样式。
- 减少冗余代码 :
- 通过统一的配置模板,避免了重复编写相似的HTML代码,从而提高代码的可维护性和可读性。
- 快速迭代 :
- 随着业务需求的变化,只需修改配置文件即可快速新增或修改表单项,大大缩短了开发周期。
- 提高开发效率 :
- 开发者可以利用配置文件快速生成表单,减少了手动编写和维护的时间。
- 灵活性和可扩展性 :
- 可以根据不同的需求动态生成不同的表单,适应多变的业务场景。
通过上面的分析,我们也可以得出在动态表单的应用场景和应该考虑的设计规则。
-
动态表单特别适用于需求数量庞大且相似的场景:输入控件类型相对固定,UI与布局保持统一。这使得定义Schema变得得心应手,通过简单的循环便可直接生成表单。
-
选择动态表单时,不应仅从技术角度做出决策,还需充分考虑业务需求与UI设计。不应仅因几行重复的HTML代码而选择动态表单方案,毕竟某些重复是"必要的";错误的选择反而会降低开发效率。
-
一旦决定采用动态表单方案,在灵活性方面应做出适度的妥协,不再接受千变万化的UI设计。这一点需要开发、业务和设计等多个角色进行良好的沟通与协作。
-
不应通过配置文件设定过于细致的CSS样式。可以封装一些固定的样式,如两列布局或三列布局,然后通过配置项进行切换,以简化操作。
3. 技术方案与技术选型
1. 表单的DSL
表单 DSL 设计的重点是三个方面,如何描述表单的数据结构?如何描述表单项之间的关系?如何描述表单的 UI 样式?
1. 描述数据
对于上述表单,我们通过json Schema来描述。
js
[
{
key: 'username',
},
{
key: 'password',
},
]
我们通过上面的json
描述了一个数据,其包含 username 和 password 属性。
2. 描述样式
设计动态表单的 DSL 除了要描述表单的数据结构之外,还需要描述表单的 UI 样式。如是通过 input 输入框表示,还是用 textarea 多行输入表示;以及是否展示 placeholder 文案;禁用状态 disabled 等。
js
[
{
key: 'username',
type: 'input',
props: {
label: '用户名',
placeholder: '请输入用户名',
},
},
{
key: 'password',
type: 'input',
props: {
label: '密码',
placeholder: '请输入密码',
},
},
]
props
我们用来承载该第三方组件库的参数。
3. 描述关系 - 联动
之所以要描述表单项之间的关系,则是因为在一个表单中,表单项之间的联动操作实在太过于常见了。
表单的联动操作简单理解就是某个表单项的变化会引起另外表单项的变化。比如表单项的某个值会控制另外一个表单项的显隐。如下图当勾选 单选框1时 时,展示 步进器 ,勾选 单选框2 时,展示 评分。
js
[
{
key: 'select',
type: 'radio',
rules: 'required',
value: select,
hidden: !showVirtualForm.value,
props: {
label: '单选框',
},
},
{
key: 'stepper',
type: 'stepper',
value: stepper,
hidden: select.value === 2,
props: {
label: '步进器',
},
},
{
key: 'rate',
type: 'rate',
rules: 'required',
value: rate,
hidden: select.value === 1,
props: {
label: '评分',
},
},
]
我们不在渲染器内部做处理,通过在外部修改json
参数来控制。设置一个 hidden
参数,来控制该项是否隐藏。这样通过响应式
的设计,当监听到数据变化时,重新渲染组件。
另外我们通过 value
参数来传入默认值,通过 rules
参数来设置校验规则。
2. 表单渲染 - 从 DSL 到表单
在这一阶段重点考虑将 DSL 转换为表单页面展示的问题,因为表单 DSL 主要由 JSON Schema 构成,因为它是一套递归的数据结构,所以 DSL 转换成真实渲染的表单实际上是一个组件逐级递归渲染的过程。
分层架构
加载渲染过程
解析流程从遍历 Schema JSON 开始,先读取顶级的属性 model。利用 model 生成模型。接下来,就是利用渲染器对节点进行渲染,如在vue中,通过读取单个表单类型,即读取 JSON 的 type
对应的真实组件,通过 vue 动态组件来完成该组件的渲染,并将 props 信息传入该组件中,由此,就可以渲染出单个表单组件。children 的渲染亦是如此,将 children 渲染完之后,作为上一层组件的子组件完成渲染。
另外,我们通过将json包裹为响应式形式,当外界响应式变量变化时,触发界面的更新,从而最终实现联动效果。
渲染器架构
整个渲染器架构如下图所示:
我们的物料为插件化设计,可以自定义组件库,如在移动端中使用antd
、vant
等组件库。 并且根据我们的业务来开发组件库。通过插件形式导入到物料库中。
表单form管理与校验
我们通过 VeeValidate 来生成表单数据,并且借助 VeeValidate 的校验能力,可以很好的实现数据校验功能。
提交表单
这里考虑了两种实现方案:
- 将提交逻辑放在组件外,通过响应式设计,当表单内容改变时,透出表单提交信息给外层。
- 通过将提交事件通过 放入 events 中处理,如下
js
[
{
key: 'rate',
type: 'rate',
rules: 'required',
value: rate,
hidden: select.value === 1,
props: {
label: '评分',
},
},
events: {
'on-change': (formInfo) => {
// submit()
}
},
]
4. 待完善
1. 动态语法
是否可以在json中通过动态语法 或动态表达式的形式来实现表单的动态联动。
2. 插槽设计
通过插槽形式使开发者可以更灵活的开发。