前端交互小技巧
收录一些关于前端交互的小技巧,包括细节思考、创新设计、实现代码等
提示
由于浏览器未必能支持新特性,使用前请到 Caniuse 相关网站查询不同浏览器对该特性的适用性。
阻止滚动穿透
滚动穿透 overscroll 是指的是当鼠标在滚动一个元素的内容(就纵向而言,就是指内容的长度大于该元素的高度),当内容滚动到底部时,如果该元素的父元素(一般就是指整个页面 <body>
元素)也是可滚动的时候,滚动操作就会「穿透」到父元素,触发父元素的内容滚动(虽然此时鼠标是 hover 在前面所述的那个子元素上),这也称为「滚动链」scroll chaining
滚动穿透是浏览器的默认行为,但是在某些特定的场景下,该行为是不合理的。
例如在 modal 弹出框中,如果它有很多内容,需要让用户滚动浏览,但是当用户将内容滚动到底部时,就会触发整个页面的滚动(如果页面此时也是可滚动的),当弹出框被关闭后,用户就会因为页面的非预期的滚动而感到「迷失」。有一些方法可以进行修正
其中最简单的做法是为元素设置样式 pointer-events: none
禁止掉在该元素上触发的所有指针操作,但是这张方法比较「激进」,因为它不仅禁止了滚动操作,还把其他鼠标相关的操作都禁止了,例如点击操作
提示
如果使用的是 Tailwind CSS 则可以为元素添加上 class 类名 pointer-events-none
针对 modal 弹出框这个场景,一个常见的做法是在 modal 弹出时,通过 JS 为 <body>
元素添加上一个特定 class 类,譬如 no-scroll
再针对该类预设了一个 CSS 样式
.no-scroll {
overflow: hidden; /* 阻止滚动 */
}
提示
如果使用的是 Tailwind CSS 则可以为 <body>
元素添加上 class 类名 overflow-hidden
// 以下是在 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: contain
或 overscroll-behavior: none
应用到元素上
以上属性同时影响了元素在纵向和横向的滚动行为,如果想只设置纵向或横向的行为,可以采用 overscroll-behavior-y
或 overscroll-behavior-x
属性
提示
如果使用的是 Tailwind CSS 则可以为元素添加上 class 类名 overscroll-contain
或 overscroll-none
如果只是针对纵向或横向则采用 overscroll-y-contain
或 overscroll-y-none
以及 overscroll-x-contain
或 overscroll-x-none
以上方法虽然方便但有一个约束,需要该元素本身必须是可滚动的(即元素的内容长度要大于自身的高度),如果 modal 模态框本来是不能滚动的,即使添加了 overscroll-behavior: contain
或 overscroll-behavior: none
也会触发页面整体的滚动
所以一个更完备和灵活的解决方案是采用 JS 监听在 wheel
鼠标滚轮事件,再进行更精细的处理
// 以下是在 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 监听在 touchstart
和 touchmove
触摸相关的事件,再进行更精细的处理,逻辑会比较复杂。
所以需要针对移动端的场景,推荐采用第二种方法,即设置 <body>
元素的 overflow
属性