前言
- 常网IT戳我呀!
- 常网IT源码上线啦!
- 本篇录入技术选型专栏,希望能祝君拿下Offer一臂之力,各位看官感兴趣可移步🚶。
- 有人说面试造火箭,进去拧螺丝;其实个人觉得问的问题是项目中涉及的点 || 热门的技术栈都是很好的面试体验,不要是旁门左道冷门的知识,实际上并不会用到的。
- 最近在做低代码平台相关的,想分享一些自己在项目中遇到的场景。
鲁迅曾经说过:中国人的性情总是喜欢调和折中的。
如果你说这屋子太暗,要在这里开一个天窗,大家一定是不允许的。但如果你主张拆掉屋顶,他们就会来调和,反而同意拆掉天窗。
仔细看下去,相信能收获一些干货滴~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
一、问题剖析
花瓣似乎在说:就是因为她对你已经没有了爱的感觉,所以才要去爱她。
正当我沉迷于幻想中,实习生小黑🙋喊道:前端大佬,有一个需求。
我说🙋🏻♂️:什么时候要?
小白🙋:别问,问就是今晚!
🙋🏻♂️真是的,慌慌张张的,说吧~
需求是这样子的:
有一个表单,有
- 下拉框A,选项值为A1和A2
- 下拉框B,选项值为B1和B2
- 下拉框C,选项值为C1和C2
现在需要做关联关系的约束,不然在填报的时候 选项太多容易搞错。
比如通过低代码配置下拉框A,选了A1之后,触发下拉框B的选项值改变
下拉框B,选了B1之后,触发下拉框C的选项值改变。
二、背景
此需求建立在之前写过的
jsx组件上的。
三、实现思路
下拉选项选择 A,约束后面的可选择项。
change
首先,我们先去找到下拉框的改变事件change。
配置平台
那么,前面我们提到,需要有个地方配置表单的选项关联。
在我们的系统中有:可视化JSON配置平台
定义格式
下拉选项选择 A,约束后面的可选择项。
那么,我们来定义一下怎么配置比较好😘。
我们先看看原本的表单配置
// 第一个表单的字段
{
"config": {
"label": "A",
"tag": "el-select", // 表名下拉框,jsx动态生成组件类型
"controlType": "radio",
},
"slot": {
// 下拉框的选项
"options": [
{
"label": "A1",
"value": "A1"
},
{
"label": "A2",
"value": "A2"
},
],
"prepend": "",
"append": ""
},
"multiple": false, // 是否多选
"id": "S_1_5", // id
},
// 第二个表单的字段
{
"config": {
"label": "B",
"tag": "el-select",
"controlType": "radio",
},
"slot": {
"options": [
{
"label": "B1",
"value": "B1"
},
{
"label": "B2",
"value": "B2"
}
],
"prepend": "",
"append": ""
},
"multiple": false,
"id": "S_1_6",
},
// 第三个表单的字段
{
"config": {
"label": "C",
"tag": "el-select",
"controlType": "radio"
},
"slot": {
"options": [
{
"label": "C1",
"value": "C1"
},
{
"label": "C2",
"value": "C2"
}
],
"prepend": "",
"append": ""
},
"multiple": false,
"id": "S_1_7",
},
我们在原本的基础上添加dataControl数组属性。配置的是多个对象,意味着他的值多个,关联多个id。
- srcFormFieldKey:关联的id
- currentVal:假如当前的选项的值为A1,那么将以下的options传送给srcFormFieldKey的options
- options:选项
dataControl": [
{
"srcFormFieldKey": "S_1_6", // 关联的id
// 假如当前的选项的值为A1,那么将以下的options传送给srcFormFieldKey的options
"currentVal": "A1",
"options": [
{
"label": "A1哈",
"value": "A1哈"
},
{
"label": "A1啊",
"value": "A1啊"
}
]
},
// 配置的是多个对象,意味着他的值多个,关联多个id
{
"srcFormFieldKey": "S_1_6",
"currentVal": "A2",
"options": [
{
"label": "A2哈",
"value": "A2哈"
},
{
"label": "A2啊",
"value": "A2啊"
}
]
},
]
数据格式已定义好,接下来看看代码怎么实现。
四、代码实现
4.1 初步版本
我们先看看change返回的data有什么。
首先看看当前的字段有没有配置dataControl属性,有则进入if
我们说道dataControl配置的数组,里面有多个对象,那么我们遍历一下😊。
还记得我们配置的srcFormFieldKey,关联的id,去找整个表单的关联id,标记为f
直接把关联的options,赋值给,我们找到的id字段。
初步版本就做好了。
change(data){
const dataControl = getV(data, 'config', 'config', 'dataControl')
// 关联关系,通过下拉选项选择 A,约束后面的可选择项
if(getV(dataControl, 'length')){
for (let index = 0; index < dataControl.length; index++) {
const element = dataControl[index];
const f = this.fields.find(f => f.id == element.srcFormFieldKey && getV(f, 'slot', 'options'))
if(f.id == element.srcFormFieldKey && getV(f, 'slot', 'options')){
f.slot.options = element.options; // update config
}
}
}
},
初步版本就做好了。
目前支持字段A配置了当前值为A1,把A1里面的option赋值给字段B。
B配置了当前的值为B1,把B1里面的option赋值给字段C
看到这里的你,已经很棒啦
4.2 需求提升
但我们的需求没那么简单。
步骤:
- A字段当前的值选了A1,把A1的option赋值给B字段
- B字段当前的值选了B1,把B1的option赋值给C字段
这个时候,A字段当前的值选了A2,值改变时,分两种情况:
- 第一种:A2有配置关联option把值给B字段
-
- 改变字段B的选项值,清空B当前选的值
- 既然字段B的选项改变,那么字段B关联的字段C的选项恢复初始化的选项,同时清空C当前选的值
- 第二种:A2无关联
-
- 字段B的选项值初始化,清空B当前选的值
- 既然字段B的选项改变,那么字段B关联的字段C的选项恢复初始化的选项,同时清空C当前选的值
4.3 递归
第一眼,想到了什么?
递归思想😊。
因为这是有关联关系,A关联B,B关联C,那么,我们通过字段配置有没有dataControl,去递归查找。
代码如下:
新添加了11行,如果A的当前值改变,则会再次调用change去将B的选项值设置或者清空options
change(data){
const dataControl = getV(data, 'config', 'config', 'dataControl')
// 关联关系,通过下拉选项选择 A,约束后面的可选择项
if(getV(dataControl, 'length')){
for (let index = 0; index < dataControl.length; index++) {
const element = dataControl[index];
const f = this.fields.find(f => f.id == element.srcFormFieldKey && getV(f, 'slot', 'options'))
if(f.id == element.srcFormFieldKey && getV(f, 'slot', 'options')){
f.slot.options = element.options; // update config
(new+) getV(f, 'config', 'dataControl', 'length') && this.change({ config: f }) // 若为空,清空之前的数据
}
}
}
},
4.4 多选
既然是下拉框,我们就还要考虑多选的情况下。
如果你有留意上面的json配置,会发现有multiple属性 😋,true/false控制单选或者多选的属性。
我们来看一下这种场景:
- A里面了当前值为A1,把A1配置的options(A1啊、A1哈)给B的选项
- A里面还配置了:当前值为A2,把A2配置的options(A2啊、A2哈)给B的选项
- 那么,我们当前是多选,同时勾选了A1、A2,意味着B此时的选项是(A1啊、A1哈、A2啊、A2哈)
- 再举个极端的情况,既然选项值会叠加,那么还要考虑重复的情况,需要去重。
所以,我们的值判断要考虑类型(当前选中的值是数组还是字符串)。
const isEqual = (element) => Array.isArray(data.item) ? data.item.includes(element.currentVal) : element.currentVal == data.item
// 当前值和配置的值--是否相等
4.5 options的值
我们说道,要初始化选项值,意味着:我们需要去备份一下选项值,标记options_backup
那么,什么时候备份options呢?
一开始想:判断第一次进入的时候备份options,最后发现不可行,因为每次触发change都相当于第一次。
🤡其实我们只需要判断没有options_backup属性则备份即可。
那么,什么时候将options设置options_backup备份呢?
我一开始想得很复杂。
这两种情况都需要设置初始化备份的选项。
如果按照这样子逻辑写,可行是可行,不是比较费代码。
😊换种思路,只要当前的options和options_backup的值一样,就是第一次勾选,直接把关联的options赋值即可。
什么时候要叠加选项呢?
判断当前的option是否和原始的一样,如果一样就直接赋值(第一次进入);否则,第二次就合并后去重
equals函数:判断数组是否全等
uniqueArray2:去重
isEqual:判断是否值相等
dataControl.filter(c => isEqual(c) ):我们先去找当前字段的dataControl,值相等的选项
- 勾选了A1、A2,意味着B此时的选项是(A1啊、A1哈、A2啊、A2哈)
然后map只要options属性,在flat扁平数组
const opt = equals(f.slot.options, f.slot.options_backup) ?
element.options :
this.uniqueArray2(dataControl.filter(c => isEqual(c) ).map(m => m.options).flat())
// update config
f.slot.options = isEqual(element) ? opt : f.slot.options_backup;
注意一点:如果有找到的话,需要break,因为我们处于for循环中,不需要在去查找其他字段了。
4.6 完整代码
change(data){
const dataControl = getV(data, 'config', 'config', 'dataControl')
// 关联关系,通过下拉选项选择 A,约束后面的可选择项
if(getV(dataControl, 'length')){
let obj = {}
for (let index = 0; index < dataControl.length; index++) {
const element = dataControl[index];
const f = this.fields.find(f => f.id == element.srcFormFieldKey && getV(f, 'slot', 'options'))
if(f.id == element.srcFormFieldKey && getV(f, 'slot', 'options')){
if(!getV(f, 'slot', 'options_backup', 'length')) f.slot.options_backup = JSON.parse(JSON.stringify(f.slot.options)) || [] // backup config
obj[f.id] = `` // update val
const isEqual = (element) => Array.isArray(data.item) ? data.item.includes(element.currentVal) : element.currentVal == data.item // 当前值和配置的值--是否相等
const opt = equals(f.slot.options, f.slot.options_backup) ? element.options : this.uniqueArray2(dataControl.filter(c => isEqual(c) ).map(m => m.options).flat()) // 判断当前的option是否和原始的一样,如果一样就直接赋值(第一次进入);否则,第二次就合并后去重
f.slot.options = isEqual(element) ? opt : f.slot.options_backup; // update config
getV(f, 'config', 'dataControl', 'length') && this.change({ config: f, notUpdateVal: data.notUpdateVal }) // 若为空,清空之前的数据
if(isEqual(element)) break;
}
}
!data.notUpdateVal && this.update(obj)
}
},
涉及到的函数方法
// 根据label和value去重
uniqueArray2(originalArray) {
return originalArray.reduce((accumulator, currentValue) => {
// 判断当前对象是否已经在accumulator中存在
if (!accumulator.some(item => (item.label === currentValue.label && item.value === currentValue.value))) {
accumulator.push(currentValue);
}
return accumulator;
}, [])
},
/**
* @description 全等判断
*
* @param {*} a 目标变量a
* @param {*} b 目标变量b
* @return {Boolean}
* @memberof ArrayTool
* @example
* equals({ a: [2, { e: 3 }], b: [4], c: 'foo' }, { a: [2, { e: 3 }], b: [4], c: 'foo' }); // true
*/
export const equals = (a, b) => {
if (a === b) {
return true
}
if (a instanceof Date && b instanceof Date) {
return a.getTime() === b.getTime()
}
if (!a || !b || (typeof a !== "object" && typeof b !== "object")) {
return a === b
}
if (a.prototype !== b.prototype) {
return false
}
let keys = Object.keys(a)
if (keys.length !== Object.keys(b).length) {
return false
}
return keys.every((k) => equals(a[k], b[k]))
}
其实这些公共的方法,在我的github上有写过:ArrayTool
后记
这种类似的场景一般用于可视化低代码平台中。
会根据配置来动态渲染。
做好扩展性,遵守开闭原则。
当然,我们还要考虑安全性防XSS注入,防止外部嵌入,导致安全问题。
如果有其他更好的方法也欢迎评论区见,这里提供的只是诸多方法之一。
最后,祝君能拿下满意的offer。
我是Dignity_呱,来交个朋友呀,有朋自远方来,不亦乐乎呀!深夜末班车
👍 如果对您有帮助,您的点赞是我前进的润滑剂。