记录一次有难度的后台重构

有这么一个需求,原来的一个后台需要重构,前端展示为这样的:

正如你所看到的,这个有添加有删除功能,还需要长成这样。后端给的数据格式经过简化为:

{
    "data": {
        "path": "data",
        "type": "dict",
        "showName": "文言文编辑",
        "value": null,
        "isNecessary": true,
        "subDefine": [
            {
                "path": "data/title",
                "type": "string",
                "showName": "标题",
                "value": "周亚夫军细柳",
                "isNecessary": true,
                "subDefine": null
            },
            {
                "path": "data/book",
                "type": "list",
                "showName": "课本",
                "value": null,
                "isNecessary": true,
                "subDefine": [
                    {
                        "path": "data/book/book_0",
                        "type": "dict",
                        "showName": "1",
                        "value": null,
                        "isNecessary": false,
                        "subDefine": [
                            {
                                "path": "data/book/book_0/version",
                                "type": "string",
                                "showName": "教材",
                                "value": "人教新版",
                                "isNecessary": true,
                                "subDefine": null
                            }
                        ]
                    },
                    {
                        "path": "data/book/book_1",
                        "type": "dict",
                        "showName": "2",
                        "value": null,
                        "isNecessary": false,
                        "subDefine": [
                            {
                                "path": "data/book/book_1/version",
                                "type": "string",
                                "showName": "教材",
                                "value": "部编本",
                                "isNecessary": true,
                                "subDefine": null
                            }
                        ]
                    }
                ]
            }
        ]
    }
}

在看看各个参数的意义:

  • path: 当前路径
  • type: 表示类型
  • showName: 展示的字
  • value: 输入框展示的内容
  • isNecessary: 是否是必须的
  • subDefine: 子元素,如果有就渲染子元素如果没有就不渲染

后端怎么把数据传给我的,我就需要按这样的格式传给他,中间用户可能修改value值,然后需要把这些值进行校验,并且传给后端。说实话拿到这个需求的时候,一头雾水,原来是用jq来写的,原来的子节点全是用jq动态插入的,现在需要迁移这个技术栈到Vue

如何画这种结构图

首先需要把这个结构图给搞出来,这种结构图需要怎么做,下面记录一下我的心里路程~~:

插件

懒人有懒人的思考,我的第一反应就是找个插件啥的,啥都不用操心了,传递数据完事了。本身后台也用的是element-ui,所以第一想法用一下tree插件,但是tree组件长这个样

虽然节点的内容可以自己定义,但是和产品要求的长的不一样,如果要改,可能要改复写很多样式,所以这种方案暂时放弃了。

jsx

关于这种方案我问过旁边的小伙伴,子节点的渲染靠subDefine是否存在,所以我不知道这个需要渲染多少层。所以请教了一下经验丰富的小伙伴,他给出了jsx的思路,因为jsx更加灵活,使用函数式编程可能更好解决这个问题。以前没有用过jsx,所以先学习了一个下午,准备开始动手了,先在这个项目加上一些jsx的代码,加上以后,发现编译报错,说我差一个loader,报错如下:


找了一些原因后,发现是在vue.config.js中加入了

chainWebpack: (config) => {
    config.module.rules.delete('js'); 
}

所以这样就不会对jsx进行编译,本来到这里我们实际是可以用jsx来完成这个项目了,但是项目中用了一个公司自己的组件库,关掉这个和这个组件库冲突了,具体怎么冲突我就不说了,反正要修改组件库的难度有点大,所以放弃了这种思路。

递归组件

旁边一个小伙伴听到我的困惑以后,提出了递归组件的思路,就是一个组件可以嵌套相同的组件,比如说tree.vue是一个组件,他内部也可以调用tree组件,但是这个组件需要要加上name属性,如下:

## tree.vue

因为每个节点的内容长的差不多,我们只需要控制一些少许的变量,就能实现这样的效果,然后通过是否有subDefine进行控制是否需要子节点。

数据处理

因为每条数据因为值不一样,表现不一样,所以我这里就使用了遍历每条数据进行处理,感觉这种方式不是很好,有更好的想法可以提出来哈哈。

function dealData(list, type) {
  const final = [];
  list.forEach((item) => {
    let subDefine;
    if (item.subDefine) {
      subDefine = this.dealData(item.subDefine, item.type);
    }
    final.push({
      required: item.isNecessary ? '必填' : '',
      ...item,
      subDefine,
    });
  });
  return final;
};

这里每条数据都做了相应的处理,并且如果子元素有subDefine继续调用该函数。问题是解决了,但是发现这个效率极差,但是本人对算法也没啥研究,如果有小伙伴有更好的建议可以提出来。 源代码中加入了各种乱起八糟的东西,下面是简化的tree.vue的内容:


在上面的需求图中可以看到可以添加,也可以删除。下面是我的相应的解决办法

删除

删除简单,点击删除实际把该元素的父亲的subDefine删除最后一个元素,也就是把父元素的trees删除最后一个元素,代码如下:

trees.pop()

这样就能删除最后一个元素了

添加

后端在传给前端的时候,除了一个data,还有一个minData, 这个minData的数据格式和data相同,不同的是每一项的value都是空的,如果该项可以扩展,意思是说能够往subDefine中添加子元素,这个subDefine是不为空的,但是只有一个元素。这个数据在添加子元素的时候极为的有用,比如说现在当前的元素的subDefine是个空的,当我们向其中添加元素的时候,那这时候这个新元素的数据结构应该是怎么样的。这时就需要通过找到minData中哪一个元素的path和当前的path是相同的。先前想过循环遍历找到相同的,但是瞬间被自己否定了,虽然咱们对算法没什么研究,但是也不能使用这么low的想法吧。所以首先对mindData进行处理,在先前提到每个元素的path都是不同的,那是不是可以重新创建一个对象,其中的key就是每条数据的pathvalue就是该条数据。这时候但我们需要添加一个新元素的时候,只需要知道对应的path,然后从minData中取出key等于path的那条数据,然后取出那条数据的subDefine的第一条数据就行了。
下面是minData的数据处理函数:

constructPathObj(subDefine, res = {}) {
  subDefine.forEach((value) => {
    res[value.path] = value;
    if (value.subDefine) {
      this.constructPathObj(value.subDefine, res);
    }
  });
  return res;
}
minDataa = constructPathObj(data)

这样就得到了一个已pathkey,数据为value的一个对象。这里还需要注意一点就是因为前面提到path是唯一的,所以在添加新元素的时候不能够让path重复。例如现在subDefine中有一个元素的pathdata/book_1,后端要求新添加的元素pathdata/book_2,所以有了以下代码

const { subDefine } = item;
let index;
if (subDefine.length === 0) {
  // 根据path找到子元素
  index = 0;
} else {
  index = subDefine.length;
}
const temp = this.dealData(minData[information.path].subDefine[0], index);
subDefine.push(temp);

function dealData(data, index) {
  const temp = {};
  if (data.subDefine) {
    temp.subDefine = [];
    data.subDefine.forEach((val) => {
      // 先对传给后面的数据path进行处理
      val.path = val.path.replace(/(.*)_[0-9]/, `$1_${data.showName}`);
      temp.subDefine.push(this.dealData(val));
    });
  }
  if (data.type === 'dict') {
    temp.showName = index + 1;
    temp.path = data.path.replace(/(.*)_[0-9]/, `$1_${index}`);
  }
  return {
    ...data,
    ...temp,
  };
}

这样就会对生成的每个元素的path进行规范,也就到达了添加一个新元素的一个效果。

总结

这个项目因为是重构,所以既有的数据结构不能变动,那么就需要对原来的数据进行处理得到我们需要的数据,因为这个项目使用了大量的递归和循环,导致性能很差,所以不得已加了一个loading,哈哈~~。当然当自己写完这篇文章的时候,发现很多地方对于循环和递归是没有必要的,有很大的性能优化空间。这篇文章主要是讲得自己面对这个项目的一些思考吧, 欢迎提出建议

你可能感兴趣的