使用position:sticky实现表格固定列/固定表头

3,650 阅读4分钟

前言

比较久以前的一个需求了,之前写了只言片语在草稿箱,今天突然翻出来想写完它,大体的思路和实现是没有问题的,有些技术细节不太记得了,如有写错的或者不专业的,欢迎指正。

需求

  1. 实现一个可以固定列和表头的表格,动态列(50+)
  2. PC端,需要兼容Chrome、FireFox、Edge

技术

Vue3+TS+ElementUI

思路

  1. 使用ElementUI的Table组件:先用ElementUI的表格组件实现了一版,满足需求没问题,但是由于这个组件功能太强太复杂,当动态列达到50多个的时候,横向滚动条基本是卡死的,性能很差。
  2. 决定自己写,参考了一下ElementUI的Table组件实现和掘金里面的相关文章,基本都是这样实现的:固定列就是把原本的table body复制了一份,使用绝对定位覆盖在原来的表格上以达到固定的效果,DOM结构有些复杂,应该是为了兼容性和可扩展性考虑的,毕竟elementui可灵活配置要fixed的列。除了这些,由于header和body是独立的两个table,还需要在拖动横向滚动条时同步header和body的位置使它们不要错位。
  3. 由于实际需求不需要这么复杂的操作,于是注意到Position的sticky这个属性,想到可以把sticky属性设置在表头和需要固定的列的th/td上。这样的话既不需要去复制一份table body,也不需要横向滚动时同步header和body的位置(如果需要把滚动条只显示在表体,仍然需要做同步)。

mozilla的说明是:

元素根据正常文档流进行定位,然后相对它的最近滚动祖先(nearest scrolling ancestor) 和 containing block (最近块级祖先 nearest block-level ancestor),包括table-related元素,基于toprightbottom, 和 left的值进行偏移。偏移值不会影响任何其他元素的位置。 该值总是创建一个新的层叠上下文(stacking context)。注意,一个sticky元素会“固定”在离它最近的一个拥有“滚动机制”的祖先上(当该祖先的overflow 是 hiddenscrollauto, 或 overlay时),即便这个祖先不是最近的真实可滚动祖先。这有效地抑制了任何“sticky”行为(详情见Github issue on W3C CSSWG)。

我自己肤浅的理解: sticky = relative + fixed,当sticky元素未离开视口时,表现出来的特性为relative(跟随文档流);当离开视口时,表现出来的特性是fixed(固定在视口的某一位置)。

效果图

  1. 初始化的样子

image.png

  1. 固定表头和左侧第一列

image.png

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的元素的边框显示会有问题。 1643187641(1).jpg 解决办法: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;
}

最终效果:

image.png

补充

当然,这个效果还比较简陋,后面还可以优化的点:

  1. 给table header和 table body固定列的右侧加上阴影,在scrollTop/scrollLeft > 0 的时候显示出来
  2. 如果要将滚动条改为仅在表体中显示,则可以隐藏整个页面的滚动条,把滚动条显示在表体的table中,并且监听表体table的横向滚动,滚动时,同时调整table header的scrollLeft即可

参考