前言
比较久以前的一个需求了,之前写了只言片语在草稿箱,今天突然翻出来想写完它,大体的思路和实现是没有问题的,有些技术细节不太记得了,如有写错的或者不专业的,欢迎指正。
需求
- 实现一个可以固定列和表头的表格,动态列(50+)
- PC端,需要兼容Chrome、FireFox、Edge
技术
Vue3+TS+ElementUI
思路
- 使用ElementUI的Table组件:先用ElementUI的表格组件实现了一版,满足需求没问题,但是由于这个组件功能太强太复杂,当动态列达到50多个的时候,横向滚动条基本是卡死的,性能很差。
- 决定自己写,参考了一下ElementUI的Table组件实现和掘金里面的相关文章,基本都是这样实现的:固定列就是把原本的table body复制了一份,使用绝对定位覆盖在原来的表格上以达到固定的效果,DOM结构有些复杂,应该是为了兼容性和可扩展性考虑的,毕竟elementui可灵活配置要fixed的列。除了这些,由于header和body是独立的两个table,还需要在拖动横向滚动条时同步header和body的位置使它们不要错位。
- 由于实际需求不需要这么复杂的操作,于是注意到Position的sticky这个属性,想到可以把sticky属性设置在表头和需要固定的列的th/td上。这样的话既不需要去复制一份table body,也不需要横向滚动时同步header和body的位置(如果需要把滚动条只显示在表体,仍然需要做同步)。
mozilla的说明是:
元素根据正常文档流进行定位,然后相对它的最近滚动祖先(nearest scrolling ancestor) 和 containing block (最近块级祖先 nearest block-level ancestor),包括table-related元素,基于
top
,right
,bottom
, 和left
的值进行偏移。偏移值不会影响任何其他元素的位置。 该值总是创建一个新的层叠上下文(stacking context)。注意,一个sticky元素会“固定”在离它最近的一个拥有“滚动机制”的祖先上(当该祖先的overflow
是hidden
,scroll
,auto
, 或overlay
时),即便这个祖先不是最近的真实可滚动祖先。这有效地抑制了任何“sticky”行为(详情见Github issue on W3C CSSWG)。
我自己肤浅的理解: sticky = relative + fixed,当sticky元素未离开视口时,表现出来的特性为relative(跟随文档流);当离开视口时,表现出来的特性是fixed(固定在视口的某一位置)。
效果图
- 初始化的样子
- 固定表头和左侧第一列
demo
<html>
<head>
<style type="text/css">
.wrap{
height: 200px;
width: 150px;
}
.table_wrap{
width: 100%;
height: 100%;
overflow: auto;
}
.table_header {
position: sticky;
top: 0px;
z-index: 2;
}
table{
border-collapse: collapse;
border-spacing: 0;
table-layout: fixed;
}
th, td{
border: 1px solid #000;
background-color: #ddd;
}
.td_fixed{
position: sticky;
left: 0px;
z-index: 1;
}
</style>
</head>
<body>
<div class="wrap">
<div class="table_wrap">
<div class="table_header">
<table style="width: 210px">
<colgroup>
<col style="width: 70px" />
<col style="width: 70px" />
<col style="width: 70px" />
</colgroup>
<thead>
<tr>
<th class="td_fixed">index</th>
<th>col1</th>
<th>col2</th>
</tr>
</thead>
</table>
</div>
<div class="table_body">
<table style="width: 210px">
<colgroup>
<col style="width: 70px" />
<col style="width: 70px" />
<col style="width: 70px" />
</colgroup>
<tbody>
<tr>
<td class="td_fixed">start</td>
<td>1-1</td>
<td>2-1</td>
</tr>
<tr>
<td class="td_fixed">2</td>
<td>1-2</td>
<td>2-2</td>
</tr>
<tr>
<td class="td_fixed">3</td>
<td>1-3</td>
<td>2-3</td>
</tr>
<tr>
<td class="td_fixed">4</td>
<td>1-4</td>
<td>2-4</td>
</tr>
<tr>
<td class="td_fixed">5</td>
<td>1-5</td>
<td>2-5</td>
</tr>
<tr>
<td class="td_fixed">6</td>
<td>1-6</td>
<td>2-6</td>
</tr>
<tr>
<td class="td_fixed">7</td>
<td>1-7</td>
<td>2-7</td>
</tr>
<tr>
<td class="td_fixed">8</td>
<td>1-8</td>
<td>2-8</td>
</tr>
<tr>
<td class="td_fixed">end</td>
<td>1-7</td>
<td>2-7</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</body>
</html>
兼容性
从下图可以看到兼容性还是ok的,但是看效果图的第二张,可以发现,当拖动横向滚动条时,表头的下边框和第一列的右边框会无法显示,也就是设置了sticky的元素的边框显示会有问题。 解决办法:table的边框改为 border-collapse: separate,然后单独设置th/td各自的边框
table{
border-collapse: separate;
border-spacing: 0;
table-layout: fixed;
}
table th,td{
background: #ddd;
text-align:center;
border-right: 1px solid #000;
}
table th:first-child,td:first-child{
border-left: 1px solid #000;
}
.table_header table th{
border-top: 1px solid #000;
border-bottom: 1px solid #000;
}
.table_body table tr:not(:first-child) td{
border-top: 1px solid #000;
}
.td_fixed{
position: sticky;
left: 0px;
z-index: 1;
border-right: 1px solid #000;
}
最终效果:
补充
当然,这个效果还比较简陋,后面还可以优化的点:
- 给table header和 table body固定列的右侧加上阴影,在scrollTop/scrollLeft > 0 的时候显示出来
- 如果要将滚动条改为仅在表体中显示,则可以隐藏整个页面的滚动条,把滚动条显示在表体的table中,并且监听表体table的横向滚动,滚动时,同时调整table header的scrollLeft即可