hello 大家好,🙎🏻♀️🙋🏻♀️🙆🏻♀️
我是一个热爱知识传递,正在学习写作的作者,ClyingDeng
凳凳!
之前我们有对基于element-plus组件库的表格进行二次封装dyingtable,实现了表格的属性化配置。但是只有这些还是远远不够的呀!
去年,有个宝子就给我提了这样一个需求:我想要一个能够支持大数据的虚拟表格,跟那个element虚拟表格
差不多,但是我们又想要一个跟它有点不一样的,我们的每个单元格数据有的多有的少,想实现一个自适应行高
的,太规整了数据挤不下啊!🧐🧐🧐
那必须要得,安排啊🥳🥳🥳~
哐哐哐,一顿操作,花了几个月搞出来了。带大家来看看👀
首先,对于技术选型,我选择了我熟悉的vue+vite组合,这样写起来也顺手一些。
初始化
先初始化一个vite项目,分析我们接到的需求,该怎么去实现?🔜🔜🔜
为了使我们这个对组件库不产生依赖,可以独立使用,我是直接基于tbale原生标签去实现了表格的基础功能和它的虚拟功能。
表格内部子组件
在设计表格的时候,考虑到头部需要置顶的功能,我将表格拆成了两个table,一个用来表示table-header,一个用来做table-body。在上下滚动的时候将头部固定,身体滚动;左右滚动时将表头与表体进行联动。按这样的思路应该是可以去实现的。
table-column
根据表头与表体,提取公用组件,表格的单元格------table-column
。
作为基石的单元格,我考虑了几个主要的情况:
- 数据展示。接收
data
,格式可能是直接单元格渲染的数据,也可以是一个对象,如果是一个对象,可以指定对象传入的keyProp。后期还可以拓展一些插槽。 - 接受一些index序号、高度等作为后期拓展使用🤪🤪🤪
table-header
表头目前仅支持一级表头,感兴趣的可以帮忙完善多级表头(将内部的tr.children
进行循环)💐。
表头是基于表格的配置项二次封装的。通过传入表头的columns
来对表头的宽度、变量、对齐方式等进行配置。
因为表头做的是一级表头,所以我只考虑了虚拟表格的左右滚动功能。
左右滚动功能
表头我是给它指定了一个高度,宽度给了表格宽度的默认值600px
。
initLR
先考虑占满一屏需要多少数据,就会出现列过多、过少的情况。
- 列数据少。如果只有三列,没有指定列表
width
,就执行平分了;如果部分指定将剩余未指定的列平分;如果指定部分超过默认列宽,未指定部分按照默认宽度80px
渲染。 - 列数据多。指定宽度之和未超出默认表格宽度,剩余未指定的列平分剩余宽度;如果超出,未指定宽度的列使用默认宽度。
获取到屏幕宽度一屏数据后,考虑大数据列情况:🤔🤔🤔
表格有n列,使用分页初始化渲染三页数据--如果只有两页数据,可能会出现第二页数据列宽完全撑不满一屏宽度,为了解决白屏,我这边使用三页数据渲染,确保一屏有两屏幕以上的数据。
此外,需要考虑数据不足三页的情况,如果只有1~2页数据就直接平铺滚动。
这样成功初始化三页列数据。
scrollEvent
滚动事件监听,对数据进行操作。
表头左右滚动判断当前元素的scrollLeft
滚动位置,将上一次的滚动位置与当前的进行对比,如果当前位置大于之前滚动位置,表明当前操作是向右滚动,所以进行后置数据插入;反之,向左滚动,进行前置数据插入。
js
const scrollEvent = (e: any) => {
let scrollLeft = e.target.scrollLeft // 当前滚动的位置
emits('scrollLeft', scrollLeft)
// 0-pageSizeLR*pageNumLR
// 开始/结束位置
if (scrollLeft > oldscrollLeft.value) {
// 向右滚动
onLeftScroll(scrollLeft)
}
if (scrollLeft < oldscrollLeft.value) {
// 向左滚动
onRightScroll(scrollLeft)
}
}
向右滚动---onLeftScroll
向左拉,加载右侧后面的列数据。👉👉👉
通过widthMap
来记录每页的列宽度,在向右滚动时进行页宽度收集。
开始滚动,判断当前第一页列数据是否滚出可视区域:
- 如果超出,就进行列数据的新增,在
columnList
后push加上一页数据,且将widthMap
设置给列表的paddingLeft
保持当前滚动位置,继续触发滚动数据变化。 - 如果加载到最后不满一页 整个屏幕禁止滚动。
js
// 滚动
let scrollWidth = ref<any>(0)
const onLeftScroll = (scrollLeft: number) => {
let midChild = scrollBody.value.getElementsByTagName('th')[pageSizeLR.value] //第一页数据的高度
nextTick(() => {
// 渲染出的真实节点的最后一个子节点滚动的位置 加上 本身高度 减去 滚动的偏移量 是否占满不了一屏
if (midChild.offsetLeft < scrollLeft) {
// 最后边界不满一页数据也需要加载
if ((pageNumLR.value - 1) * pageSizeLR.value > props.columns.length) {
return
}
addDataFnLR() // 加数据
// console.log(midChild.offsetLeft, scrollLeft, widthMap.value, pageNumLR.value)
let arr = cloneDeep(columnList.value)
// 完全滚出页面的数据高度
scrollWidth.value = widthMap.value[pageNumLR.value - 4]
// 数据处理 超出的最前面一页的数据去除
columnList.value = arr.slice(Number(pageSizeLR.value), columnList.value.length)
// 去除数据后 使用padding占位
scrollBody.value.style.paddingLeft = scrollWidth.value + 'px'
nextTick(() => {
let second = scrollBody.value.getElementsByTagName('th')[columnList.value.length - 1]
widthMap.value[pageNumLR.value - 1] = second.offsetLeft + second.offsetWidth
oldscrollLeft.value = scrollLeft
// console.log('columnList', columnList.value, pageSizeLR.value)
//加载到最后不满一页 整个屏幕禁止滚动
if (columnList.value.length < pageSizeLR.value * 3) {
scrollWidthContainer.value = widthMap.value[pageNumLR.value - 1]
emits('maxScrollWidth', scrollWidthContainer.value)
return
}
//滚动触发数据变化
onLeftScroll(scrollLeft)
})
}
})
}
向左滚动---onRightScroll
向左滚动与向右滚动类似,当页面向右拉动。👈🏻👈🏻👈🏻
当最后一页数据超出宽度,将数据去除并在前置方向加上上一页数据,即在总列数据进行向前一页分页截取。
js
// 向上/左添加数据
const unshiftDataFnLR = (allData = props.columns) => {
let pageData = allData.slice(pageSizeLR.value * (pageNumLR.value - 5), pageSizeLR.value * (pageNumLR.value - 4))
if (pageData.length) columnList.value = pageData.concat(columnList.value)
pageNumLR.value--
}
table-body
表体依旧接收一些基础功能的参数,对于接收到的columns
进行列渲染,再通过接收表格数据参数dataList
,进行初始页面的渲染。
html
<tbody ref="scrollBody" class="scroll-container">
<tr v-for="(item, index) in dataList" :key="`tbody_${Math.random() * index}`" class="dy-vt-wrapper-tr">
<td
v-for="(column, i) in columnList"
:key="`tcolumn_${column[i]}_${Math.random() * index}`"
class="dy-table__cell"
:class="[
{ 'dy-table__cell-border': border },
`dy-table_cell-text-${alignDir.includes(column.align) ? column.align : 'center'}`
]"
:style="{
width: setColumnWidth(column).realWidth + 'px',
// @ts-ignore
height: heightItemMap[pageSize * (pageNum - 4) + index] + 'px'
}"
>
<dy-table-column :data="item" :index="index" :column="column" :key-prop="column.prop"></dy-table-column>
</td>
</tr>
</tbody>
依旧同表头组件类似,初始渲染三页数据,不同的是需要判断上下滚动、左右滚动两大方向。
向下滚动
表格向下滚动,不给单元格设置默认高度,单元格根据列宽自适应行高。
而我们操作的时候,通过js去获取元素每页渲染后的高度,滚动超出通过padding
进行填补。
js
// 滚动
let scrollHeight = ref(0)
const onDownScroll = (scrollTop: number) => {
[dataList.value.length - 1] //最后一个元素离顶部的距离
let midChild = scrollBody.value.getElementsByTagName('tr')[pageSize.value] //第一页数据的高度
oldScrollTop.value = scrollTop
nextTick(() => {
// 渲染出的真实节点的最后一个子节点滚动的位置 加上 本身高度 减去 滚动的偏移量 是否占满不了一屏
if (midChild.offsetTop < scrollTop) {
// 最后边界不满一页数据也需要加载
if ((pageNum.value - 1) * pageSize.value > props.data.length) {
return
}
addDataFn() // 加数据
let arr = cloneDeep(dataList.value)
// 完全滚出页面的数据高度
scrollHeight.value = heightMap.value[pageNum.value - 4]
// 数据处理 超出的最前面一页的数据去除
dataList.value = arr.slice(Number(pageSize.value), dataList.value.length)
nextTick(() => {
// 去除数据后 使用padding占位
scrollBody.value.style.paddingTop = scrollHeight.value + 'px'
let second = scrollBody.value.getElementsByTagName('tr')[dataList.value.length - 1]
heightMap.value[pageNum.value - 1] = second.offsetTop + second.offsetHeight
oldScrollTop.value = scrollTop
// 收集每页数据高度
collectItemHeight(
pageSize.value * (pageNum.value - 4) + dataList.value.length - pageSize.value * 3,
pageSize.value * (pageNum.value - 4) + dataList.value.length
)
//加载到最后不满一页 整个屏幕禁止滚动
if (dataList.value.length < pageSize.value * 3) {
scrollHeightContainer.value = heightMap.value[pageNum.value - 1]
return
}
//滚动触发数据变化
debounce(() => onDownScroll(scrollHeight.value), 500)
})
}
})
}
组件引用
当我们完成这个组件后,可以对外进行一个暴露。通过install方法进行export。
js
import DyVirtualTable from './virtual-table/index.vue'
import CanvasTable from './canvas-table/index.vue'
let components = [DyVirtualTable, CanvasTable]
const install = (Vue: any) => {
components.forEach((_: any) => {
Vue.component(_.name, _)
})
}
if (typeof window !== 'undefined' && (window as any).Vue) {
install((window as any).Vue) // 全局直接通过script 引用的方式会默认调用install
}
export default {
install
}
基础用法
columnList
表格配置项:
- 在
table
的配置项中用prop
属性来对应对象中的键名即可填入数据 - 用
label
属性来定义表格的列名 - 可以使用
width
属性来定义列宽 align
是每列的对齐方式,可以是left / center / right
,设置列左对齐、居中对齐、右对齐。
也可以配置表格的条纹:
以上数据是可以支持大数据和正常表格数据的哦,不需要使用两个不同的表格组件哦~
canvas-table
在使用div来实现一个虚拟表格后,凳凳还额外用canva画了一个,哈哈😂。
宝子们,可以使用看看,不过支持的应用场景比较少🌹。
原理类似,将我们获取到的表格宽高进行等宽、等高划分,获取初始一屏幕数据,向下滚动的时候,进行数据更新。
js
// 画外框
drawBorder(ctx, canvasWidth, canvasHeight)
// 表格头渲染
renderTHeader(ctx, canvasWidth, canvasHeight, row.value, col.value, regularHeadHeight)
// 画行 横线
drawRows(ctx, canvasWidth, canvasHeight, row.value, col.value, 0, regularHeadHeight)
// 竖线
drawCols(ctx, canvasWidth, canvasHeight, row.value, col.value, 0, regularHeadHeight)
// 表格数据渲染
renderData(ctx, canvasWidth, canvasHeight, row.value, col.value, 0, regularHeadHeight)