zoukankan      html  css  js  c++  java
  • 快速实现一个简单可复用可扩展的Vue树组件

    大概因为平时工作项目的原因,写了很多次树形组件,越写越觉得可以写得更简单并且更具有复用性、扩展性。树组件的应用场景很多,比如一篇文章的目录、一个公司部门组织情况、思维导图等,其实都可以用树形结构来描述。本文讲述一下vue中树组件的简单实现。

    树组件在线体验地址:http://wintc.top/laboratory/#/tree。

    一、树形数据结构

    树形数据是指形如以下的数据结构:

    [
      {
        id: '1',
        title: '节点1'
        children: [
          {
            id: '1-1',
            title: '节点1-1'
          },
          {
            id: '1-2',
            title: '节点1-2'
          }
        ]
      },
      {
        id: '2',
        title: '节点2',
        children: [
          {
            id: '2-1',
            title: '节点2-1'
          },
          {
            id: '2-2',
            title: '节点2-2'
          }
        ]
      },
      {
        id: '3',
        title: '节点3'
      }
    ]

    这并不是《数据结构与算法》里严格意义上的树定义,严格的树定义,第一层应该是一个根节点,而此处的第一层就包含了多个节点。不过这不重要,实用为上,这样的结构或许更加通用(试想,如果第一层是只包含一个节点的树,和标准定义的树又有什么本质区别呢)。

    树结构是递归的,它可能有很多级,我们在渲染树结构的时候也采用递归的方式来渲染。

      

    二、vue树组件实现的两个重要属性

    1. 组件的name属性

    或许我们在平常开发的过程中都很少使用这个属性(至少我是如此),不过这个属性却有两个很重要的作用,摘自Vue官网:

      • 允许组件模板递归地调用自身。注意,组件在全局用 Vue.component() 注册时,全局 ID 自动作为组件的 name。

      • 指定 name 选项的另一个好处是便于调试。有名字的组件有更友好的警告信息。另外,当在有 vue-devtools,未命名组件将显示成 <AnonymousComponent>,这很没有语义。通过提供 name 选项,可以获得更有语义信息的组件树。

    这里我们因为要递归地渲染树形结构,即渲染树节点的时候使用当前组件作为子组件,所以会用到name属性。

      

    2. 插槽

    Vue插槽借鉴于Web components,是在父组件自定义实现子组件部分dom的一个方法,这大大提高了子组件的复用性和扩展性。下面是一个简单的插槽使用例子。

    子组件:

    <template>
      <div>
        子组件
        <slot name="custom-content"></slot>
        <slot></slot>
      </div>
    </template>
    
    

    父组件:

    <child>
      <template v-slot:custom-content>
        <div>父组件通过插槽插入的内容——具名插槽</div>
      </template>
      <template v-slot:default>
        父组件通过默认插槽插入的内容——默认插槽
      </template>
    </child>

    渲染结果:

    父组件可以通过v-slot提供一些插槽来达到组件内容自定义的目的,只要子组件模板里预留了这些插槽的位置。插槽可以有名字作为唯一标识,如也可以不设置名字而使用默认值default;父组件中填充插槽使用v-slot:name填充对应的插槽,name表示填充的插槽名称,未指定名字的插槽填充时使用v-slot:default或v-slot,二者作用是相同的。

    关于Vue插槽,官网描述很清楚了,这里不再赘述。此处介绍几种插槽的特殊用法。

    作用域插槽

    默认情况下,父组件模板里通过插槽插入的部分只能访问父组件上下文,如果你需要访问子组件的部分属性,可以通过作用域插槽:在子组件中给slot传递一些prop,然后在父组件插入内容时使用这些prop,示例如下。
    子组件:

    <template>
      <div>
        子组件
        <slot :nodeData="nodeData"></slot>
      </div>
    </template>
    
    <script>
    export default {
      data () {
        return {
          nodeData: {
            title: '节点1'
          }
        }
      }
    }
    </script>​

    父组件:

    <template>
      <div id="app">
        <child>
          <template v-slot:default="someProp">
            子组件传到插槽的nodeData的title:{{ someProp.nodeData.title }}
          </template>
        </child>
      </div>
    </template>​

    someProp是可以解构的,可以把从子组件传递给插槽的prop解构出来直接使用: v-slot:default="{ nodeData }"。

    插槽函数

    从Vue2.6.0开始,所有的插槽(包括作用域插槽和普通插槽)都会作为一个函数,并通过vm.$scopeSlots暴露出来,比如上述父组件模板中给子组件插入了一个作用域插槽(名字为默认的default),在子组件实例的$scopeSlots就会暴露对应的函数:

    • 插槽函数

    传递相应的参数(比如nodeData)调用这个函数,就可以得到插槽对应的虚拟DOM即VNode,在render函数中非常好使。只要你能访问到这个函数,就可以生成对应的虚拟DOM(并不是非得在当前组件),接下来在树组件叶子节点的渲染中,会用到这个用法。

    三、树组件渲染的实现

    1. 树组件调用形式

    作为一个可复用的树节点,渲染出来的树结构应该是可以自定义的,同时对于传入组件的prop尽可能简单。这里设计的prop仅传入一个树形数据treeData,对于每一个节点,使用作用域插槽提供每一个节点的数据来调用者自定义渲染。所以组件调用形式很简单:

    <tree :treeData="treeData" ref="tree">
      <template v-slot="{ nodeData }">
        <!-- 树节点自定义内容,比如展开折叠按钮,节点文本,节点菜单等 -->
        <div class="node-content">
          {{ nodeData.title }}
        </div>
      </template>
    </tree>

    这里的调用仅仅把节点的文本渲染出来了,通常结合具体的场景,可能我们会有更多的功能需求,比如展开折叠,删除节点,添加节点等,给组件添加一个ref属性,我们可以方便地调用组件提供的函数。

    广州设计公司https://www.houdianzi.com 我的007办公资源网站https://www.wode007.com

    2. 树组件代码实现

    通常递归实现树组件会分为两个部分:

      • 树节点渲染组件,该组件通过归调用自身渲染一个子树,一直递归到渲染完全部叶子节点。
      • 供外部调用的树形组件,提供一些外部调用的接口

    下面直接给出树组件、节点组件的简单代码实现如下:

    树组件tree.vue:

    <template>
      <div class="tree-node-container">
        <node-content></node-content>
        <div
          class="tree-node-children"
          :style="{
            paddingLeft: indent
          }"
          v-if="nextShow">
          <tree-node
            v-for="(child, idx) of nodeData.children"
            :nodeData="child"
            :indent="indent"
            :key="idx">
          </tree-node>
        </div>
      </div>
    </template>
    
    <script>
    
    export default {
      name: 'tree-node',
      props: {
        nodeData: {
          type: Object,
          required: true
        }
      },
      components: {
        'node-content': {
          render (h) {
            let slot = this.$parent.tree.$scopedSlots.default
            let { nodeData, parentData, level, nextShow } = this.$parent
            return (slot ? slot({ parentData, data: nodeData, level, nextShow }) : '<div>未定义插槽内容</div>')
          }
        }
      },
      data () {
        return {
          tree: false,
          level: 0,
          parentData: null,
          childrenShow: true,
          indent: undefined
        }
      },
      computed: {
        nextShow () {
          return this.nodeData.children && this.nodeData.children.length && this.childrenShow
        }
      },
      created () {
        let parent = this.$parent
        if (parent.isTree) {
          this.level = 1
        } else {
          this.level = parent.level + 1
          this.parentData = parent.nodeData
        }
        while (parent && !parent.isTree) {
          parent = parent.$parent
        }
        this.tree = parent
        this.indent = this.tree.indent
        this.tree.registerNodeComponent(this.nodeData.id, this)
      },
      beforeDestroy () {
        this.tree.removeNodeComponent(this.nodeData.id)
      },
      methods: {
        showChildren (show) {
          this.childrenShow = show
        }
      }
    }
    </script>

    节点组件tree-node.vue:

    <template>
      <div class="tree-container">
        <tree-node
          v-for="(nodeData, idx) of treeData"
          :nodeData="nodeData"
          :key="idx">
        </tree-node>
      </div>
    </template>
    
    <script>
    import treeNode from './tree-node'
    
    export default {
      components: {
        treeNode
      },
      props: {
        treeData: {
          type: Array,
          requied: true
        },
        indent: {
          type: String,
          default: '20px'
        }
      },
      data () {
        return {
          isTree: true,
          level: 0,
          componentMap: {}
        }
      },
      methods: {
        registerNodeComponent (id, component) {
          this.componentMap[id] = component
        },
        removeNodeComponent (id) {
          this.componentMap[id] = undefined
        },
        showChildren (id, show) {
          this.componentMap[id] && this.componentMap[id].showChildren(show)
        }
        // 更多功能
      }
    }
    </script>
    
    <style lang="stylus" scoped>
    .tree-container
      text-align left
    </style>

    有几个点值得注意:

    • 树组件的componentMap保存了所有后代组件的引用。这样好处在于:在使用tree组件的时候,我们直接访问的只有树组件,想直接调用叶子节点的相关方法,可以通过树组件做转发。比如想让id为1的节点折叠起后代元素,可以调用this.$refs.tree.showChildren(1, true),在树组件的showChildren函数内,取出对应的节点组件,调用节点组件的方法。

    • 节点自身内容的渲染。节点组件的tree树形保存了树组件的引用,节点自身内容的渲染,使用了一个子组件node-content。而node-content的内容,来自于树组件的插槽,我们通过前面所描述函数形式访问到树组件的插槽,调用函数得到了虚拟DOM,作为组件node-content的内容。当然如果未提供插槽,node-content的内容会是一段提醒含义的html串,提醒使用者插入插槽(你也可以修改为一个默认的渲染,比如渲染出nodeData.title)。
    • 在提供的作用域插槽中,node-content组件向该插槽传递了parentData(上级节点数据), data(本级节点数据), level(层级)等一些可能常用的属性。

    上述提供的树组件比较简单,仅仅提供了渲染树形结构、展开/折叠等功能,但是作为一个示例性的树形组件,它还是具有很强地扩展性以及复用性。因为使用Vue,我们用数据控制视图,所以你在使用的过程中想给组件增加比如节点多选、增删节点等操作,其实非常简单,你只用修改你的树形数据,并且在插槽上做相应的视图处理即可(比如增加选择框、添加下级/删除节点等菜单)。

  • 相关阅读:
    039 RabbitMq及数据同步01
    038 商品详情02-----页面静态化
    037 商品详情01
    036 搭建搜索微服务04----分类和品牌的过滤
    035 搭建搜索微服务03----页面分页效果
    034 通过域名访问服务器或本地的图片资源---switchhost+nginx
    033 搭建搜索微服务02----实现基本搜索功能
    ubuntu server 无线网卡的处理
    grub 启动错误 "file not found"
    Ubuntu 的 desktop 和 server 还是有区别。
  • 原文地址:https://www.cnblogs.com/qianxiaox/p/13830970.html
Copyright © 2011-2022 走看看