实践 -- 前后端如何统一选择框查询?

前言

在开发中后台系统过程中,经常会出现一个表格上面有众多选项框,如下图所示。往往每一个搜索框后端都要做一个新的接口,前端也需要为了适配后端的接口而重新开发相关组件,那么有没有某种做法能让这些重复劳动简单化呢?另外每个选项框的可选值随着业务的发展,会逐渐增多,这个过程中又如何保证前端代码不需要修改,后端业务上线后,前端能够自动适配?本文将提供一种做法,统一掉类似的查询需求。

设计

按照传统的做法,一个表单一个接口,有些复杂的表头,可能要多个接口一起返回,比如上述查询条件中,状态来源可能来自于后端定义的业务枚举,而供货商通道运营商可能来自于DB表,后端在实现时可能会将其放在一个接口中一起返回给前端,前端拿到后,在分别对应到对应的选项框。遇到第二个表格,则可能需要再增加一个接口应对第二个表格的表头,这个过程中接口数量增加,造成的是前端开发成本的增加,如果出现关联逻辑,则前端还要做选择框关联,会更加麻烦,那么设计的第一步就是统一接口,这种业务全局只有一个接口。

统一接口

选择框对应的值可以抽象成成是一个{"id":"传值使用","name":"展示描述"}的object对象,而前端调用无非两种情况,1:给定一个选择框,告诉其该选择框下有哪些值可选。2:联动场景下,选择框1假设选择了A,然后选择框2会联动选择框1进行筛选。因此可以抽象为以下接口。

1. 请求接口

1
2
3
4
5
6
{
"keys":["选择框A","选择框B"],
"selected":{
"选择框A":["值1","值2"]
}
}

其中keys,是描述前端要获取哪些值,该值由后端定义,可以为后端的一个枚举类。
selected字段为当前哪些选项卡已经被选择,后端根据已经选择的值,直接做关联查询,筛选,前端就不需要额外的做关联逻辑。

2. 返回接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"选项框A": [
{
"id": "选项1 key",
"name": "选项1 展示字段"
},
{
"id": "选项2 key",
"name": "选项2 展示字段"
}
],
"选项框B": [
{
"id": "选项1 key",
"name": "选项1 展示字段"
},
{
"id": "选项2 key",
"name": "选项2 展示字段"
}
]
}

由于后端已经做了关联筛选,因此接口只需要返回每一个选项的可选值集合。

前端统一组件

接口统一后,对于前端来说就很容易做出一个通用组件,组件的样子大概如下,其中SelectGroup是一个表格的所有搜索内容,ConfigQueryService则是请求API的封装,其参数是selectGroup发生变化时,主动传递过来。
为了加快响应速度,ConfigQueryService需要做一定的触发限流,以及key维度的缓存配置,否则每次变动都请求接口,这个耗时有点无法接受。

举个例子,以开局的图为例,当进入该页面后,SearchGroup会调用ConfigQueryService发出如下请求

1
2
3
{
"keys": ["state","source","channel","operator"]
}

后端收到后,则返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"state": [
{
"id": "选项1 key",
"name": "选项1 展示字段"
},
{
"id": "选项2 key",
"name": "选项2 展示字段"
}
],
"source": [
{
"id": "选项1 key",
"name": "选项1 展示字段"
},
{
"id": "选项2 key",
"name": "选项2 展示字段"
}
]
。。。
}

如果存在关联选项框,则可以增加个属性,代表选项变化时,发出事件,重新刷新数据。例如当供应商渠道发生变化后,操作人也需要跟着变化,就可以发出如下请求,后端根据channel已经选择了A,能够自动进行过滤,返回符合的operator值选项,这样前端无需关心关联逻辑,该层逻辑完全被后端吃掉。

1
2
3
4
{
"keys": ["state","source","channel","operator"],
"selected":{"channel":["A"]}
}

那么最大的问题,性能该如何解决?对于选项框,绝大多数都是无需关联的,对于这种前端可以在key维度上做个缓存,比如key上增加个shouldInitCache属性,代表可以在启动时就获取数据到内存,那么这一部分就不需要请求接口,直接命中缓存。对于关联类型的
选择框,除了加缓存,目前没想到特别好的处理方式。

后端统一组件

该设计增加了后端的复杂性,因此后端需要一套良好的框架让每一个值之间水平分布,扩展新的值也只是增加一个新的实现类而已,那么该如何设计呢?

首先根据以上描述,可以简单理出一个后端流程图,从流程图中可以得出,后端获取到入参后,会根据选项框key做一个并行分发逻辑,然后统一获取到结果后,再返回给前端,这个流程很简单。

为了避免贴大量代码,这里逻辑尽量用图来表示,如下图所示,后端首先使用策略模式把不同的选择框获取值逻辑进行分离,即接口ConfigKeyQueryStrategy,该接口入参为Map<ConfigKeyEnum, Set<String>> selected即已经选择的值,每一个选择框是能感知其与哪些框进行关联,因此当关联时,该选项框实现策略就能获取到所关联的值,从而进行过滤。然后在ConfigKeyQueryService中做路由,即该接口要感知到所有的策略,如果项目是Spring,则可以在项目启动时直接根据接口把对应的策略实现类全部获取到。另外分离之后由于逻辑没交集,所以策略之间是可以并行运行,因此路由可以按照下面逻辑来实现。

1
2
3
4
SearchResult result = req.getKeys()
.parallelStream()
.map(x -> new Pair<>(x, CACHE_BEAN.get(x).search(req.getSelected())))
.collect(SearchResult::new, (l, x) -> l.put(x.getKey(), x.getValue()), SearchResult::merge);

该方案最大的优势是ConfigKeyQueryStrategy接口被水平管理起来,每一个选项框查询逻辑自己来控制,如果是枚举类,则直接返回Enum.values(),如果需要去DB查询,则调用对应DAO,如果是不经常改变的数据,则可以加一层缓存,每一个选项框之间水平拆分,逻辑清晰简单,扩展则仅仅需要增加新的ConfigKeyQueryStrategy实现类。

实现mock逻辑
如果一个选项框值获取逻辑没定下来,则可以先mock,后端可以在ConfigKeyQueryService层中实现该层逻辑,实现方案有很多,举个例子,定义ConfigKeyEnum后,使用自定义mock注解,然后ConfigKeyQueryService中优先找对应的ConfigKeyQueryStrategy,如果未发现该实现类,则判断是否有注解修饰,存在注解,则去寻找对应mock文件,返回即可。

问题点

这套设计下,前端为统一组件逻辑,后端则每一个选项被水平管理,代码上是很灵活了。但是从用户体验角度出发,遇到关联选项时,每选择一个选项值,都需要发一次请求判断下次需要选择什么,网络耗时可能会造成用户体验的卡顿。当然从优化角度,前端加缓存,后端也需要加缓存,会尽可能的加速网络请求。针对中后台管理系统,使用这种方式需要一个权衡。

MySQL -- 索引指南
实践 -- Velocity渲染SQL如何避免注入?