实战:低代码的场景-下拉选择关联

381 阅读9分钟

前言

  • 常网IT戳我呀!
  • 常网IT源码上线啦!
  • 本篇录入技术选型专栏,希望能祝君拿下Offer一臂之力,各位看官感兴趣可移步🚶。
  • 有人说面试造火箭,进去拧螺丝;其实个人觉得问的问题是项目中涉及的点 || 热门的技术栈都是很好的面试体验,不要是旁门左道冷门的知识,实际上并不会用到的。
  • 最近在做低代码平台相关的,想分享一些自己在项目中遇到的场景。

鲁迅曾经说过:中国人的性情总是喜欢调和折中的。
如果你说这屋子太暗,要在这里开一个天窗,大家一定是不允许的。但如果你主张拆掉屋顶,他们就会来调和,反而同意拆掉天窗。

1.jpg

仔细看下去,相信能收获一些干货滴~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

一、问题剖析

花瓣似乎在说:就是因为她对你已经没有了爱的感觉,所以才要去爱她。

正当我沉迷于幻想中,实习生小黑🙋喊道:前端大佬,有一个需求。

我说🙋🏻‍♂️:什么时候要?

小白🙋:别问,问就是今晚!

🙋🏻‍♂️真是的,慌慌张张的,说吧~

需求是这样子的:

有一个表单,有

  • 下拉框A,选项值为A1和A2
  • 下拉框B,选项值为B1和B2
  • 下拉框C,选项值为C1和C2

现在需要做关联关系的约束,不然在填报的时候 选项太多容易搞错。

比如通过低代码配置下拉框A,选了A1之后,触发下拉框B的选项值改变

下拉框B,选了B1之后,触发下拉框C的选项值改变。

二、背景

此需求建立在之前写过的

Vue如何高效通过JSX动态渲染组件

jsx组件上的。

三、实现思路

下拉选项选择 A,约束后面的可选择项。

change

首先,我们先去找到下拉框的改变事件change。

配置平台

那么,前面我们提到,需要有个地方配置表单的选项关联。

在我们的系统中有:可视化JSON配置平台

22.jpg

定义格式

下拉选项选择 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

33.jpg

看到这里的你,已经很棒啦

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赋值即可。

44.jpg

什么时候要叠加选项呢?

判断当前的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_呱,来交个朋友呀,有朋自远方来,不亦乐乎呀!深夜末班车

👍 如果对您有帮助,您的点赞是我前进的润滑剂。

以往推荐

靓仔,说一下keep-alive缓存组件后怎么更新及原理?

面试官问我watch和computed的区别以及选择?

面试官问我new Vue阶段做了什么?

前端仔,快把dist部署到Nginx上

多图详解,一次性啃懂原型链(上万字)

Vue-Cli3搭建组件库

Vue实现动态路由(和面试官吹项目亮点)

项目中你不知道的Axios骚操作(手写核心原理、兼容性)

VuePress搭建项目组件文档

原文链接

juejin.cn/spost/73965…