本文介紹內容包括:javascript
- Element UI 實現表頭表列固定思考與總結
translate3d
如何實現表頭表列固定
書承上文,在前文【Vue進階】青銅選手,如何自研一套UI庫中介紹了Vue組件庫的開發細節,舉例實現了button、table等組件的開發。在Ange這個UI庫中,我實現了一個內容高可定製的表格組件:可固定表頭和表列,內容則自行定義。html
首先要認可,這個table組件實現的功能很簡單:java
表格組件是UI庫裏面最爲複雜的組件之一,項目中使用表格的場景特別多,咱們很難覆蓋全部人的需求,比較常見的就有:git
從做用對象來看,這些需求又可歸爲影響佈局(Eg: 固定表頭表列)和影響數據(Eg: 勾選數據)兩個大類。在Ange UI的table組件中,僅僅實現了影響佈局這個類下面的部分功能,該組件不操做數據,甚至具體到使用tr、td標籤(以及td裏面如何包裹數據)展現數據也是由使用者本身定義的。狠狠點擊這裏在線查看示例,或者查看代碼:github
<ag-table offsetTop="57.5">
<tr slot="thead">
<!-- 定義表頭列 -->
<th v-if="isExpand">姓名</th>
<th v-for="(each, index) in singleTableHead" :key="index">{{ each }}</th>
</tr>
<tr v-for="(each, index) in singleTableBody" slot='tbody' :key="`tbody-${index}`">
<!-- 渲染表體內容 -->
<td v-if="isExpand">{{ each.name }}</td>
<td>{{ each.verdict }}</td>
<td>{{ each.song }}</td>
</tr>
</ag-table>
複製代碼
經過插槽slot指定thead或是tbody。簡單就意味着精細和可拓展性強,同時帶來的問題就是用戶的使用成本高了(好比實現數據選擇功能,固然ag-table
在不操做源數據的原則下也能拓展出這個功能)。瀏覽器
從瀏覽器中審查Element table組件的渲染效果看,Element實現固定表頭表列的方式是:將固定的部分(如表頭)和不固定的部分(如表體)拆分放在不一樣區域(不一樣的div下),設置表體所在區域可滾動便可,而後再經過必定的手段(如陰槽、表格數據備份)去同步不一樣區域之間的佈局。app
在一篇餓了麼專題的文章中,詳細闡述了固定表頭表列的實現。下面簡單總結並整理其中存在的問題。佈局
從瀏覽器中審查table組件的渲染效果看: post
el-table__header-wrapper
&
el-table__body-wrapper
,如此表體內容超出容器高度時,會出現滾動條,只在本身區域內滾動,達到了表頭固定的效果。這樣的實現
致使了兩個問題:
針對上面的問題,element也作了處理,引用餓了麼文中一張圖片: 性能
這種實現方式有什麼缺點呢?
實現固定表列相對比較複雜,爲實現這個功能,element可謂是付出了「巨大的成本」。在這個左右列固定的渲染效果中:
el-table__header-wrapper
&
el-table__body-wrapper
是表體區域,
el-table__fixed
是左固定列區域、
el-table__fixed-right
是右固定列區域),每一份表格又有2個table,
一共是6個table;經過設置左右區域絕對定位和寬度實現固定的效果。
這樣實現會有什麼問題呢?
基於此,Ange UI的table實現考慮用另一種方式去實現,達到了最低的DOM成本。
在介紹固定表頭表列實現方法以前,先科普下getBoundingClientRect這個API。
getBoundingClientRect()方法返回元素的大小及其相對視口的位置,它的返回值是一個DOMRect對象。DOMRect對象包含了一組用於描述邊框的只讀屬性:left、right、top、bottom,單位爲像素。除了width和height外的屬性都是相對於視口的左上角而言的。
以下圖:
在一個table中分別用thead和tbody展現表頭表體,以下代碼:
<template>
<div class="ange-table">
<table ref="middle-table">
<thead class="thead-middle" :style="theadStyle">
<slot name="thead" />
</thead>
<tbody>
<slot name="tbody" />
</tbody>
</table>
</div>
</template>
複製代碼
監聽頁面滾動事件,計算table的位移,使用translate3d
反向設置thead的y軸位移值,達到固定表頭的效果。以下圖:
translate3d(0px, -top2, 0px)
。這樣,thead就一直處在頁面頂端位置了。 在某些場景下,thead達到Header的位置時就應該被fixed了,那們咱們能夠設置一個
offsetTop
參數,用戶自定義偏移值,thead在
top=0 - offsetTop
時被fixed。看關鍵實現代碼:
export default {
data () {
return: {
fixed: { // fixed狀態
top: false
},
clientRect: { // 位移值
top: 0
}
}
},
computed: {
theadStyle () {
const { top } = this.clientRect
return {
transform: `translate3d(0px, ${this.fixed.top ? -top : 0}px, 1px)`
}
}
},
watch: {
'clientRect.top': function (val) {
// 判斷到DOMRect的top值小於0時,開始fixed
this.fixed.top = val < 0
}
},
mounted () {
// 監聽頁面滾動事件,獲取table對象的DOMRect屬性
window.addEventListener('scroll', this.scrollHandle, {
capture: false,
passive: true
})
},
methods: {
scrollHandle () {
const $table = this.$refs.table
if(!$table) return
const { top } = $table.getBoundingClientRect()
this.clientRect.top = Math.floor(top - parseInt(this.offsetTop, 10))
}
}
}
複製代碼
結合 @前言 部分ag-table的使用示例,在<ag-tbale>
中傳入一個offsetTop
參數,便可實現thead在指定位置的fixed。另因爲thead和tbody在同一個table中,不須要維護每一列的寬度,它能夠根據內容自適應。查看demo。
固定列的實現須要三個表格(分別固定左列和右列),以下代碼:
<template>
<div class="ange-table">
<!-- left table -->
<table v-if="hasLeftTable" ref="leftTable" :style="leftStyle">
<thead class="thead-left" :style="theadStyle">
<slot name="leftThead" />
</thead>
<tbody>
<slot name="leftBody" />
</tbody>
</table>
<!-- middle table -->
<table ref="table" class="table-middle">
<thead class="thead-middle" :style="theadStyle">
<slot name="thead" />
</thead>
<tbody>
<slot name="tbody" />
</tbody>
</table>
<!-- right table -->
<table v-if="hasRightTable" ref="rightTable" :style="rightStyle">
<thead class="thead-right" :style="theadStyle">
<slot name="rightThead" />
</thead>
<tbody>
<slot name="rightBody" />
</tbody>
</table>
</div>
</template>
複製代碼
table橫向滾動時,計算容器的橫向滾動距離scrollLeft
,使用translate3d
反向設置左table的x軸位移值,固定左列;對於右table,先要將其初始位置設置在容器的最右端,橫向滾動時再結合scrollLeft設置x軸的位移值。以下圖:
$rightTable.right - $container.right
,
leftTable就是0;發生橫向滾動時,
leftTable的橫向位移值:
scrollLeft
,
rightTable的位移值:
初始位移 - scrollLeft
。看關鍵實現代碼:
export default {
computed: {
leftStyle () { // 左側表格位移
const { left } = this.clientRect
return {
transform: `translate3d(${this.fixed.left ? left : 0}px, 0px, 1px)`
}
},
rightStyle () { // 右側表格位移
const { right } = this.clientRect
return {
transform: `translate3d(${-right}px, 0px, 1px)`
}
}
},
watch: {
'clientRect.left': function (val) {
// 橫向滾動距離爲正,開始設置fixed
this.fixed.left = val > 0
}
},
mounted () {
// 存在由表格時設置其初始位移
if(this.hasRightTable) {
const container = this.$refs.container.getBoundingClientRect()
const rightTable = this.$refs.rightTable.getBoundingClientRect()
this.clientRect.right = Math.floor(rightTable.right - container.right)
// 記錄右表格初始位移值
this.initRight = this.clientRect.right
}
// 監聽表格容器的滾動事件
this.$refs.container.addEventListener('scroll', this.scrollXHandle, {
capture: false,
passive: true
})
// ...
},
methods: {
scrollXHandle () {
// ...
this.clientRect.left = Math.floor(this.$refs.container.scrollLeft)
const right = Math.floor(this.initRight - this.$refs.container.scrollLeft)
this.clientRect.right = right
}
}
}
複製代碼
按照這個思路實現左右列固定,效果以下(在線查看):
最後一步,由於這個表格是由三份table組成,所以當鼠標hover在其中一個table行上時,須要在剩餘兩個table的對應行中同步hover效果。看關鍵代碼的實現:
export default {
mounted () {
if(this.hasLeftTable || this.hasRightTable) {
// 定義鼠標hover事件
this.$el.addEventListener('mouseover', this.mouseOver, false)
this.$el.addEventListener('mouseout', this.mouseLeave, false)
}
},
methods: {
mouseOver (e) {
this.hoverClass(e, 'add')
},
mouseLeave(e) {
this.hoverClass(e, 'remove')
},
hoverClass(e, type) {
const tr = e.target.closest('tr')
if(!tr) {
return
}
const idx = tr.rowIndex // 當前hover行的編號
const trs = querySelectorAll(`tbody tr:nth-child(${idx})`, this.$el)
if(trs.length === 0) {
return
}
// 對全部tbody下同一編號的tr添加hover類
trs.forEach(each => {
each.classList[type]('hover')
})
}
}
}
複製代碼
經過translate3d
設置左右列的位移實現固定列的效果,避免了:
table組件一直是開發複雜度較高的組件,既要考慮性能,又要考慮儘量地對開發者使用友好。在此拋磚引玉,提供另外一種開發思路,只爲給有計劃開發table組件的你提供一點幫助。
固然你有其餘的想法歡迎評論一塊兒交流~
The end.