可视化编辑器(数据映射)

589 阅读4分钟

前言

最近接到一个需求就是给echarts可视化编辑器增加动态请求配置项。其中包含了数据源映射功能,毕竟第三方接口返回的数据格式是不固定的。查了一圈资料也没找到我想要的,接下来就来分享一下我的实现方式,希望对遇到这种需求的同学有所帮助
ce6f9fabbe6e457b9d8a54d822d95a1a~tplv-73owjymdk6-jj-mark_0_0_0_0_q75.png

功能分析

在需求开始前,我们需要针对可能遇到的问题进行分析,如下是我们所考虑到场景问题

  1. 数据映射面板应该是如何设计
    如下是我们的模版数据,fields代表echarts需要的数据项
    • 产品名称: fields第一项表示图表的横轴
    • 数据1: 表示柱状轴1
    • 数据2: 表示柱状轴2,以此类推后面的所有项都是柱状标识
{
    fields: ['产品名称', '数据1', '数据2'],
    values: [
        ['Mon', 120, 130],
        ['Tue', 200, 130],
        ['Wed', 150, 312],
        ['Thu', 80, 268],
        ['Fri', 70, 155],
        ['Sat', 110, 117],
        ['Sun', 130, 160],
    ],
}

如下可以看出values第一列就是横轴数据,第二列就是蓝色轴数据,第三列就是绿色数据, image.png

因为我们的模版数据是可以通过导入json的形式,例如我们可以追加数据3。因此我这边就没有涉及追加fileds按钮了。如下除了标准字段映射字段列是必要的,其他可根据需求来追加。 image.png

  1. 映射字段需要展示怎样的数据,以及如何转换第三方提供的数据
    在映射字段列表中我们需要展示的是第三方提供的数据的属性名称。针对属性列表生成规则分成如下三种形式基础数据类型对象数据类型数组数据类型。我们从上图可以看出映射字段列表展示的就是第三方提供数据的属性名称
  • 基础数据类型 image.png
  • 对象数据类型 image.png
  • 数组数据类型
    数组类型比较特殊,数组存在基础类型数组(字符串数据)对象类型数组(对象数组)多维数组(二维、三维数组),对于递归获取属性,去追究数组小标索引是没有意义的,毕竟可能每次返回的数组长度都与上一次不一致

基础类型数组不会再继续递归内部索引类型key:value就直接是当前数组属性名称 image.png

对象类型数组需要继续递归获取属性直到值为基础类型 image.png 多维数组类型不会再继续递归内部索引类型key:value就直接是当前数组属性名称 image.png

  1. 模版字段与第三方属性如何建立映射关系
    通过配置面板,选择对应的映射字段,我们就可以得出一个属性链 image.png

  2. 通过映射规则转换第三方数据
    第三数据 -> 映射规则 -> 最终出码数据 -> 验证数据合法性 image.png

具体实现

经过上一节我们了解了数据映射的具体流程,接下就来看看如何通过代码来实现

  1. 生成映射属性列表
/** 第三方数据转换为对象tree */
const thirdDataToTree = data => {
    const tree = {}
    function runTree(data, tree) {
        for (const key in data) {
            if (typeof data[key] !== 'object' || data[key] == null) {
                // $ 基础数据类型的情况
                tree[key] = key
            } else if (Array.isArray(data[key])) {
                // $ 数组的情况
                const arr = data[key]
                const isBaseType = arr.some(item => typeof item !== 'object' || item == null)
                const isArrayType = arr.some(item => Array.isArray(item))
                // 1.基础类型数组
                // 3.数组类型数组
                if (isBaseType || isArrayType) {
                    // 基础数据类型的情况
                    tree[key] = key
                } else {
                    // 2.对象类型数组
                    tree[key] = {}
                    runTree(data[key][0], tree[key])
                }
            } else {
                // $ 对象的情况
                tree[key] = {}
                runTree(data[key], tree[key])
            }
        }
    }

    runTree(data, tree)
    return tree
}

const tree = thirdDataToTree(testData)

通过thirdDataToTree第三方数据进行转换生成数据如下。这里不在阐述代码含义
image.png

  1. 将属性列表转换成ant可用数据格式
    因为我是用ant组件库的a-tree组件来渲染树的,属性列表数据不符合要求,所以需要多一次转换
/** 将对象树转为antd的treeData */
const treeToAntTree = (data, parentKey = '') => {
    return Object.keys(data).map(key => {
        const value = parentKey ? `${parentKey}.${key}` : key
        if (typeof data[key] === 'object' && data[key] !== null) {
            // $ 对象的情况
            return {
                label: key,
                value,
                children: treeToAntTree(data[key], value),
            }
        } else {
            // $ 基础数据类型的情况
            return {
                label: key,
                value,
            }
        }
    })
}

经过treeToAntTree我们就获得了属性链以及符合antd格式 image.png

  1. 通过映射规则生成最终数据
    如下是通过配置面板配置的映射关系
const mapping = {
    product: 'data.list.product',
    '数据1': 'data.list.objArray.string',
    '数据2': 'data.list.baseArray'
}
  1. 通过遍历mapping执行getDataByChain查找到对应的数据返回的数据可能是多维数组我们需要将他打平,例如数据2返回的数据就是[[1,2,..],[1,2,..]]
  2. 经过getDataByChain生成的数据,不一定符合我们数据要求,因此需要执行validData来校验每一项数据是否合法
  3. 因为生成的数据最终是[[product项1,product项2], [数据项1,数据项2] ...]与我这边需要[[product项1,数据项1], [product项2,数据项2] ...]因此需要执行buildData构建我们想要的数据。将二维数组的不同下标的相同项组合成一个数组,也是面试的高频题目

/** 生成合规的数据 */
const buildData = data => {
    const maxLength = Math.max(...data.map(arr => arr.length))
    const result = Array.from(
        { length: maxLength },
        (_, index) => data.map(row => row[index] || 0), // 如果该列的值不存在,填充为 0
    )
    return result
}

/** 验证数据是否合法 */
const validData = (data, mapping) => {
    const fields = Object.keys(mapping)
    const errors = []
    data.forEach((item, index) => {
        const isValid = item.some(i => typeof i !== 'object' || i == null)
        if (!isValid) {
            errors.push({
                key: fields[index],
                value: `数据映射${fields[index]}不合法`,
            })
        }
    })
    return errors
}

/** 根据a.b.c链接查找数据 */
const getDataByChain = (data, rules) => {
    const key = rules.shift()

    if (!rules.length) {
        // $ 最后一个元素,直接取值返回
        return data[key]
    } else {
        if (Array.isArray(data[key])) {
            // $ 类型为数组
            const arr = []
            for (const item of data[key]) {
                arr.push(getDataByChain(item, [...rules]))
            }
            return arr
        } else {
            // $ 类型为对象
            return getDataByChain(data[key], rules)
        }
    }
}

/** 生成数据 通过映射规则 */
const generateByRule = (thirdData, mapping) => {
    const fields = Object.keys(mapping)
    const result = []
    fields.forEach(key => {
        // 原型链接
        const rule = mapping[key]
        if (!rule) return
        const rules = rule.split('.')
        // $ 存在多维数组,需要打平
        const data = getDataByChain(thirdData, rules)
        if (Array.isArray(data)) {
            result.push(data.flat(Infinity))
        } else {
            result.push([data])
        }
    })
    const errors = validData(result, mapping)
    console.log('errors: ', errors)
    if (!errors.length) {
        const data = buildData(result)
        console.log('data: ', data)
    }
}

generateByRule(testData, mapping)

经过如上这些步骤,最终就生成了我们需要的数据了

image.png

总结

这一章节我们学会了如何实现可视化编辑器的数据映射功能,步骤分为如何设计配置面板->生成映射属性列表->建立映射关系->生成最终数据->数据是否合规校验。当然可能我们遇到的数据模版数据格式可能会有很大的区别,但只要掌握了基础实现思路,其他都是手拿把掐的事情