[舉個栗子]增長組件通用性的幾個點

寫代碼有時候就和彈簧加工同樣。一個看似簡單的物品,加工起來未必簡單css

1.前言

最近在作項目的時候,看到有兩個功能同樣,可是交互,樣式不同的需求,爲了圖方便維護,就封裝了組件,發現一個看似簡單的組件,若是要封裝得通用些,要考慮的東西其實也很多。前端

該文章只是舉例說明能夠從哪些點入手,增長組件的通用性。以及提供一些封裝的思路。說起的組件仍然與項目需求有挺大的關係,差很少是針對項目的定製開發,在其餘項目上可能還不能開箱即用,要使用的話,還須要對組件進行修改。vue

2.先看組件

這個組件看着就很簡單,一下就寫好了git

出於篇幅的考慮,css ,以及一些不關聯的 js 代碼就不提供了,須要看源碼能夠移步: 文章例子源碼:HandleButtonOld,項目完整代碼:項目代碼github

HandleButtonOld.vuebash

<template>
  <div class="ec-handle">
    <div
      class="ec-handle--item"
      v-for="(item,index) in value"
      :key="index"
      :class="{'cur':nowClickIndex===index}"
      @click="switchCur(item,index)"
    >
      <ec-text v-if="item.fileType==='text'" :src="item.fileUrl"></ec-text>
      <video :src="item.fileUrl" v-if="item.fileType==='video'"></video>
      <audio :src="item.fileUrl" controls="controls" v-if="item.fileType==='audio'"></audio>
      <ec-image :src="item.fileUrl" v-if="item.fileType==='image'" />
      <ul class="customer-form-view-action-box">
        <li class="iconfont icon-icon-cus-edit" @click.stop="handleEvent('edit',index)"></li>
        <li
          class="iconfont icon-icon-cus-up"
          @click.stop="handleEvent('up',index)"
          v-if="index!==0"
        ></li>
        <li
          class="iconfont icon-icon-cus-down"
          @click.stop="handleEvent('down',index)"
          v-if="index!==value.length-1"
        ></li>
        <li class="iconfont icon-icon-cus-del" @click.stop="handleEvent('delete',index)"></li>
      </ul>
    </div>
  </div>
</template>
<script>
export default {
  name: 'HandleButton',
  componentName: 'HandleButton',
  props: {
    value: {
      type: Array,
      default () {
        return []
      }
    }
  },
  data () {
    return {
      nowClickIndex: ''
    }
  },
  mounted () {},
  methods: {
    handleEvent (type, index) {
      let _list = JSON.parse(JSON.stringify(this.value))
      let _nowItem = _list[index]
      switch (type) {
        case 'up':
          this.nowClickIndex--
          _list.splice(index, 1)
          _list.splice(index - 1, 0, _nowItem)
          break
        case 'down':
          this.nowClickIndex++
          _list.splice(index, 1)
          _list.splice(index + 1, 0, _nowItem)
          break
        case 'delete':
          _list.splice(index, 1)
      }
      this.$emit('input', _list)
      this.$emit(type, _nowItem, index)
    },
    switchCur (item, index) {
      this.nowClickIndex = index
    }
  }
}
</script>
<style lang="scss" scoped>
// 略
</style>
複製代碼

3.改進優化

組件用起來也很簡單,簡單一行代碼就出來了微信

<handle-button-old v-model="sortData"/>ide

sortData函數

sortData: [
    {
      fileType: 'text',
      content: '前端開發',
      index: 2,
      size: 12
    },
    {
      fileNmae: '251bb6d882024b11a6051d604ac51fc3.jpeg',
      fileType: 'image',
      fileUrl:
        'https://file-cdn-china.wechatify.net/marketing/sms/mms_material/53ce422f14e516af0eb9a5c7251cc1ca.jpeg',
      index: 3,
      size: 101109,
      fileName: '53ce422f14e516af0eb9a5c7251cc1ca.jpeg'
    },
    {
      fileType: 'text',
      content: '守候',
      index: 5,
      size: 12
    }
 ]
複製代碼

可是若是頁面上又有這樣一個需求,功能同樣,樣式排版不同,好比下圖這樣優化

而後組件就沒法使用了。

這個時候,確定不是複製一個文件,改下樣式再寫一個組件,只能把原來的組件改得通用些,能適合更多需求。

遇到這樣的需求,很是不建議複製一個文件,再寫一個組件。若是下次再有這種狀況,又要再複製一個文件,再寫一個組件。就可能會致使組件文件很是多,影響維護

讓組件更通用些,能適合更多需求,主要就是要把常常會變的因素抽取出來,交給用戶自定義,至於有哪些地方能夠改進優化?下面就簡單列舉一下

3-1.支持自定義內容

首頁,看到兩個需求,排版樣式和顯示字段就不同了。不知道之後第三種,第四種排版樣式,也不知道會顯示什麼字段。因此這裏最好是他操做按鈕抽出來,做爲組件封裝,至於怎麼排版,顯示什麼字段,組件無論,只須要提供一個 slot 由用戶自定義。

HandleButtonOld.vue

<template>
  <div class="ec-handle">
    <div
      class="ec-handle--item"
      v-for="(item,index) in value"
      :key="index"
      :class="{'cur':nowClickIndex===index}"
      @click="switchCur(index)"
    >
      <!-- 提供slot -->
      <slot :data="item"></slot>
      
      <ul class="customer-form-view-action-box">
        <!--重複代碼略-->
      </ul>
    </div>
  </div>
</template>
複製代碼

頁面調用

<handle-button-old v-model="sortData">
<!--提供 slot-scope 須要什麼字段以及排版能夠自定義-->
  <div slot-scope="item" class="view-item">
    <span v-if="item.data.fileType==='text'">{{item.data.content}}123</span>
    <video :src="item.data.fileUrl" v-if="item.data.fileType==='video'"></video>
    <audio :src="item.data.fileUrl" controls="controls" v-if="item.data.fileType==='audio'"></audio>
    <img :src="item.data.fileUrl" v-if="item.data.fileType==='image'" />
  </div>
</handle-button-old>
複製代碼

3-2.支持自定義選中樣式

來到這裏,看一下選中的效果,

除了顯示幾個操做按鈕以外,還有一個藍色的邊框線,可是不一樣需求,選中效果多是不同的,好比有一個地方要用灰色雙實線,再有一個地方要用白色實現,邊距增長 30px 等等。沒法猜想下一次用這個組件的時候,選中樣式是什麼。因此選中樣式不能在 handle-button-old 內部寫死或者判斷,只能讓用戶自定義。咱們能提供的,就是給一個字段,告訴用戶哪一項是當前選中的。

HandleButtonOld.vue

<template>
  <div class="ec-handle">
    <div
      class="ec-handle--item"
      v-for="(item,index) in value"
      :key="index"
      :class="{'cur':nowClickIndex===index}"
      @click="switchCur(item,index)"
    >
      <!--對 item 進行封裝-->
      <slot :data="getItem(item,index)"></slot>
      //代碼略 
    </div>
  </div>
</template>
<script>
export default {
  //代碼略
  methods: {
    getItem (item, index) {
      // 把索引($index) 和 當前是否選中($select) 字段合併到 item 裏面
      //這裏是順便把索引傳過去了,是爲了之後的不時之需,這裏不展開講
      return Object.assign({}, item, { $index: index, $select: this.nowClickIndex === index })
    }
    //代碼略
  }
}
</script>
複製代碼

頁面調用

<!--根據 $select 判斷是否添加 cur 這個 class-->
<handle-button-old v-model="sortData">
  <div slot-scope="item" class="view-item" :class="{'cur':item.data.$select}">
    //代碼略
  </div>
</handle-button-old>
<style lang="scss">
.view-item {
    padding: 10px;
    border:4px dashed transparent;
    &.cur{
      border:4px double #ccc;
    }
}
</style>
複製代碼

這樣就可讓用戶自定義選中的樣式了

3-2.設置操做按鈕的顯示位置和方向

再看一下兩個需求的樣式

首先看到按鈕的位置和方向是不同的。按鈕的位置,能夠給默認值,但也要讓用戶能夠自定義。要肯定按鈕的定位,則 handle-button-old 組件須要提供 top,right,bottom,left,四個參數。爲了方便定位,除了能夠設置具體像素,百分比以外,還要支持用戶輸入'center',方便用戶設置垂直或者水平居中。

按鈕的方向就須要提供 direction 參數,用戶輸入 horizontal 就垂直顯示,輸入 vertical 就水平顯示

handle-button-old

<template>
  <div class="ec-handle">
    <div
      class="ec-handle--item"
      v-for="(item,index) in value"
      :key="index"
      :class="{'cur':nowClickIndex===index}"
      @click="switchCur(item,index)"
    >
      <slot :data="getItem(item,index)"></slot>
      <!--綁定style,以及根據direction 設置 class,設置ul的樣式-->
      <ul class="customer-form-view-action-box"
          :style="ulPosition"
          :class="{'handle-vertical':direction==='vertical'}"
      >
        //代碼略
      </ul>
    </div>
  </div>
</template>
<script>
export default {
  //代碼略
  props: {
    //代碼略
    top: {
      type: [String, Number],
      default: '0'
    },
    bottom: {
      type: [String, Number],
      default: 'auto'
    },
    left: {
      type: [String, Number],
      default: 'auto'
    },
    right: {
      type: [String, Number],
      default: '-26px'
    },
    direction: {
      type: String,
      default: 'horizontal'
    }
  },
  computed: {
    ulPosition () {
      let obj = {
        left: this.left,
        right: this.right,
        top: this.top,
        bottom: this.bottom
      }
      let _x = '0'
      let _y = '0'
      if (this.top === 'center' || this.bottom === 'center') {
        obj.top = '50%'
        obj.bottom = 'auto'
        _y = '-50%'
        obj.transform = `translate(${_x},${_y})`
      }
      if (this.left === 'center' || this.right === 'center') {
        obj.left = '50%'
        obj.right = 'auto'
        _x = '-50%'
        obj.transform = `translate(${_x},${_y})`
      }
      return obj
    }
  }
}
</script>
<style lang="scss" scoped>
.ec-handle--item {
  position: relative;
  ul {
    position: absolute;
    right: -26px;
    top: 0;
    display: none;
    line-height: 24px;
    &.handle-vertical {
      li {
        display: inline-block;
        vertical-align: top;
      }
    }
  }
}
</style>

複製代碼

頁面調用

<!--設置按鈕的位置和方向-->
<handle-button-old
      v-model="sortData"
      direction="vertical"
      right="6px"
      bottom="center"
    >
  <div slot-scope="item" class="handle-item">
    //代碼略
  </div>
</handle-button-old>
export default {
  data () {
    return {
      iconByFileType: {
        text: 'icon-wenben',
        image: 'icon-tupian1',
        video: 'icon-shipin',
        audio: 'icon-yinpin',
        link: 'icon-duanlian'
      },
      //代碼略
    }
  },
  //代碼略
  methods: {
    formatSize (val) {
      if (val === 0) {
        return '0B'
      }
      let sizeObj = {
        MB: 1048576,
        KB: 1024,
        B: 1
      }
      val = +val
      for (let key in sizeObj) {
        if (val >= sizeObj[key]) {
          return +(val / sizeObj[key]).toFixed(2) + key
        }
      }
    },
    //代碼略
  }
}
</script>
<style lang="scss" scoped>
//代碼略
</style>
複製代碼

這樣效果就實現了

3-3.設置操做按鈕的顯示方式

想必你們已經看到問題了,【3-2】最後看到的結果是隻有其中一項是有操做按鈕的,而【3-2】一開始,看到的需求是全部的結果都要顯示出來。那麼這裏就要設置一個 display 屬性了,設置操做按鈕的顯示方式。目前提供三個值'default'-選中的項顯示,'visible'-全部項顯示,'none'-不顯示。

handle-button-old

<template>
  <div class="ec-handle">
    <div
      class="ec-handle--item"
      v-for="(item,index) in value"
      :key="index"
      :class="{'cur':nowClickIndex===index || display==='visible'}"
      @click="switchCur(item,index)"
    >
      <slot :data="getItem(item,index)"></slot>
      <ul class="customer-form-view-action-box"
          :style="ulPosition"
          v-if="display!=='none'"
          :class="{'handle-vertical':direction==='vertical'}"
      >
        //代碼略
      </ul>
    </div>
  </div>
</template>
<script>
export default {
  props: {
    display: {
      type: [String],
      default: 'default'
    },
    //代碼略
  },
  //代碼略
}
</script>
複製代碼

頁面調用

<handle-button-old
      v-model="sortData"
      direction="vertical"
      right="6px"
      display="visible"
      bottom="center"
    >
    //代碼略
</handle-button-old>
複製代碼

這樣就能實現了

3-4.點擊操做按鈕前的觸發動做

不少人在開發上會遇到一些需求,特別是在執行好比刪除,清空等「危險操做」以前,要給一個彈窗或者其餘方式的提醒,讓用戶謹慎操做。而這個組件的操做按鈕,也有多是「危險操做」。因此須要讓用戶能夠自定義操做前的回調。

拿文章說起的 handle-button-old 組件來講,若是需求是「刪除」按鈕前須要給提醒彈窗,其餘的按鈕直接操做。

handle-button-old

<template>
    <!--代碼略-->
</template>
<script>
export default {
  props: {
    beforeDelete: {
      type: Function
    },
    beforeUp: {
      type: Function
    },
    beforeDown: {
      type: Function
    },
    beforeEdit: {
      type: Function
    }
  },
  data () {
    return {
      nowClickIndex: '',
      eventType: '',
      curHandleIndex: ''
    }
  },
  methods: {
    /**
     * @description 執行事件
     */
    handle () {
      let _list = this.value
      let _nowItem = _list[this.curHandleIndex]
      switch (this.eventType) {
        case 'up':
          this.nowClickIndex--
          _list.splice(this.curHandleIndex, 1)
          _list.splice(this.curHandleIndex - 1, 0, _nowItem)
          break
        case 'down':
          this.nowClickIndex++
          _list.splice(this.curHandleIndex, 1)
          _list.splice(this.curHandleIndex + 1, 0, _nowItem)
          break
        case 'delete':
          _list.splice(this.curHandleIndex, 1)
      }
      this.$emit('input', _list)
      this.$forceUpdate()
      this.$emit(this.eventType, _nowItem, this.curHandleIndex)
    },
    /**
     * @description 處理事件
     */
    handleEvent (eventType, item, index) {
      // 記錄事件類型
      this.eventType = eventType
      // 記錄當前操做項的索引
      this.curHandleIndex = index
      let _type = eventType.substr(0, 1).toUpperCase() + eventType.substr(1)
      if (typeof this[`before${_type}`] === 'function') {
        // 把當前操做的函數,當前項,索引做爲參數,傳給調用函數
        this[`before${_type}`](this.handle, item, index)
      } else {
        this.handle()
      }
    },
  }
  // 代碼略
}
</script>

複製代碼

頁面調用

<template>
    <handle-button-old
      v-model="sortData"
      direction="vertical"
      right="6px"
      display="visible"
      bottom="center"
      :beforeDelete="handleBefore"
    >
      <!--代碼略-->
    </handle-button-old>
</template>
<script>
methods: {
    /**
     * @description 操做前的回調
     * @augments done - 用於執行操做
     * @augments item - 當前項
     * @augments index - 當前索引
     */
    handleBefore (done, item, index) {
      // 點擊確認才進行操做,點擊取消不作處理
      this.$confirm('確認進行刪除操做?')
        .then(() => {
          done()
        })
        .catch(() => {})
    }
  }
</script>
複製代碼

3-5.切換選中的項的觸發動做

好比有需求,點擊切換選中的時候,須要拿當前項的數據,作爲請求的參數。實現這個需求,只須要在 handle-button-old 組件裏面須要提供一個自定義事件便可

handle-button-old

methods:{
    switchCur (item, index) {
      this.nowClickIndex = index
      //觸發自定義事件
      this.$emit('change', item, index)
    }
}

複製代碼

頁面調用

<handle-button-old
  style="margin-bottom:500px;"
  v-model="sortData"
  direction="vertical"
  right="6px"
  display="visible"
  bottom="center"
  @change="handleChange"
>
  
</handle-button-old>
複製代碼

3-6.取消選中

可能你們在一早已經發現了這個問題,若是選中了某一項,出現了下面狀況。

可是若是需求是取消選中呢?那就作不到了。從代碼邏輯上來說,只要選中了,就要選中一項,沒辦法取消。因此,在 3-5 的 switchCur 函數就須要判斷一下,若是點擊的是當前項,就取消選中

handle-button-old

methods:{
    switchCur (item, index) {
      
      if (this.display === 'visible') {
        return
      }
      // 若是點擊的是當前項,就取消選中
      this.nowClickIndex = this.nowClickIndex !== index ? index : ''
      this.$emit('change', item, index)
    }
}
複製代碼

3-7.按鈕摺疊顯示

在上面的圖片能夠看到,按鈕要麼是橫向排列,要麼是豎向排列。若是哪天需求以爲按鈕太佔位置,須要摺疊顯示按鈕,這個也很簡單就能夠兼容了,給 handle-button-old 加個 type 參數判斷下要根據什麼方式顯示就能夠了。

handle-button-old

<template>
  <div class="ec-handle">
    <div
      class="ec-handle--item"
      v-for="(item,index) in value"
      :key="index"
      :class="{'cur':nowClickIndex===index || display==='visible'}"
      @click="switchCur(item,index)"
    >
      <slot :data="getItem(item,index)"></slot>
      <!--若是不是 dropdown 類型以及 dispaly 不爲 none-->
      <ul
        class="customer-form-view-action-box"
        :style="ulPosition"
        v-if="type!=='dropdown'&&display!=='none'"
        :class="{'handle-vertical':direction==='vertical'}"
      >
        <!--代碼略-->
      </ul>
      <!--若是是dropdown類型-->
      <el-dropdown v-else-if="type==='dropdown'" class="customer-form-view-action-box" :style="ulPosition" style="position:absolute;">
        <span class="el-dropdown-link">
          操做<i class="el-icon-arrow-down el-icon--right"></i>
        </span>
        <el-dropdown-menu>
          <el-dropdown-item><div @click.stop="handleEvent('edit',item,index)">編輯</div></el-dropdown-item>
          <el-dropdown-item v-if="index!==0"><div @click.stop="handleEvent('up',item,index)">上移</div></el-dropdown-item>
          <el-dropdown-item v-if="index!==value.length-1"><div @click.stop="handleEvent('down',item,index)">下移</div></el-dropdown-item>
          <el-dropdown-item><div @click.stop="handleEvent('delete',item,index)">刪除</div></el-dropdown-item>
        </el-dropdown-menu>
      </el-dropdown>
    </div>
  </div>
</template>
export default {
  name: 'HandleButton',
  componentName: 'HandleButton',
  props: {
    type: {
      type: String
    }
    // 代碼略
  }
  // 代碼略
}
複製代碼

頁面調用

type="dropdown" 以後,direction 參數會不起效

<!--type="dropdown" 摺疊顯示操做按鈕,不然爲平鋪並列顯示-->
<handle-button-old
  style="margin-bottom:500px;"
  v-model="sortData"
  direction="vertical"
  type="dropdown"
  right="6px"
  display="visible"
  bottom="center"
  :beforeDelete="handleBefore"
>
  <div slot-scope="item" class="handle-item">
    <div class="message-item___box">
      <span class="message-item___icon iconfont" :class="iconByFileType[item.data.fileType]"></span>
    </div>
    <div class="message-item___info">
      <p v-if="item.data.fileType==='text'">
        <span>{{item.data.content}}</span>
      </p>
      <p v-else>{{item.data.fileName}}</p>
      <span class="message-item___info_size">{{formatSize(item.data.size)}}</span>
    </div>
  </div>
</handle-button-old>
複製代碼

3-8.按鈕顯示方式

回到這個場景,可能你們在開發的時候已經想到了,要出現操做按鈕,必需要點擊某一項纔會出現。但不少時候的需求,須要鼠標 放上去的時候就顯示操做按鈕,不須要點擊。要實現這個,只須要添加 一個 trigger 參數,triggle 默認爲 'click'-點擊出現,'hover'-鼠標放上去出現

<template>
  <div class="ec-handle">
    <div
      class="ec-handle--item"
      v-for="(item,index) in value"
      :key="index"
      :class="{'cur':nowClickIndex===index || display==='visible'}"
      @click="switchCur(item,index,'click')"
      @mouseenter="switchCur(item,index,'hover')"
      @mouseleave="handleMouseLeave"
    >
        <!--代碼略-->
    </div>
  </div>
</template>
<script>
export default {
  props: {
    // 代碼略
    triggle: {
      type: String,
      default: 'click'
    }
  },
  methods: {
    // 加上 eventType 參數區分當前觸發的事件
    switchCur (item, index, eventType) {
      if (this.display === 'visible') {
        return
      }
      //若是當前觸發事件與 triggle 不一樣,則不執行操做
      if (eventType !== this.triggle) {
        return
      }
      this.nowClickIndex = this.nowClickIndex !== index ? index : ''
      this.$emit('change', item, index)
    },
    handleMouseLeave () {
        // 若是triggle 爲 hover ,鼠標移除的時候,取消選中當前項
      if (this.triggle === 'hover') {
        this.nowClickIndex = ''
      }
    }
  }
  // 代碼略
}
</script>
複製代碼

頁面調用

<handle-button-old v-model="sortData" triggle="hover">
  <!--代碼略-->
</handle-button-old>
複製代碼

3-9.關於其餘

首次 handle-button-old 這個組件爲例,列舉了一些改進優化的功能。若是想折騰,仍是有很多功能能夠折騰的,好比按鈕的樣式(圖標的顏色、形狀、背景顏色、大小等等)、間距,自定義按鈕等。至於要不要折騰,就看需求有沒有必要了,具體狀況,具體分析。文章的這個,仍是很簡單的一個組件,若是是複雜的組件,須要優化的點可能就更多了。

4.小結

封裝組件的時候,若是一開始對組件的要求比較單一,或者時間比較緊急,也能夠先封裝個有基本功能,能知足需求的組件。以後若是發現組件不能知足業務需求了,再進行改進和優化也不遲。

-------------------------華麗的分割線--------------------

想了解更多,和我交流,請添加我微信。或者關注個人微信公衆號:守候書閣

相關文章
相關標籤/搜索