前端交互小技巧


前端交互小技巧

收录一些关于前端交互的小技巧,包括细节思考、创新设计、实现代码等

提示

由于浏览器未必能支持新特性,使用前请到 Caniuse 相关网站查询不同浏览器对该特性的适用性。

阻止滚动穿透

滚动穿透 overscroll 是指的是当鼠标在滚动一个元素的内容(就纵向而言,就是指内容的长度大于该元素的高度),当内容滚动到底部时,如果该元素的父元素(一般就是指整个页面 <body> 元素)也是可滚动的时候,滚动操作就会「穿透」到父元素,触发父元素的内容滚动(虽然此时鼠标是 hover 在前面所述的那个子元素上),这也称为「滚动链」scroll chaining

滚动穿透是浏览器的默认行为,但是在某些特定的场景下,该行为是不合理的。

例如在 modal 弹出框中,如果它有很多内容,需要让用户滚动浏览,但是当用户将内容滚动到底部时,就会触发整个页面的滚动(如果页面此时也是可滚动的),当弹出框被关闭后,用户就会因为页面的非预期的滚动而感到「迷失」。有一些方法可以进行修正


其中最简单的做法是为元素设置样式 pointer-events: none 禁止掉在该元素上触发的所有指针操作,但是这张方法比较「激进」,因为它不仅禁止了滚动操作,还把其他鼠标相关的操作都禁止了,例如点击操作

提示

如果使用的是 Tailwind CSS 则可以为元素添加上 class 类名 pointer-events-none


针对 modal 弹出框这个场景,一个常见的做法是在 modal 弹出时,通过 JS 为 <body> 元素添加上一个特定 class 类,譬如 no-scroll 再针对该类预设了一个 CSS 样式

css
.no-scroll {
    overflow: hidden; /* 阻止滚动 */
}
提示

如果使用的是 Tailwind CSS 则可以为 <body> 元素添加上 class 类名 overflow-hidden

js
// 以下是在 nuxt 项目中的一个代码片段
// 监听一个 state 状态变量 showSeriesModal 的变化,该变量指示 modal 的弹出和关闭状态
watch(showSeriesModal, () => {
  if (!document?.body) { return }

  // 根据 showSeriesModal 的变化为 <body> 元素添加或移除 `overflow-hidden` 类名
  if (showSeriesModal.value) {
    document.body.classList.add('overflow-hidden')
  } else {
    document.body.classList.remove('overflow-hidden')
  }
})

以上方法虽然可以解决滚动穿透问题,但是通过修改 <body> 标签的 overflow 属性控制页面的滚动,会造成页面的滚动条时而显示时而隐藏,由此造成页面(可显示内容区域)的宽度改变 :thumbsdown: 这会引入另一个问题,就是导致元素 reflow 重排,在视觉上的感觉就是在显示和隐藏 modal 弹出框时页面会「跳一下」

对于这个问题有两种解决方案,一是为 <body> 设置 overflow: hidden 的同时,设置 padding-left 宽度与滚动条一致;另一种方案是在默认情况下采用 overflow: overlay 模式,这样滚动条就不会占据页面的宽度,而是「浮在」页面上


其实 CSS 针对这一种需求专门提供了一个属性 overscroll-behavior 进行设置,虽然该属性依然处于草案阶段,但是目前的浏览器兼容性已经挺好的

  • overscroll-behavior: auto 默认值,运行滚动穿透
  • overscroll-behavior: contain 不允许滚动穿透
  • overscroll-behavior: none 不允许滚动穿透,而且也阻止了滚动到边界时所触发的 "bounce" effect「回弹」效应(通过会利用该行为实现顶部下拉刷新底部上拉加载的操作)

如果要阻止滚动穿透,可以将 overscroll-behavior: containoverscroll-behavior: none 应用到元素上

以上属性同时影响了元素在纵向和横向的滚动行为,如果想只设置纵向或横向的行为,可以采用 overscroll-behavior-yoverscroll-behavior-x 属性

提示

如果使用的是 Tailwind CSS 则可以为元素添加上 class 类名 overscroll-containoverscroll-none 如果只是针对纵向或横向则采用 overscroll-y-containoverscroll-y-none 以及 overscroll-x-containoverscroll-x-none


以上方法虽然方便但有一个约束,需要该元素本身必须是可滚动的(即元素的内容长度要大于自身的高度),如果 modal 模态框本来是不能滚动的,即使添加了 overscroll-behavior: contain overscroll-behavior: none 也会触发页面整体的滚动

所以一个更完备和灵活的解决方案是采用 JS 监听在 wheel 鼠标滚轮事件,再进行更精细的处理

ts
// 以下是在 nuxt 项目中的一个代码片段
// 它是事件处理函数,用于处理 wheel 鼠标滚轮事件
// 该处理函数绑定到一个下拉菜单
// 限制了最大高度为 60vh
// 而菜单内容是动态生成的,所以该元素可能是可滚动的,也可以是不需要滚动
// (不管下拉菜单是否可滚动的情况下)都要阻止它的滚动穿透
const scrollHandler = (event: WheelEvent) => {
  // 阻止事件冒泡
  event.stopPropagation()
  // 先判断该下拉菜单元素是否存在
  if(subNav.value) {
    // 再对两种极端情况(滚动到顶部和滚动到底部)进行判断
    if (subNav.value.scrollTop === 0 && event.deltaY < 0) {
      // 如果已经滚动到了顶部 subNav.value.scrollTop === 0
      // 而且触发这次事件的滚动方向还是向上
      event.preventDefault(); // 则阻止默认行为(即阻止滚动穿透)
    } else if (Math.ceil(subNav.value.scrollTop + subNav.value.clientHeight) >= subNav.value.scrollHeight && event.deltaY > 0) {
      // 如果已经滚动到了底部 Math.ceil(subNav.value.scrollTop + subNav.value.clientHeight) >= subNav.value.scrollHeight
      // 而且触发这次事件的滚动方向还是向下
      event.preventDefault(); // 则阻止默认行为(即阻止滚动穿透)
    }
  }
}
注意

在以上方法中涉及到几个元素的尺寸属性

  • 属性 scrollHeight 表示元素内容的完全高度/长度
  • 属性 scrollTop 表示内容向上滚动(出去)的长度
  • 属性 clientHeight 表示该元素自身的高度

当元素自身的高度小于它所包含内容的长度时 element.clientHeight < element.scrollHeight 该元素就是可滚动的

而可以 scrollTop 则表示已经滚动了多少距离

所以可以通过将已经滚动的距离与元素自身的高度相加 element.scrollTop + element.clientHeight 与内容的总长 element.scrollHeight 进行对比,以判断是否已经滚动到底部

但是由于 JS 在浮点运算时并不准确,所以需要特别需要留意的一点是,在比较之前要采用 Math.ceil() 对求和运行进行向上修约,虽然该操作会在某些特殊的情况会造成误判(滚动到距离底部很小一段距离时,会误认为滚动到底了),但是这些误判一般并不会造成奇异的行为

适配移动端

如果网页需要考虑移动端的情况,则可以采用 JS 监听在 touchstarttouchmove 触摸相关的事件,再进行更精细的处理,逻辑会比较复杂。

所以需要针对移动端的场景,推荐采用第二种方法,即设置 <body> 元素的 overflow 属性


Copyright © 2024 Ben

Theme BlogiNote

Icons from Icônes