深刻理解组件的扩展性和易用性

1. 前言

    在前端开发工做中尤为是项目起步阶段,常常会选取第三方组件库快速构建,拿Vue生态来讲,大众广泛对element-uiiview 情有独钟,其中element-ui多用于对接前台项目,iview多用于对接后台管理项目。可不管是用哪一种组件库,只要其易用性、扩展性和感官性足够强,一般都会为咱们带来良好的体验。然而,当咱们过度对第三方组件库产生依赖后,它也可能带来一些问题。好比A项目依赖element-ui,B项目依赖iview,且为了不出现组件样式污染,一般一个项目只引用一个组件库。这样当在A项目中经过使用element-ui的一些基础组件(如select)“封装”了一个新组件,我就不能直接在B项目中使用,而仍是须要依赖iview 去从新写一套。长此以往,若是两个项目的共用组件愈来愈多,那将是不小的工做量。因此,这个时候就须要咱们本身动手丰衣足食了。前端

    当前各个论坛上已经有了不少如何搭建组件库项目的文章,但却鲜有具体组件开发思路的探讨和总结,所以本文会把重点内容放在后者。对于具体组件库项目的搭建方式,我比较推崇基于vuePress来实现。在此也推荐掘友 @_安歌 的一篇文章【Vue进阶】青铜选手,如何自研一套UI库?。本文带你们看几个经典的例子,相信新手朋友必定会有一些收获。但愿你们多多支持点赞,谢谢你们vue

    先看看我目前作的一个组件库,虽然还存在很大的提高空间,但只要时间充裕,我仍是能够作地更好:node

块级选择器: element-ui

块级选择器alt

树形组件: json

块级选择器

想来想去,我的总结一下写本身组件库的一些好处:bash

  • 更加深刻组件的生命周期钩子,深入掌握不一样钩子下组件的状态;
  • 更加使用一些平时写业务代码时比较少用的方法,如<slot>插槽、双向绑定、组件递归等;
  • 更加充分考虑组件的扩展性和复用性,提升业务驱动开发的能力;
  • 更加充分考虑组件的惟一性,确保不会来自其余第三方组件库的样式所覆盖。

接下来我以选择器和树形组件做为例子,深刻讲解一下实现过程和思路数据结构

2. 选择器 seletor

selector

    <select>选择器是最多见的组件之一,具体又可分为通常选择器、多选选择器、远程搜索选择器等。当咱们使用选择器时可能会想,会不会不少第三方组件库的选择器就是创建在传统<select><option>元素上。其实否则,由于这样作太局限,针对复杂场景时的拓展性太差。别看只是一个小小的选择器,它所涉及到的问题有时比咱们想象的要多,咱们应该至少解决下述场景:iview

  1. 实现对option作适配,知足不一样数据的兼容;
  2. 实现数值结果value和显示结果label的实时关联;
  3. 实现value的双向绑定,方便value的实时更新和获取。

2.1 结构优化

针对上述问题,咱们会发现下面这种方式布局组件是存在很大缺陷的:dom

<template>
   <kd-select 
     :options='optionList' // 传入选项集
     @getResult='getResult' 
   />
</template>
export default {
   data() {
      return {
        result: ''
        getResult: [
          {
            value: 'Juventus',
            label:'尤文图斯',
          },
          {
            value: 'RealMadrid',
            label:'皇家马德里',
          },
          {
            value: 'Barcelona',
            label:'巴塞罗那',
          }
        ]   
      }
   },
   methods: {
      getResult(val) {
        this.result = val;
      }
   }
}
复制代码

    该方式当然能够实现,但太过局限,首先是<option>彻底没有暴露到外部,形成绑定<option>labelvalue 没法配置,即没解决问题1。其次result也没有作双向绑定,不够简洁。所以,针对此类问题,对组件进行结构优化,代码以下:ide

<template>
    <kd-select v-model="teamName">
        <kd-option 
          v-for="(item,i) in teams" 
          :key="i" 
          :value='item.value' 
          :label='item.label'
        />
    </kd-select>
</template>
export default {
   data() {
      return {
        teamName: '',
        teams: [
            {
              value: 'Juventus',
              label:'尤文图斯',
            },
            {
              value: 'RealMadrid',
              label:'皇家马德里',
            },
            {
              value: 'Barcelona',
              label:'巴塞罗那',
            }
        ]
      }
   }
}
复制代码

这样就直观地解决了问题1,从而也将select选择器划分为由两个子组件构成的形式。下述为<select>的实现思路:

// select.vue
<template>
  <div class="kd-input-select">
     <div class="kd-input" v-outsideClick='showDropdown=false'>
        <input 
          class="kd-input-inner" 
          type="text" 
          readonly
          :value="result"
          placeholder="请选择" 
          @click="showDropdown = !showDropdown"
        />
     </div>
     <div v-show="showDropdown">
        <ul ref="dropdownList" class="kd-select-dropdown_list">
          <slot></slot>
        </ul>       
     </div>
  </div>
</template>
<script>
export default {
  model: {
    prop: 'bindVal',
    event: 'bindEvent'
  },
  props: {
    bindVal: [Number,String],
  },
  data() {
    return {
       result: '',
       showDropdown: false,
    }
  },
  methods: {
     getChoice(val) {
       this.result = val.label; // result只做为选取的label,不做为绑定值
       this.$emit('bindEvent', val.value);
     }
  }
};
</script>
复制代码

组件解析:

  • 一般选择器的label和value是不一样的,所以result只是做为显示label,真正是要经过bindVal作value,并经过 v-model实现双向绑定;

下述代码为与之对应的option的简单实现:

// option.vue
<template>
  <div>
    <li 
      class="kd-select-dropdown__item"
      @click="choice({ value:value, label:label })"
    >
      {{label}}
    </li>
  </div>
</template>
<script>
export default {
   props: ['value', 'label'],
   watch: {
     '$parent.bindVal': {
         handler(newVal , val) {
            if(newVal && newVal === this.value) {
               this.$parent.result = this.label;
            }
         },
         immediate: true
     }
   },
   methods: {
      choice(val) {
        this.$parent.getChoice(val)
      }
   }
}
</script>
复制代码

    其中经过监听父级组件selectbindVal,将label赋值给父级的result,从而解决结果显示的问题。 上述就是一个最基本的选择器的实现方式,咱们还能够作的更精致、更具扩展性。好比设置icon、设置只读和禁止属性,以及实现可远程搜索等功能。限于篇幅,这里再也不详述。

3. 树形组件

    树形组件的功能很强大,主要用于对层级划分鲜明且复杂的数据作直观的二维展现。年初当我看到业务需求时,起初也是想直接使用element现成的组件,但后来发现咱们的视觉设计和数据结构很复杂,无奈只好本身亲手来了。对于树形组件,起码也要解决以下问题:

  1. 数据的扩展性,即数据层级可无限划分,组件也可无限承载;
  2. 充分暴露组件各钩子状态,如数据的加载、就绪以及销毁等生命周期阶段;
  3. 任意层级的触发事件要充分、高效地暴露到组件最外部;
  4. 解决组件初始化状态的问题;
  5. 保证各层级标识的惟一性,从而实现对不一样层级进行样式、初始化事件等的特殊操做。

先看一个数据结构例子:

[
  {
    "code": "1212",
    "id": "1",
    "name": "云产品系统",
    "type": "product-line",
    "childs": [
        {
            "name": "公有云产品",
            "id": 1,
            "code": "gyyxl",
            "type": "product",
            "childs": [
                {
                    "name": "财务会计",
                    "id": 1,
                    "code": "cwkj",
                    "type": "domain",
                    "childs": [
                        {
                            "code": "GL",
                            "id": 4979,
                            "name": "总帐",
                            "type": "module",
                            "childs": null
                        },
                    ]
                },
                {
                    "name": "财务总览",
                    "id": 2,
                    "code": "cwzl",
                    "type": "domain",
                    "childs": null
                }
            ]
        }
    ]
  }
]
复制代码

    可见,各层有name、id、code、type、childs等字段,且经过childs字段定义子层级。俗话说的好,"人之初,性本善"。在产品经理不断和我确认并拍着胸膛保证数据层级必定是4层且永远不会改变的状况下,做为一个实诚的老实人,我开始真的就老老实实写个下面这种组件:

// tree.vue
<template>
      <section class="kd-product-line" v-for="(productLine , pl) in data">
        <p class="kd-product-bar">
         {{productLine.name}}
        </p>
        <ul> <!-- 1层 -->
           <section class="kd-product" v-for="(product , p) in productLine.childs">
              <p class="kd-product-bar">
               {{product.name}}
              </p>
              <ul class="kd-rank-domain"> <!-- 2层 -->
                <section class="kd-domain" v-for="(domain , d) in product.childs">
                        <p class="kd-doamin-bar">
                           {{domain.name}}
                        </p>
                        <ul class="kd-rank-module"> <!-- 3层 -->
                            <section v-for="(moduleObj, d) in domain.childs">
                               <p class="kd-module-bar">
                                 {{moduleObj.name}}
                                </p>
                                <ul class="kd-rank-bizentity"> <!-- 4层 -->
                                    <p class="kd-bizentity-bar" v-for="(bizentity, b) in moduleObj.childs">
                                       {{bizentity.name}}
                                    </p>
                                </ul>
                            </section>
                        </ul>
                </section>
              </ul>
           </section>
        </ul>
      </section> 
</template>
复制代码

    是否是已经看不进去了?没错,写出这样一种相似数据结果层级关系的组件,不只不扁平,也不利于后期的扩展和维护,你们都知道,产品经理的承诺听听就好,认真你就输了。好比有一天数据忽然从4层变成了5层,你就要再写深一层,那若是变成100层呢?那岂不是能够直接撂挑子了。

3.1 组件递归

    这个时候咱们就须要用到组件递归了,即经过对上面的层级组件进行分析,找到能够提取的公共部分做为子组件(下述代码中的<tree-item>组件),以后经过子组件递归的方式实现结构定义的抽象和扁平。代码以下:

// tree.vue
<div class="kd-tree card root">
  <tree-item 
    v-for="item in data"
    :key="item.id"
    :item='item'
  >
  </tree-item>
</div>
import TreeItem from './treeItem'
export default {
  name: 'kd-tree',
  props: {
    data: [Array],
  },
  components: {
     TreeItem
  },
}
</script>
复制代码

tree.vue主要充当一个容器,用于承载子组件<tree-item>及传递数据,且在此获取组件各周期。下面主要看一下<tree-item>的实现:

// TreeItem.vue
<template>
    <section class="tree-item">
       <div
         class="kd-bar">
          <span>{{item.name}}</span>
       </div>
       <ul v-if="item.childs && item.childs.length > 0"> 
          <!-- 组件递归 -->
          <tree-item 
            v-for="secondItem in item.childs"
            :key="secondItem.id"
            :item='secondItem'
          ></tree-item>
       </ul>
    </section>
</template>
<script>
export default {
   name: 'tree-item',
   props: {
      item: [Object],
   },
}
</script>
复制代码

3.2 层级惟一性

    经过组件递归的形式,便可实现代码的扁平和简洁。那如何实现各层级样式的差别?这里就须要定义各层级样式的惟一性。可见,最显而易见的方式就是定义每一个层级的class的差别,这里我限定各层级的顶级class名为“kd-层级数”,依次类推,分别定义"kd-层级数-bar"、"kd-层级数-name",实现方式为:

// tree.vue
<template>
  <div class='kd-tree'>
      <tree-item 
        v-for="item in data"
        :rankNum='initRank'
        :key="item.id"
        :item='item'
        :initFold='initFold ? initFold : true'
      >
      </tree-item>
  </div>
</template>
export default {
  props: {
     data: [Array], initFold: [Boolean]
  },
  data() {
      return { 
        initRank: 1, // 最外层初始化层级数为1
      }
  }
}

// treeItem.vue
<template>
    <section class="tree-item" :class="[outClassName]">
       <div
         class="kd-bar"
         :class="[barClass]"  
       >
          <span :class='[nameClass]'>{{item.name}}</span>
       </div>
       <ul v-if='item.childs && item.childs.length > 0'>
          <tree-item 
            v-for='secondItem in item.childs'
            :key='secondItem.id'
            :item='secondItem'
            :rankNum='(rankNum + 1)'
            :initFold='initFold'
          ></tree-item>
       </ul>
    </section>
</template>
<script>
export default {
   name: 'tree-item',
   props: {
      item: [Object],
      parentNode: [Object],
      rankNum: [Number],
      initFold: [Boolean]
   },
   data() {
       return {
           outClassName: `kd-${this.rankNum}`,
           barClass: `kd-${this.rankNum}-bar`,
           nameClass: `kd-${this.rankNum}-name`,
           foldChildNodes: true, // 是否折叠子节点
       }
   },
   created() {
     this.foldChildNodes = this.initFold;
   },
}
复制代码

这样就实现了层级样式差别,且暴露给外部初始化状态的方式。看看解析出来的元素:

tree CSS

3.3 事件传递

    以后就是<tree-item>事件的暴露了。由子是<tree-item>的递归,形成组件的抽象化,所以经过传统$emit方式不断向父级传递事件的方式显得格外低效和繁琐。在此,相信不少人都有了答案,那就是经过eventBus的跨组件通讯的方式实现事件直接暴露在tree上:

// bus.js
import Vue from 'vue'
export default new Vue({ })

// treeItem.vue
<template>
    <section>
       <div>
          <span 
            @click="controlChildNodes"
          >{{item.name}}</span>
       </div>
       <ul 
         v-if="item.childs && item.childs.length > 0 && !foldChildNodes">
          <tree-item 
            v-for='secondItem in item.childs'
            :key='secondItem.id'
            :item='secondItem'
            :rankNum='(rankNum + 1)'
          ></tree-item>
       </ul>
    </section>
</template>
<script>
import eventBus from '../../../libs/utils/bus.js'; 
export default {
   name: 'tree-item',
   props: {
      item: [Object],
      rankNum: [Number]
   },
   methods: {
      controlChildNodes() {
         this.foldChildNodes = !this.foldChildNodes;
         eventBus.$emit("node-click", this.item);
      }
   },
}

// tree.vue
<div class="kd-tree card root">
  <tree-item 
    v-for="item in data"
    :rankNum='initRank'
    :key="item.id"
    :item='item'
  >
  </tree-item>
</div>
<script>
import eventBus from '../../../libs/utils/bus.js'; 
import TreeItem from './treeItem'
export default {
  props: {
    data: [Array],
    initFold: [Boolean], 
  },
  data() {
    return { initRank: 1 }
  },
  mounted() {
     eventBus.$on("node-click",(itemData) => {
        this.$emit("node-trigger", itemData);
     })
  },
}
</script>
复制代码

这样,外部调用组件时,就能够经过node-trigger获取到触发节点。以后,针对具体业务场。

最后,菜炒好了,看一下怎么吃吧:

<template>
   <div>
      <kd-tree 
        :data='testData'
        @node-trigger="handleNodeClick">
      </kd-tree>
      <section v-show="curNode">
         当前点击的节点是:{{curNode.name}},
         类型是:{{curNode.type}}
      </section>
   </div>
</template>
<script>
import Tree from '@components/tree'
export default {
   name: 'kd-tree-demos',
   components: {
      'kd-tree': Tree
   },
   data() {
      return {
          testData: require('../public/treeData.json'),
          curNode:''
      }
   },
   methods: {
      handleNodeClick(nodeVal) {
         if(nodeVal) {
            this.curNode = nodeVal;
         }
      }
   }
}
</script>
复制代码

这样使用的感受仍是很简介鲜明的。

4. 总结

    从自写一套组件库,咱们会学习和领悟到不少平时写业务代码中不多用到的方法,强化咱们的知识体系和开发能力。对于组件库,咱们也要抱有持续开发和扩展的长线做战准备,只有不断优化,才会不断适应各类复杂新颖的业务。最后借用腾讯 @当耐特 大神的一句话:

你写的越好,头发掉的越多,别人就越方便,头发就掉的越少。

相关文章
相关标签/搜索