深刻理解組件的擴展性和易用性

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. 總結

    從自寫一套組件庫,咱們會學習和領悟到不少平時寫業務代碼中不多用到的方法,強化咱們的知識體系和開發能力。對於組件庫,咱們也要抱有持續開發和擴展的長線做戰準備,只有不斷優化,纔會不斷適應各類複雜新穎的業務。最後借用騰訊 @當耐特 大神的一句話:

你寫的越好,頭髮掉的越多,別人就越方便,頭髮就掉的越少。

相關文章
相關標籤/搜索