table表格组件预览地址 (展示官网会略卡,一开始容易加载不出来)
基于vue写一个table组件
目前暂时打算完成的功能
- 固定表头
- 固定列
- 固定排序,接受排序函数,请求后端排序
- 请求时期的动画
- 多选框
- 展开行
结构和api借鉴AntDesign的,
"columns1"
:data="data">
复制代码
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去固定,这东西非常特殊。于是, 把这里分为两个部分,body
和header
。
header
是一个崭新的table
,该table
组件里面只有,通过绝对定位覆盖
body
的头部,以达到固定头部的目的。
复制代码
Api设计与宽度的控制
首先固定头部肯定需要一个最大高度,也就是maxHeight
。
"columns1"
:data="data"
//.....
maxHeight="300"
>
复制代码
这里的header部分的table
因为是没有body
的,两个table
对应的每个格子的宽度不相同,就导致了不对齐的问题,于是就需要固定宽度。
在columns
传入的每条数据里面加入width
,意味着每列对应格子的宽度。
columns1:[
{text:'名字',field:'name',width:200},
],
复制代码
现在是如何控制格子的宽度,开始踩坑的时候我用js遍历去给格子.style.width
赋值,但这种做法是完全不可行的。
colgroup
做的。
"(column,index) in columns" :key="index" :style="{width:`${column.width}px`}">
复制代码
这里还要考虑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
包裹的时候可以直接用maxWidth
和overflow:hidden
截取显示的部分。
//左边固定
'左边'>
"width: 60px">
"(column,index) in fixedLeft" :key="index" :style="{width:`${column.width}px`}">
//.....
"column in fixedLeft" :key="column.field">
{{column.text}}
//中间滚动
'滚动区域'>
"width: 60px">
"(column,index) in fixedLeft" :key="index" :style="{width:`${column.width}px`}">
//.....
"column in fixedLeft" :key="column.field">
{{column.text}}
//右边固定
'右边'>
"width: 60px">
"(column,index) in fixedLeft" :key="index" :style="{width:`${column.width}px`}">
//.....
"column in fixedLeft" :key="column.field">
{{column.text}}
复制代码
横向滚动
需要注意的是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'
//......
},
复制代码
结合固定头部
因为考虑到固定列的同时还能固定头部,左右的结构和之前的大体相同
"main">
"left">
"right">
复制代码
这里固定左列和固定右列除了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外的浏览器滚动十分缓慢。 目前尝试的两个方法:
- 使用原生的
addEventListener
和removeEventListener
。控制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
};
复制代码
要使用的时候只需
import xScroll from './同步滚动'
export default {
directives:{
xScroll
},
复制代码
最后的一个效果
目前的table
组件就是练练手,有问题的地方希望指出。最后,厚颜无耻的求个赞,如果你觉得还可以的话,哈哈。