万能表单的设计
说先说明下:我的设计是面向内部用户的,我想的更多的是:如何在多租户场景下,开发人员如何快速开发配置表单。至于用户使用的简易版,本身是开发人员使用的版本的一个子集,可以在思路成型后进行设计。
我最想解决的问题是:在已经具备相关组件后,前端能够零代码实现一个表单的渲染。
我觉得这个问题不能从属性的角度开始思考,实现一个表单是复杂的,表单中不单单是简单的文本输入框,还可能存在拥有复杂联动关系的组件,最简单的案例就是二级联动的下拉框。二级联动的下拉框,每个框的输入值都为我们需要的属性的值,我们现在的设计方案如何描述这种约束,如何解决这个需求呢?从属性的角度去设计,我没有找到比较好的方案。
我的思路
我提出来的解决问题的思路是什么呢,是组件级别的复用。说明白我的设计思路前,需要先谈谈我对组件的理解。我认为组件就是一段交互逻辑,我们没有办法避免产品设计出新的交互逻辑,但是我们需要想办法提高这些交互逻辑的复用性。
如何提交复用性呢,将数据和逻辑剥离。前端在设计一个表单的时候,是没有任何业务假设的,他只知道自己要什么样的数据,渲染出什么样的结果。比如二级联动的下拉框,在开发这个组件的时候,前端只知道自己需要一个List、一个Map,至于这两个集合来自哪儿,是无所谓的,可能来自服务器,可能是用户提供的固定List。前端拿到这两个集合,他就知道了当用户选择了第一个框的时候,第二个框的预选值就确认了。
在开发一个组件的时候,是有可能存在对表单中其他字段依赖的,比如开发一个动态渲染某些字段的表单,他会根据用户选择的类目,渲染出不同的表单出来。我们也需要将这些数据剥离出来,前端告诉组件的使用着,你需要提供这个字段,组件的使用者会指定一个表单中已经存在的字段,交给组件使用(我会在我的案例中说明这个问题)。
目前我就总结出了三种数据源:一是渲染字段的名字,这个需要我们绑定我们的属性;二是表单中的其他字段;三是从服务器获取的值。我认为为一个表单提供这三种数据,就能够实现交互逻辑的复用。
我设计的案例(Version 1)
我举了一个二级联动组件的案例:这个需求是这样的,用户需要在其他组件中选择一个品牌,我们的组件需要根据这个品牌所属的国家,然后在一个二级组件中选择这个品牌所在地(这个案例是专门设计的,不代表真实存在这个需求)
于是前端开发了一个二级联动的组件,他将这个组件的描述(这个描述不是最终版,是方便讲解版):
组件ID:
COMPONENT_001
组件属性:
province
city
组件表单上下文输入:
brand
组件服务器输入:
服务器输入一:
输入名称:brand_info
请求参数:brand
返回值格式:{
"country":"CountryName"
}
服务器输入二:
输入名称:province_infos
请求参数:country
返回值格式:{
["AProvince","BProvince","CProvince","DProvince"]
}
服务器输入三:
输入名称:city_infos
请求参数:province
返回值格式:{
[
"AProvince":[],
"BProvince":[],
"CProvince":[],
"DProvince":[]
}
前端怎么使用他索取的这些数据呢?他首先会将用拿到的属性渲染出两个文本输入框,然后监听表单中brand值的变化,一旦发生了变化,就使用brand_info配置的请求,去服务器获取当前brand所属的国家(当然,在实现上服务端可以选择当焦点在第一个文本框上时再去获取这些信息),然后再利用返回的值,调用第二个请求获取到省份信息,然后渲染第一个文本框的下拉框。在第二个文本框的值被选定后,前端又会用第一个文本框的值,调用第三个请求,去获取省份信息。
这种实现需要多次与服务端交互,肯定不是最佳的实现方案,但是这个是完全取决于前端怎么实现自己组件的。
现在我们在做表单编排的时候,我们又该怎么做呢?我们提供如下一份文件,就能够指导页面渲染出该组件。
组件ID:
COMPONENT_001
组件属性:
province:(绑定到我们属性库中的province属性)
city:(绑定到我们属性库中的city属性)
组件表单上下文输入:
brand:context.brand(context代表表单上下文)
组件服务器输入:
brand_info:anta-material/getBrandInfo
province_infos:anta-material/getProvinceInfos
city_infos:anta-material/getCityInfoMap
当然,服务端是需要开发自己的Controller的,而且这些Controller接受的参数必须为组件要求的参数。
Okay,经过上面的工作,我们已经能够指导我们的页面渲染出一个我们想要的组件了,这个组件做逻辑时需要的所有数据我们也提供给他了。这个组件在提交数据时也会根据我们绑定的属性,提交到我们指定字段上(我其实在这块还有一些别的设计,但是不太成熟,所以就不提了)。
我设计的案例(Version 2)
上面的案例因为命名的原因导致这个组件用在其他场景并不是那么好理解,我们对它进行如下调整:
组件ID:
COMPONENT_001
组件属性:
property1
property2
组件表单上下文输入:
in1
组件服务器输入:
服务器输入一:
输入名称:context_info_url
请求参数:context_param
返回值格式:{
"context_return":"ContextReturn"
}
服务器输入二:
输入名称:first_inputbox_url
请求参数:context_info_url_return
返回值格式:{
["AAA","BBB","CCC","DDD"]
}
服务器输入三:
输入名称:second_inputbox_url
请求参数:first_input
返回值格式:{
[
"AAA":[],
"BBB":[],
"CCC":[],
"DDD":[]
}
调整后可能不是太好理解,所以前端需要提供充分的文档说明。类似于后端的YAPI,前端也需要组件库之类的东西,用于展示自己的组件,这个后面会谈到。
接下来我们演示两种不同场景中使用该组件,第一个还是刚才的案例:
组件ID:
COMPONENT_001
组件属性:
property1:(绑定到我们属性库中的province属性)
property2:(绑定到我们属性库中的city属性)
组件表单上下文输入:
in1:context.brand(context代表表单上下文)
组件服务器输入:
context_info_url:anta-material/getBrandInfo
first_inputbox_url:anta-material/getProvinceInfos
second_inputbox_url:anta-material/getCityInfoMap
第二个案例的需求为:我们需要一个二级联动组件,这个组件需要根据之前组件选择的性别信息来为当前用户选择居住的楼层及房间(我随便编的,为了套这个组件)。
组件ID:
COMPONENT_001
组件属性:
property1:(绑定到我们属性库中的floor属性)
property2:(绑定到我们属性库中的room属性)
组件表单上下文输入:
in1:context.sex(context代表表单上下文)
组件服务器输入:
context_info_url:(我们不提供这个值,需要组件支持不提供该值)
first_inputbox_url:anta-material/getFloorInfo
second_inputbox_url:anta-material/getCityInfo
这个需求在使用这个组件的时候,需要充分了解这个组件如果不提供context_info_url时是如何处理的。嗯,我问过开发,开发说如果不传递context_info_url值的话,他会将in1获取的值当做first_inputbox_url请求参数。
我的期待
-
如果这个方案可行,我希望我们能有个组件库的东西。在组件库里,前端开发的组件先通过mock接口跑起来,然后配上该组件输入信息的描述,当然还可以带上一些其他功能,比如开发将自己生成的数据放在这块来试下组件能跑起来不。总之组件库是肯定有存在必要性的。
-
如果可行,我们也开发一个表单编排器,拖一拖拽一拽就能搞出一张表单。组件的输入需求,我们可以用数据源、表单上下文的概念呈现给用户,至于属性,就是我们的属性库中的属性。
哪些好处
-
前端将注意力集中于组件的开发、管理,而不是一整个表单的。
-
开发流程上前端开发组件、自己提供mock接口测试,后端开发接口,然后进行测试,当一个组件变得非预期的时候,可能很快的知道问题出现在哪。
我暂时就想到这些。
哪些不足
-
样式的问题,如果这个方案实施,可能我们需要统一我们表单的页面的样式,这样组件适用性就比较强。如果开发组件的时候,将样式也剥离出来,或许也会有些别的收获。
-
关于属性关系部分,我还没有花功夫去设计。如何与表单编排绑定,我也没去设计这些细节。
-
前端目前的框架可能实现不了目前的这些需求,因为这套方案基本设计了一套自己的数据结构。前端可能需要开发表单渲染框架。
-
后端也是有付出的,之前有针对性开发组件的时候,后端可以找一个现成的结构,直接甩给前端,现在不行了,前端成主导了,我们需要针对前端提出的数据结构需求出数据。
-
还有很多细节需要设计,这些设计关于到前后端开发万能表单的规范。
-
还需要继续收集需求,确保方案能覆盖全部的需求。