9

今天,你的浏览器 “滚动” 了吗?

在 Web 页面中,一个有高度或者宽度的容器是最常见的构成元素,而在其中的子元素有很大的概率超过父容器的尺寸限制,我们称之为“溢出”。而应对“溢出”,隐藏或者滚动是最常见的处理方式。滚动,作为 FEers 最经常处理的一种行为,却因为不同浏览器的各种表现形式让大家头痛不已,今天笔者从自身维护的组件出发,和大家分享一下自己在处理滚动和滚动条时遇到的问题,以及解决的办法,希望能够给你在解决同类问题时带来一些启发。同时本文也是 “从零开始的 React 组件开发之路” 系列的第二篇 - 表格篇番外。

遇到的问题 1:尴尬的双滚动条

笔者在团队中负责基础组件的开发和维护,作为一个 B 类业务较多的团队,表格是最常用和需求最为旺盛的组件,假设有下方这样一个最简单的表格结构。

图1:最简单的表格结构

因为空间有限,我们希望表格高度限定,这样势必引入表格上下滚动的情况。同时,为了查看的方便,我们希望表格头不会一起滚动,即表格头需要固定,只有表格体滚动,因此我们需要把表格头和表格体放入两个容器中,而只让表格体的容器滚动。这是很普通的需求,也很容易实现,到目前为止一切都很顺利。

图2:表头固定,表格体滚动

然而这一切的美好,随着表格列数的增多,变的有了一点乌云。因为页面宽度受到电脑屏幕的限制,我们往往对表格的宽度也有限制,不可能无限延展开。那么如果有很多的列呢?显然,让表格左右滚动是一个很自然的想法。由于我们的表头和表格体在两个不同的容器中,让这件事变的稍微麻烦一点。关于如何让表头和表格体同步左右滚动,不是这篇文章讨论的重点,所以不做详细讨论,简单来说,我们通过监听横向滚动的事件和不断获取当前的 scrollLeft 来获得同步。有一个麻烦的点是,我们不希望只能通过滚动表格体来实现表格滚动,也希望可以通过滚动表头来实现表格的左右滚动,这就要求必须设置表头的容器为 overflow-x: auto

图3:由于表格头和表格体都需要横向滚动,会引入两个滚动条。

这显然突破了大多数人对表格的认知,横向滚动会有两个滚动条,一点都不美观,需要我们在这个基础之上进行优化。表格体上的横向滚动条是没有问题的,主要问题在于表格头的,我们既希望能够横向滚动,又不想看到那个该死的滚动条,怎么办呢?想办法隐藏掉他就好了!首先我们设置表格头的容器 overflow-x: scroll 以保证无论是否需要横向滚动都会出现滚动条,方便我们简化状态的判断。接下来我们可以再设置表格头的容器 margin-bottom: -scrollBarWidth 来隐藏让他的父级帮忙吞掉这个滚动条,一切就大功告成了。但令人头大的是,滚动条的尺寸在不同浏览器,甚至是不同系统(例如 Windows 和 Mac 下的 chrome)中都是不一样的! 我们无法很暴力地通过制定一个固定的值来做这件事,因此我们需要在表格渲染到页面上去之后,主动去探测滚动条的宽度。

const scrollbarMeasure = {
  position: 'absolute',
  top: '-9999px',
  width: '50px',
  height: '50px',
  overflow: 'scroll',
};

let scrollbarWidth;

const measureScrollbar = () => {
  if (typeof document === 'undefined' || typeof window === 'undefined') {
    return 0; // 如果 document 不在,则证明不在浏览器环境,直接返回,兼容 node server render。
  }
  if (scrollbarWidth) {
    return scrollbarWidth; // 滚动条在固定的环境下宽度不会改变,因此只做一次探测即可,优化性能。
  }
  const scrollDiv = document.createElement('div');
  Object.keys(scrollbarMeasure).forEach((scrollProp) => {
    if (Object.prototype.hasOwnProperty.call(scrollbarMeasure, scrollProp)) {
      scrollDiv.style[scrollProp] = scrollbarMeasure[scrollProp];
    }
  }); // 创造一个远离人世的带滚动条的 div 用于探测,用户对于此无感知。
  document.body.appendChild(scrollDiv);
  const width = scrollDiv.offsetWidth - scrollDiv.clientWidth; // 获取滚动条的宽度,offsetWidth 和 clientWidth 的区别,你能说清楚吗?
  document.body.removeChild(scrollDiv); // 探测完成,销毁测试元素,减少对页面的影响。
  scrollbarWidth = width; // 缓存结果,优化性能
  return scrollbarWidth;
};

遇到的问题 2:对不齐的表头和数据

通过上面的方法,我们成功地隐藏了表格头的横向滚动条。稍微滚动一下,一切正常,一切都按照预想的执行,直到一直滚动到头,问题出现了,最后一列的表头和下面的数据居然是对不齐的!!

图4:当表格体又可以左右滚动时,问题开始复杂起来~

这是怎么回事呢?

原来,因为我们允许表格体上下滚动,使得在容器右侧出现了一个纵向的滚动条。而表头因为我们希望他是固定的,因此放在了另一个容器中,这导致他不能共享这个滚动条。因此,这使得表格体拉到头的时的 scrollLeft 也正好是表格头到头的位置,两个到头的位置上差了一个滚动条的宽度!看到这里,也许有的小伙伴可能会开始自己操练起来看看是不是这样,然后发现并没有类似问题,大呼坑爹,他们看到的情况大致如下图:

图4:Mac 某些设置下,看到的是另一份景象。

这引出了一个 Mac 下一个比较好玩的小设置,在 Mac 下滚动条何时显示也可以配置,大致分为三类,具体的配置可以在 系统偏好设置 -> 通用 中看到。

当我们选择 滚动时 的时候,只有当我们滚动一个元素的时候才会显示滚动条,且这个滚动条是飘在内容上,不会占据体积,于是便能看到 图3 中的情景。这个兼容性上升到了系统的程度,在 PC 上还是比较少见的(笑)。

所以这个问题只会在这种情况下没有出现,在 Mac 下选择 始终显示,也同样会出现。

那么如何解决这个问题呢?

其实,从上面的分析中我们也可以也大致地找到了问题的根源,表头没有共享表格体的纵向滚动条,那么我们只要想办法解决这个就好了。这个说起来简单,但毕竟不是在同一个容器中,如何共享呢?方案一:插入一个和滚动条相同宽度的 dom 元素充当滚动条,但这会引起另一个问题就是表格头和表格体的实际宽度不同,在各种滚动计算上引发很多麻烦。方案二:滚动条虽然在不同系统、不同浏览器里都不一样尺寸,但却有个优点就是,只要在同一系统、统一浏览器里不管因为什么原因,出现在什么地方,他的尺寸总是保持一致的。利用这个特点,我们可以设置表头容器的 overflow-y: scroll,总是包含一个纵向滚动条的道,这样就兵不血刃的解决了这个麻烦的问题。

图5:利用空的滚动条连接下面的滚动条来就可以解决上面的问题。

但这样又引入了新的问题

这样虽然比较好的解决了表格体有滚动条的情况,但是如果表格体没有滚动的情况下,遇到的问题就正好逆转了,又会出现对不齐的情况!

图6:当表格体没有纵向滚动条的情况下,又会出现新的问题。

解决这个问题也有几种思路,简单一点的思路可以模仿上面,给表格体也设置 overflow-y: scroll,这样不管是否有滚动,看起来都是一样而且不会错位了。但是这种方法并不十分美观,尤其在 windows 下显得比较丑陋。于是在此方案基础之上,我们加入了对于表格体纵向滚动的检测,当滚动区域的高度不大于容器高度时,即可认为没有滚动,此时同时设置表头和表格体 overflow-y: hidden ,就可以达到解决上述问题,而在没有滚动的情况下也保持美观的要求。

遇到的问题3:列固定下的整体横向滚动

解决了上面两个问题之后,一个完整的支持表头固定和横向滚动的表格就完成了。接下来又有了新的需求,要在原有表格基础之上,支持左右侧列固定。这也是表格中比较常见的需求,一些数据列或者操作列处于高频使用下,希望能够固定,这时就产生两种处理表格体横向滚动条的方式。

图7:支持左右列固定后的方案 A 和 B

方案 A 模仿表头的设计方案,将固定的列放在另一个容器当中,把需要滚动的列单独放置在一个可以滚动的容器当中。这种方案的问题在于,固定列的宽度和列数都不像表头一样固定且较小。当固定区域很大的时候,会严重挤压中间滚动区域滚动条的可操作区域,影响用户体验。同时他也会遇到和表头一样,滚动至最后一行对不齐的情况。方案 B 则比较好的解决了方案 A 的第一个问题,左右侧漂浮在表格的左右两边,覆盖对应的固定区域,横向滚动条可以覆盖整个表格,不会受到固定列宽度的限制。

方案 B 虽然好,但是仍有两个问题需要解决

  1. 如何实现在固定区域也可以做到整个表格上下滚动
  2. 漂浮的固定列会遮盖住全局的横向滚动条。

对于问题 1,他的解决思路其实和刚才表头的解决思路类似。主要是通过适当地设置 margin 来隐藏本应存在的滚动条,这里不再赘述。
对应问题 2,如果是没有纵向滚动,即 height: auto 这样的情况下经过测试不存在这个问题,float 的元素会很自然地不占用滚动条的体积,因此不会有遮挡。如果是有纵向滚动的,情况则复杂了一些,当 float 的元素和下面的主表格等高时会出现遮挡的情况。

图8:方案 B 引入的遮挡滚动条的问题

那既然同高会有遮挡的问题,只要我们对应的减掉对应的滚动条高度就可以了,解决第一个问题时我们得到的大杀器,获取滚动条的高度,也可以用在这里。这个方案也能很好地应该对 Mac 下有的设置不显示滚动条的情况,在不显示滚动条的情况下,我们获取到的宽度是 0,即没有影响。而滚动时,滚动条会自动浮在最高的位置,因此仍然整条可见。

总结

那么,今天,你的浏览器 “滚动” 了吗?你是否已经笑对其中了呢~

本文从实际的组件需求出发,通过三个有关滚动条的问题出现和解决为线索,和大家分享了如何跨系统、跨浏览器地兼容滚动尤其是滚动条的问题,虽然是以表格为核心阐述,但解决方案不局限于表格之中,希望能给大家在遇到类似问题时提供一些灵感。文中涉及的行列固定相关的知识,因为非本文重点,故一笔带过,如果有兴趣了解实现详情,可以参考我们团队开源的 PC 端 UI 组件库 UXCore 和对应的组件 Table 里的代码~


紅白
1.9k 声望195 粉丝