vue实现树形组件,参考并解析elementUI树形组件

3,409 阅读7分钟

废话部分,不想看的跳过就行了,发发牢骚

本来是不想发出来的,但是呢,最后出于装逼,讨论和分享的想法下还是拿出来。

而且我对于树形组件的自定义节点这块,还是没有理解透彻。也希望有大神帮忙解惑。

然后其实我的眼界还是有限,一直都停留在ui组件上面,但是作为一个普通的前端,大家不都干这样的事情吗?既然你觉得我眼界小,那么请说说你在干什么。(一次偏激的回复)

感慨:

如果没有网络的世界我想我会抑郁而死。其实作家不是因为他想当作家,他是为了发泄自己心中的不快。为什么网上肆无忌惮,因为没有生活中那么多的指责。现在大家都活成了人面兽心的人了吧。

说明:

我不是一个大神,我真实的写代码的经历只有一年,一开始是微信小程序开发,后面年初的时候开始写的web网页。我不知道为什么,我只有一年的代码经历,但是却超过了很多人(同样也不如很多人)。但是在这样一个社会环境和南京这样一个哪怕是互联网不怎么地的环境下,我依旧是一个菜鸟。而且我是一个耿直的人,所以我喜欢直抒胸臆,但是这个社会不让,生活不让。很苦恼。

一、递归思想

递归:自己调用自己

写树形组件肯定是可以无限嵌套的,我记得之前我写过一篇关于递归组件的文章,但是那个是两个组件相互调用从而实现树形组件的递归。

这次不一样,是一个比较省事的方式来写。更少的代码。

二、先写你的容器组件

1、代码先全部放上来

<template>
  <div class="dht-tree-main">
    <twig-node
      v-for="(item, index) in data"
      :key="getNodeKey(item, index)"
      :child="item"
      :level="1"
      :data-location="[1, index]"
      :indent="indent"
    >
    </twig-node>
  </div>
</template>

<script>
export default {
  name: "dhtTree",
  components: {
    twigNode: () => import("./twig-node.vue")
  },
  props: {
    data: {
      type: Array
    },
    indent: {
      // 每一层缩进多少像素
      type: Number,
      default: 18
    }
  },
  data() {
    return {
      active: true
    };
  },
  beforeCreate() {},
  created() {
    this.isTree = true;
  },
  beforeMount() {},
  mounted() {
    //是否有子元素作为模板
    this.slot = !!this.$scopedSlots.default;
  },
  destroyed() {},
  methods: {
    getNodeKey(node, index) {
      return node.id ? node.id : index;
    }
  }
};
</script>

2、解析

这里其实很简单,就是写一个容器,用来存放你的递归组件。

参数解析:

:key="getNodeKey(item, index)"
:child="item"
:level="1"
:data-location="[1, index]"
:indent="indent"

key:这个这样写的意义在于让客户可以自定义key值

child:子节点数据

level:层级,在main下是定义初始层级

data-location:用于表示每一个节点的位置,也就是定位的作用

indent:节点是需要缩进的,定义一个初始缩进值,后面将按这个计算每一层需要缩进多少

三、核心的递归子节点部分

这里我会逐步拆分的来讲

先放全部的html部分

<div class="dht-tree-twig-one">
  <div
    class="dht-tree-node-content"
    :style="{ paddingLeft: level * indent + 'px' }"
    @click="showNode"
  >
    <!--箭头-->
    <span
      v-if="child.children.length > 0"
      class="iconfont icon-jiantou arrow"
      :style="{ transform: 'rotate(' + rotate + 'deg)' }"
    ></span>
    <!--icon图标-->
    <span v-if="child.icon" :class="child.icon" class="icon"></span>
    <!--可自定义部分-->
    <node-content :node="child"></node-content>
  </div>
  <transition-group name="dht-tree-node">
    <twig-node
      v-for="(item, index) in child.children"
      v-show="isShow"
      :key="getNodeKey(item, index)"
      :child="item"
      :level="level + 1"
      :data-location="[level + 1, index]"
      :indent="indent"
    ></twig-node>
  </transition-group>

这里主要分为两部分,在transition-group上面为当前节点展示的效果,而其中的部分是组件的递归部分。

1、当前节点的缩进

<div
  class="dht-tree-node-content"
  :style="{ paddingLeft: level * indent + 'px' }"
  @click="showNode"
>

这里我很简单,就是按层级计算缩进的像素,然后算就行了。

这个click函数是当前节点关闭或者打开。这块后面需要单独展开

2、实现自定义子节点,并且实现类似elementUI的插槽作用域

作用域插槽部分请看vue文档:

https://cn.vuejs.org/v2/guide/components-slots.html#作用域插槽

<!--箭头-->
<span
  v-if="child.children.length > 0"
  class="iconfont icon-jiantou arrow"
  :style="{ transform: 'rotate(' + rotate + 'deg)' }"
></span>
<!--icon图标-->
<span v-if="child.icon" :class="child.icon" class="icon"></span>
<!--可自定义部分-->
<node-content :node="child"></node-content>

这里的箭头还有icon都是修饰作用,意义不大。核心在于

node-content这个组件,这个组件是jsx组件。说实在,这块我看了半天还是对于elementUI中的一些东西不理解。

这是组件的实现:

components: {
  nodeContent: {
    props: {
      node: {
        required: true
      }
    },
    render(ce) {
      const parent = this.$parent;
      const tree = parent.tree;
      const node = this.node;

      // return ce("span", node.label);
      // console.log(tree);
      if (tree.slot) {
        return tree.$scopedSlots.default
          ? tree.$scopedSlots.default({ node })
          : (parent.$scopedSlots = {
              default: () => {
                return node;
              }
            });
      } else {
        return ce("span", node.label);
      }
    }
  }
},

这里很关键在于需要判断是否是父节点,是否是当前节点的父节点,然后设置$scopedSlots。这样就是把组件本身设置成了作用域插槽。不过说实在,我这块代码不是很理解。我是参考elementUI部分,写出来的。(希望看过elementUI源码的,或者懂的人能给我解惑下。

这块我只有一个模糊的概念,应该这么写,但是我不能自信的写出来。

内部拆解

三个声明:

parent        父节点

tree            一级节点,也就是第一菜单

node            当前传入的数据编辑数据

if (tree.slot) {
        return tree.$scopedSlots.default
          ? tree.$scopedSlots.default({ node })
          : (parent.$scopedSlots = {
              default: () => {
                return node;
              }
            });
      } else {
        return ce("span", node.label);
      }

这里的if部分是判断一级菜单是否有编写自定义的节点数据。

slot的判断语句是这样的,在main组件下的mouted下面

this.slot = !!this.$scopedSlots.default;

然后如果有自定义的节点数据,那么渲染的时候就根据当前是哪一级的节点进行作用域数据渲染。

这里我最不理解的是,我明明自定义的节点数据在main组件下,但是渲染的时候却可以实现每一级递归的数据都变成一样的节点。明明自定义的节点数据在外层啊。望大神解惑

三、当前这一层的菜单开启和关闭

这个比较简单,但是处理方式有两种。

第一种:这种很麻烦,需要层层递递归计算当前的菜单开启关闭的高度。

第二种:利用vue的transition-group组件,包裹你的递归组件,必要的时候开启关闭就行了

<transition-group name="dht-tree-node">
  <twig-node
    v-for="(item, index) in child.children"
    v-show="isShow"
    :key="getNodeKey(item, index)"
    :child="item"
    :level="level + 1"
    :data-location="[level + 1, index]"
    :indent="indent"
  ></twig-node>
</transition-group>

看v-show,就这么简单了。

三、递归子节点的代码和性能提醒

1、先说性能

这是我刚写博客的时候,一位掘金大神(好像还是一个漂亮妹子)提供的。

掘金昵称:无恙作别

他说:在自定义组件渲染的时候(也就是我刚才的那段jsx语法)如果,渲染数据超过100条会感觉到卡顿,500条就会非常明显,但是如果是换成普通的html元素就不会卡顿。

所以,自己会写组件也很重要的。

2、代码部分

<template>
  <div class="dht-tree-twig-one">
    <div
      class="dht-tree-node-content"
      :style="{ paddingLeft: level * indent + 'px' }"
      @click="showNode"
    >
      <!--箭头-->
      <span
        v-if="child.children.length > 0"
        class="iconfont icon-jiantou arrow"
        :style="{ transform: 'rotate(' + rotate + 'deg)' }"
      ></span>
      <!--icon图标-->
      <span v-if="child.icon" :class="child.icon" class="icon"></span>
      <!--可自定义部分-->
      <node-content :node="child"></node-content>
    </div>
    <transition-group name="dht-tree-node">
      <twig-node
        v-for="(item, index) in child.children"
        v-show="isShow"
        :key="getNodeKey(item, index)"
        :child="item"
        :level="level + 1"
        :data-location="[level + 1, index]"
        :indent="indent"
      ></twig-node>
    </transition-group>
  </div>
</template>

<script>
export default {
  name: "twigNode",
  components: {
    nodeContent: {
      props: {
        node: {
          required: true
        }
      },
      render(ce) {
        const parent = this.$parent;
        const tree = parent.tree;
        const node = this.node;

        // return ce("span", node.label);
        // console.log(tree);
        if (tree.slot) {
          return tree.$scopedSlots.default
            ? tree.$scopedSlots.default({ node })
            : (parent.$scopedSlots = {
                default: () => {
                  return node;
                }
              });
        } else {
          return ce("span", node.label);
        }
      }
    }
  },
  data() {
    return {
      tree: null,
      rotate: 0, // 三角形标记
      isShow: false //操作子元素关闭
    };
  },
  props: {
    indent: {
      // 缩进
      type: Number,
      default: 18
    },
    dataLocation: Array, //数据定位,表示层级和数据位置
    level: Number, //当前层级
    child: Object //子节点数据
  },
  beforeCreate() {},
  created() {
    const parent = this.$parent;

    if (parent.isTree) {
      this.tree = parent;
    } else {
      this.tree = parent.$parent.tree;
    }

    /*if (this.child.children.length > 0 || this.level === 1) {
      this.isShow = true;
    }*/
  },
  beforeMount() {},
  mounted() {},
  destroyed() {},
  methods: {
    getNodeKey(node, index) {
      return node.id ? node.id : index;
    },
    //打开或者关闭节点
    showNode() {
      if (this.child.children.length <= 0) return false;
      //操作子元素方式开启关闭
      if (this.isShow) {
        this.isShow = false;
        this.rotate = 0;
      } else {
        this.isShow = true;
        this.rotate = 90;
      }
    }
  }
};
</script>


四、关于ui组件库css

我其实一开始写组件库的时候css都是写在每个组件后面的,有些人会直接用scope。

直到我写递归组件我想到一个问题,vue在解析的时候每次都会解析一次css,那么就会导致你递归多少次,css加载多次。如果是scope的情况下,那么就比较崩溃了。会非常多重复的css出现。特别是写组件库的时候,你千万别用scope,会很容易导致连样式穿透也无法修改css,导致自定义性很差。

所以我现在把我的组件库的css全部单独抽出来了,用一个index.scss文件统一加载

下面是递归组件的css部分

关于scss变量部分请自己换成css属性

.dht-tree-main {
    position: relative;
}
.dht-tree-twig-one {
    position: relative;
    //transition: height 0.5s ease;
    overflow: hidden;
    .dht-tree-node-content {
        display: flex;
        flex-flow: row;
        align-items: center;
        padding-left: 18px;
        height: 25px;
        overflow: hidden;
        &:hover,
        &:active {
            background: rgba(15, 128, 255, 0.1);
        }
        .arrow {
            font-size: 12px;
            color: $font_info;
            margin-right: 5px;
            transition: transform 0.5s ease;
            //transform: rotate(90deg);
        }
        .icon {
            font-size: $size-main;
            color: $icon-main;
            margin-right: 5px;
        }
    }
    .dht-tree-node-enter-active, .dht-tree-node-leave-active {
        transition: opacity .5s;
    }
    .dht-tree-node-enter, .dht-tree-node-leave-to /* .fade-leave-active below version 2.1.8 */ {
        opacity: 0;
    }
}


五、使用

<dht-tree :data="data">
  <span slot-scope="{ node }" class="dhtceshi">{{ node.label }}</span>
</dht-tree>

data数据:里面没有icon,注意icon是css类

data: [
  { id: 2, label: "第2个", children: [] },
  {
    id: 1,
    label: "第1个",
    children: [
      {
        id: 1,
        label: "二级第1个",
        children: [
          {
            id: 1,
            label: "三级1第1个",
            children: [
              { id: 1, label: "1", children: [] },
              { id: 2, label: "2", children: [] },
              { id: 3, label: "3", children: [] },
              { id: 4, label: "4", children: [] },
              { id: 5, label: "5", children: [] }
            ]
          },
          { id: 2, label: "三级2第2个", children: [] },
          { id: 3, label: "三级3第3个", children: [] },
          { id: 4, label: "三级4第4个", children: [] },
          { id: 5, label: "三级5第5个", children: [] }
        ]
      },
      { id: 2, label: "二级第2个", children: [] },
      { id: 3, label: "二级第3个", children: [] },
      { id: 4, label: "二级第4个", children: [] },
      { id: 5, label: "二级第5个", children: [] }
    ]
  },
  { id: 3, label: "第3个", children: [] },
  { id: 4, label: "第4个", children: [] },
  { id: 5, label: "第5个", children: [] }
]


六、致谢和说明

本文已经把源码完全贴出来了,main组件部分在一开始,递归子组件在最后部分

使用是单独贴出来的第五点。

感谢elementUI团队开源代码