vue組件之table表格

table表格組件預覽地址 (展現官網會略卡,一開始容易加載不出來)css

基於vue寫一個table組件

目前暫時打算完成的功能vue

  • 固定表頭
  • 固定列
  • 固定排序,接受排序函數,請求後端排序
  • 請求時期的動畫
  • 多選框
  • 展開行

結構和api借鑑AntDesign的,git

<x-table      
 :columns="columns1"
 :data="data">
 </x-table>
複製代碼

api設計:github

  • data,顯示的表格數據,是一個數組,數組裏的每一個對象都須要一個惟一的key好用來確認他們的index
{key:1,name:'JavaScript',price:80,year:12},
複製代碼
  • columns 表頭,裏面的屬性名對應data裏面的屬性名。
columns1:[
                {text:'名字',field:'name'},
                {text:'價格',field:'price'},
                {text:'年份',field:'year'},
            ],
複製代碼

固定表頭

效果如圖 web

實現原理和結構

遺憾的是table的頭部沒法只經過css去固定,這東西很是特殊。因而, 把這裏分爲兩個部分,bodyheaderchrome

header是一個嶄新的table,該table組件裏面只有<thead></thead>,經過絕對定位覆蓋body的頭部,以達到固定頭部的目的。後端

<template>  
   <div>
    <!-- 固定的頭部,header部分-->
   <table>
   <thead>
   </thead>
   </table>
   
    <!-- body部分-->
   <table>
   <tbody>
   </tbody>
   </table>
   </div>
</template>
複製代碼

Api設計與寬度的控制

首先固定頭部確定須要一個最大高度,也就是maxHeightapi

<x-table :columns="columns1"
                 :data="data"
                 //.....
                 maxHeight="300"
        >
        </x-table>
複製代碼

這裏的header部分的table由於是沒有body的,兩個table對應的每一個格子的寬度不相同,就致使了不對齊的問題,因而就須要固定寬度。數組

columns傳入的每條數據裏面加入width,意味着每列對應格子的寬度。瀏覽器

columns1:[
                    {text:'名字',field:'name',width:200},
                    ],
複製代碼

如今是如何控制格子的寬度,開始踩坑的時候我用js遍歷去給格子.style.width賦值,但這種作法是徹底不可行的。

在網頁上檢查了elementUI table組件的HTML結構後,發現是用 colgroup作的。

<colgroup>
                 <col v-for="(column,index) in columns" :key="index" :style="{width:`${column.width}px`}">
               </colgroup>
複製代碼

這裏還要考慮checkBox的影響,這在後面會說到。

而後就能夠用絕對定位覆蓋到上面。

position: absolute;
            left: 0;
            top: 0;
複製代碼

固定最大高度,超出部分能夠滾動

首先table的外層須要包裹一層div,用來控制最大高度。設置css:overflow:auto;。`

固定列

實現原理和固定頭部相似,可是複雜的多。 固定列分爲左邊固定和右邊固定,這須要用戶去設置 也就是說

columns1:[
            {.......,fixed:'left'},
            {........,fixed:'right'},
        ],
複製代碼

全部fixed:'left'的都放在左邊,fixed:'right'放在右邊。 如今總體就能夠分爲三部分了。左列固定,中間滾動區域,右列固定。

三個部分的的頭部數組收集

收集三個部分的數組,而且在格子的table裏面遍歷他們。

'收集函數'(){
           let [left,right,main] = [[],[],[]]
                this.columns.forEach(item=>{
                  [item.fixed].push(item)
                })
                    this.fixedLeft = left.concat(main,right)
                    this.fixedRight = right.concat(main,left)
                    this.scrollArea = left.concat(main,right)
     }
                
複製代碼

concat對作一個拼接,這樣子在外層div包裹的時候能夠直接用maxWidthoverflow:hidden截取顯示的部分。

//左邊固定
                   <table class='左邊'>
                        <colgroup>
                            <col style="width: 60px">
                            <col v-for="(column,index) in fixedLeft" :key="index" :style="{width:`${column.width}px`}">
                        </colgroup>
                        <thead>
                        <tr>
                         //.....
                            <th v-for="column in fixedLeft" :key="column.field">
                                    {{column.text}}
                            </th>
                        </tr>
                        </thead>
                    </table>
                    //中間滾動
                      <table class='滾動區域'>
                        <colgroup>
                            <col style="width: 60px">
                            <col v-for="(column,index) in fixedLeft" :key="index" :style="{width:`${column.width}px`}">
                        </colgroup>
                        <thead>
                        <tr>
                         //.....
                            <th v-for="column in fixedLeft" :key="column.field">
                                    {{column.text}}
                            </th>
                        </tr>
                        </thead>
                    </table>
                      //右邊固定
                      <table class='右邊'>
                        <colgroup>
                            <col style="width: 60px">
                            <col v-for="(column,index) in fixedLeft" :key="index" :style="{width:`${column.width}px`}">
                        </colgroup>
                        <thead>
                        <tr>
                         //.....
                            <th v-for="column in fixedLeft" :key="column.field">
                                    {{column.text}}
                            </th>
                        </tr>
                        </thead>
                    </table>
複製代碼

橫向滾動

須要注意的是table的寬度會受外面div包裹層的寬度影響。因此須要在開始就固定好table的寬度。而後給父級一個maxWidth

setMainWidth(){
            let [width,$refs] = [getComputedStyle(this.$refs.table).width,this.$refs]
            $refs.table.style.width = width
            $refs.wrapper.style.width = this.maxWidth +'px'
              //......
    },
複製代碼

結合固定頭部

由於考慮到固定列的同時還能固定頭部,左右的結構和以前的大致相同

<div class="main">
     <!-- 中間的頭部部分-->
     <table></table>
      <!-- 中間的body部分-->
     <table></table>
</div>
<div class="left">
    <!-- 左邊的頭部部分-->
     <table></table>
      <!-- 左邊的body部分-->
     <table></table>
</div>
<div class="right">
    <!-- 右邊的頭部部分-->
     <table></table>
      <!-- 右邊的body部分-->
     <table></table>
</div>
複製代碼

這裏固定左列固定右列除了css樣式外,還有些不一樣的地方。

  • checkbox,一旦存在左列固定,CheckBox必定算在左邊。
  • colgroup 左邊就須要考慮到CheckBox的佔位和寬度。
  • 固定右列須要考慮滾動條的寬度。(由於這裏滾動條尚未自制,可能會有樣式誤差)

hover同步變色

hover其中一部分其餘的一塊兒改變背景顏色

hoverChangeBg(index,e){
              let typeName = {
                  mouseenter:'#FCF9F9',
                  mouseleave:''
              }
              this.$refs.trMain[index].style.backgroundColor = typeName[e.type]
              if(this.fixedLeft.length>0){
                  this.$refs.trLeft[index].style.backgroundColor = typeName[e.type]
              }
              if(this.fixedRight.length>0){
                  this.$refs.trRight[index].style.backgroundColor = typeName[e.type]
              }
          },
複製代碼

圖畫表示

滾動條厚度的計算,消失與覆蓋

  • 首先讓左邊固定的部分右邊的滾動條消失
'不須要展現滾動條的部分'{
                  &::-webkit-scrollbar{
                  display: none;
              }
              -ms-overflow-style: none;
              scrollbar-width: none;
              -ms-overflow-style: none;
              overflow: -moz-scrollbars-none;
            }
            //兼容chrome,firefox和IE(?)和其餘大部分瀏覽器。
複製代碼
  • 接着右邊部分定位時設置
position:absolute;
right:0;
top:0;
複製代碼

覆蓋中間滾動區域的豎直滾動條

  • 獲取滾動條的厚度,兩邊固定部分高度減去那個厚度(如圖所示),若是沒有則爲0。
'我是獲取滾動條厚度的函數'(){
                 const scrollBar = document.createElement('div')
                     let style = {
                         height:'50px',
                         overflow:'scroll',
                         position:'absolute',
                         top:'-9999px',
                         width:'50px'
                     }
                      Object.keys(style).forEach(item=>{
                         scrollBar.style[item]=style[item]
                      })
                      document.body.appendChild(scrollBar)
                      this.scrollBarWidth= scrollBar.offsetWidth - scrollBar.clientWidth
                      document.body.removeChild(scrollBar)
   }
複製代碼

至於這個函數的兼容性問題,暫時沒有考慮。

同步滾動

最麻煩的一部分,至今尚未徹底解決,事實上在elementUI上也略微有點瑕疵。先說下個人解決過程。

最開始的嘗試(已放棄): 一開始使用mouserwheel監聽,但兼容性存在問題。

判斷瀏覽器是否爲火狐

const isFirefox = typeof navigator !== 'undefined' && navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
複製代碼

來監聽mouserwheel或火狐特立獨行的DOMMouseScroll。 固然,原生的監聽須要在beforeDestroy鉤子裏刪除一下。

const mousewheel = function(element, callback,name) {
    if (element && element.addEventListener) {
        element.addEventListener(isFirefox ? 'DOMMouseScroll' : 'mousewheel', function(event) {
            callback && callback.apply(this, [event,name]);
        });
    }
};

export default {
    bind(el, binding,name) {
        mousewheel(el, binding.value,name);
    }
};

複製代碼

大致想法是用deltaY控制其餘兩部分的scrollTop,然而火狐沒有這東西(至關的蛋疼),須要一些庫的支持。總之大致的寫法就是相似下面的

'須要同步滾動的部分'.scrollTop += e.deltaY
複製代碼

只這麼寫這樣子有個問題,就是滾滑輪的時候目標元素不必定在滾動,多是父級或者window,可是依然會觸發mousewheel事件。這樣子就會出現大量移位的狀況。 以後試了n種方法,

'須要同步滾動的部分'.scrollTop += e.deltaY
'wheel的那部分'.scrollTop  += e.deltaY
複製代碼

第二種方法: 使用原生的scroll事件,經過scrollTop來作同步

'須要同步滾動的部分'.scrollTop = scrollTop
複製代碼

須要監聽三個部分的scroll事件,可是一旦其中一個觸發了scroll事件,就會修改其餘兩個的scrollTop,以後又會觸發其餘兩部分的scroll事件。也就是說假設監聽調用的都是同一個函數,那麼滾一次就會調用三次這個函數。(在非Chrome瀏覽器上實際額外觸發的次數更多,這也是滾動緩慢的緣由) 這種狀況在除了chrome外的瀏覽器滾動十分緩慢。 目前嘗試的兩個方法:

  • 使用原生的addEventListenerremoveEventListener。控制scrollTop以前移除監聽,以後再監聽。
  • 添加hover監聽,只有hover的區域能夠觸發scroll
scrollGradient(part){
              if(part!=='正在hover的區域')return
                let position = {
                    left:[`tableLeftWrapper`,`tableMainWrapper`,`tableRightWrapper`],
                    main:[`tableMainWrapper`,`tableLeftWrapper`,`tableRightWrapper`],
                    right:[`tableRightWrapper`,`tableMainWrapper`,`tableLeftWrapper`],
                }
                let scrollTop = this.$refs[position[part][0]].scrollTop
                //........
                }
複製代碼
  • 事實上在pc上滾動滾動條的時候鼠標也是懸浮在改滾動條的元素區域內的,因此這方法看似是沒有問題的。 然而在正在看其餘程序的時候,hover網頁是不觸發的,這時候就會直接return了。

並且有時候mousewheel的區域並不必定會滾動,多是其餘的元素(例如window)

目前的解決方法:每次計算scrollTop的時候作一個記錄,每次觸發scrollGradient的時候作一個判斷,當前元素的scrollTop是否等於記錄的scrollTop,是就return。這樣子就能確保每次某個部分滾動並修改其餘部分的scrollTop的時候,不會有額外的操做。

滾動錯位

在同步滾動的過程當中,不免會由於修改元素的scrollTop從而再次觸發scroll的監聽函數或者是滾動a元素的同時,快速切換滾動b元素,觸發回調再次修改a元素的scrollTop。 這可能會引起

  • 局部沒有渲染或者沒有迴流等,致使的大量顯示錯位甚至空白。
  • 極高的開銷,由卡頓掉幀帶來的很差的體驗。

目前嘗試過的方法

  • pointer-events:none(毛用沒有)
  • vue的passive(沒有解決)
  • div層覆蓋(反而有bug)
  • 原生的防抖和節流(效果不太滿意,由於須要平緩的滾動效果),可是實測滾動限速頗有效。
  • 在Firefox下,滾動一次會觸發屢次,有着良好的滾動效果,而且幾乎沒有出現錯位bug。
  • (借鑑了一下element源碼後)阻止兩邊的mousewheel,修改中間滾動部分的scrollTop,而後由中間滾動部分來同步兩邊的scrollTop。(bug依然會出現,可是出現次數少了不少)

重繪和迴流

封裝函數

最後就是在Firefox採用監聽三個部分的scroll,用其中一個的scrollTop來同步其餘部分scrollTop的老方法。

其餘瀏覽器用監聽兩邊固定部分的mouserwheel事件,禁止兩邊的wheel。控制中間滾動區域的scrollTop,而後再給兩邊的scrollTop賦值從而達到三部分同步。好處在於在幾個部分滾動切換的過程當中下降了在滾動當前元素的同時再次去修改自身的scrollTop的次數。而這種作法的確下降了錯位出現的頻率。

//同步滾動
const isFirefox = typeof navigator !== 'undefined' && navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
const data={
    currentScrollLeft:0,
    currentScrollTop:0,
}
const wheel =  function fixedWheel(e,target,scrollArea){
    //Chrome
    //....
    event.preventDefault()
    '中間滾動區域'.scrollTop += e.deltaY
    //....
}

const scroll = function (event,el,partArr) {
//Firefox
   let {scrollTop} = el
    if(data.currentScrollTop===scrollTop)return
    //....
       this.$refs.tableMain.classList.remove('transformClass')
            window.requestAnimationFrame(()=>{
            //'重繪以前調用這個回調'
              '若是存在的話'&&'其餘部分的'.scrollTop = scrollTop//多是中間的滾動區域,也多是兩邊的固定區域
                   window.requestAnimationFrame(()=>{
                        this.$refs.'中間滾動區域'.classList.add('transformClass')
                    })
               })
}
const xScroll = function('一些參數') {
    if (el && el.addEventListener) {
         el.addEventListener(!isFirefox?'mousewheel':'scroll', function(event) {
             !isFirefox && wheel.apply(this, ['一些參數'])
             isFirefox && scroll.apply(this, ['一些參數'])
        })
    }
}

export default {
    bind(el, binding,name) {
        xScroll('一些參數');
    },
    data
};

複製代碼

要使用的時候只需

<template>
<div v-xScroll>
    <table></table>
</div>
</template>
import xScroll from './同步滾動'
    export default {
        directives:{
            xScroll
        },
複製代碼

最後的一個效果

目前的table組件就是練練手,有問題的地方但願指出。最後,厚顏無恥的求個贊,若是你以爲還能夠的話,哈哈。

相關文章
相關標籤/搜索