vue组件之table表格

10,649 阅读5分钟

table表格组件预览地址 (展示官网会略卡,一开始容易加载不出来)

基于vue写一个table组件

目前暂时打算完成的功能

  • 固定表头
  • 固定列
  • 固定排序,接受排序函数,请求后端排序
  • 请求时期的动画
  • 多选框
  • 展开行

结构和api借鉴AntDesign的,

 <x-table      
 :columns="columns1"
 :data="data">
 </x-table>

api设计:

  • data,显示的表格数据,是一个数组,数组里的每个对象都需要一个唯一的key好用来确认他们的index
{key:1,name:'JavaScript',price:80,year:12},
  • columns 表头,里面的属性名对应data里面的属性名。
          columns1:[
                {text:'名字',field:'name'},
                {text:'价格',field:'price'},
                {text:'年份',field:'year'},
            ],

固定表头

效果如图

实现原理和结构

遗憾的是table的头部无法只通过css去固定,这东西非常特殊。于是, 把这里分为两个部分,bodyheader

header是一个崭新的table,该table组件里面只有<thead></thead>,通过绝对定位覆盖body的头部,以达到固定头部的目的。

<template>  
   <div>
    <!-- 固定的头部,header部分-->
   <table>
   <thead>
   </thead>
   </table>
   
    <!-- body部分-->
   <table>
   <tbody>
   </tbody>
   </table>
   </div>
</template>

Api设计与宽度的控制

首先固定头部肯定需要一个最大高度,也就是maxHeight

        <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组件就是练练手,有问题的地方希望指出。最后,厚颜无耻的求个赞,如果你觉得还可以的话,哈哈。