【紧贴业务,拿来即用】自定义滚动条组件复杂嘛,一起来看看
起因
-
正在沉浸摸鱼快乐的我,突然感受到背后有人;我不动声色地瞟了一眼正对我的反光挡板,发现原来是UI大大
-
她说:老板要统一不同浏览器下的滚动条样式
-
我说:卧...........(又是老板?)
原因
-
css样式美化原生滚动条,不同浏览器解析渲染后的效果有区别
-
原生滚动条对内容空间有侵入,内容展示区域变小
需求
- 支持鼠标滑轮滚动
- 滚动条不额外占空间,默认不展示,移入后展示
- 支持鼠标左键拖动滑动
- 支持鼠标左键点击轨道快速定位
解决思路(垂直滚动条为例)
- 本质是借助原生滚动条的能力(scroll 事件提供的事件对象信息),通过 css 处理达到视觉上不可见,再通过自定义 dom 美化来代替滚动条展示
- 先确定一下 dom 结构(顺便同步一下每个 dom 在此篇文章的命名),如下图

-
确定手柄高度
- 页面渲染后,需要根据 可视区域高度 / 实际内容区域高度 = 手柄高度 / 轨道高度(等同于可视区域高度)
- 高度的获取直接使用 Element.clientHeight
- 实际内容区域高度两种方式都可获取 content.clientHeight、wrap.scrollHeight
-
确定手柄偏移高度(handleMove)
- 手柄顶端到轨道顶端(handleMove) / 轨道高度 = 可视区域移动距离(scollTop)/ 实际内容区域高度
- scollTop 借助原生滚动条的存在,监听 wrap 的 scroll 事件获取,根据上面公式获取 handleMove
-
点击轨道、拖动手柄如何达到可视区域内容滚动
- 手柄顶端到轨道顶端(handleMove) / 轨道高度 = 可视区域移动距离(scollTop)/ 实际内容区域高度
- 根据鼠标触发事件计算 handleMove,再根据上面公式获取 scollTop
- 借助原生滚动条能力,给 wrap 区域设置 scrollTop,达到触发 wrap 的 scroll 事件,进而通过步骤 4 确定手柄偏移高度
具体实现
滚动条组件隐藏原生滚动条
- 默认 wrap 区域是否存在原生滚动条,取决于 content 与 wrap 的高度,就会出现存在或者不存在情况,直接给 wrap 设置 overflow 为 scroll,让其一定展示原生滚动条;在 wrap 加父 dom,设置 overflow 为 hidden,wrap 设置负右边距,达到隐藏效果
- 获取负右边距:动态生成父子 dom,父 dom 设置 overflow 为 scroll,子 dom 宽度 100%,通过父子宽度相减得到当前浏览器样式下的滚动条宽

轨道组件
- 模板结构为父子 dom,分别是轨道、手柄两个 dom
- 入参
- isVertical:是否为垂直
- thumbLen:手柄长度
- moveRatio:手柄移动
- wrapRefKey:wrap ref 值,轨道组件计算需要用此 dom 相关值
确定手柄高度
- 基于上面的公式:可视区域高度 / 实际内容区域高度 = 手柄高度 / 轨道高度
- 计算出可视区域高度 / 实际内容区域高度结果 heightRatio
- heightRatio 大于等于1,则赋值手柄长度为0;否则赋值占轨道的百分比
initThumbLen() {
const wrapEl = this.$refs.wrap
if (!wrapEl) return
const heightRatio = wrapEl.clientHeight / wrapEl.scrollHeight
const widthRatio = wrapEl.clientWidth / wrapEl.scrollWidth
this.thumbHeight = heightRatio >= 1 ? 0 : heightRatio * 100
this.thumbWidth = widthRatio >= 1 ? 0 : widthRatio * 100
},
确定手柄偏移高度
- 鼠标滚动触发原生滚动条(隐藏)scroll 事件,自定义滚动条也要同步移动
- 原生滚动条高度与自定义滚动条高度都是 wrap 区域高度
- 偏移高度 = scrollTop / wrap区域高度
- 不需要考虑边界情况,因为原生滚动条已经处理
scrollWrap() {
const wrapEl = this.$refs.wrap
this.moveY = (wrapEl.scrollTop / wrapEl.clientHeight) * 100
this.moveX = (wrapEl.scrollLeft / wrapEl.clientWidth) * 100
},
点击轨道、拖动手柄如何达到可视区域内容滚动
- 不管是点击轨道,还是拖动手柄,此公式适用:手柄顶端到轨道顶端 / 轨道高度 = 可视区域移动距离(scollTop)/ 实际内容区域高度,公共方法如下
// 入参:鼠标顶端到轨道起点位置的距离 handleMove
getScorllByclickToTrack(handleMove) {
// 轨道上的比率,移动距离 / 轨道长度
const moveRatio = handleMove / this.$el[this.barObj.offset]
// 基于比率,得出实际内容长度移动的距离
this.$emit('wrapScroll', this.barObj.scroll, this.warpEl[this.barObj.scrollSize] * moveRatio)
},
- 处理不同情况下获取鼠标点击位置到轨道起点位置的距离
-
鼠标点击轨道快速定位
- 求如下图红色线段
- 主要利用 Element.getBoundingClientRect MouseEvent.clientY 获取的距离都是相对浏览器可视区域
clicktrackDown(e) { const distance = e[this.barObj.client] - e.target.getBoundingClientRect()[this.barObj.direction] const halfThumb = this.$refs.thumb[this.barObj.offset] / 2 this.getScorllByclickToTrack(distance - halfThumb) }
-
鼠标左键拖动滑动
- 求如下图红色线段
- 鼠标点击手柄,求鼠标位置到轨道顶端距离
clickthumbDown(e) { // 点击手柄,阻止点击事件到轨道 e.stopPropagation() if (e.ctrlKey || e.button === 2) { return } this.downObj.cursorDown = true const clickClient = e[this.barObj.client] // 点击位置到轨道起点位置 const clickToTrack = clickClient - this.$el.getBoundingClientRect()[this.barObj.direction] const clickToThumb = clickClient - e.target.getBoundingClientRect()[this.barObj.direction] // 记录此时的 handleMove this.downObj.handleMove = clickToTrack - clickToThumb this.initMoveFuc() }
- 鼠标拖动手柄,求移动后鼠标位置到轨道顶端距离
mouseMoveDoc(e) { this.downObj.handleMove = this.downObj.handleMove + e[this.barObj.movement] this.getScorllByclickToTrack(this.downObj.handleMove) }
-
其他
- 默认不展示滚动条,移入指定区域展示
- 绑定移动、抬起事件要在 document,并且移动过程中禁止鼠标选中
- 点击手柄时,阻止点击事件冒泡到轨道上
- 通过定义垂直、水平对象来抹平适用差异(BAR_MAP)
体验
最后
如果对你开发某些功能有所帮助,麻烦多点赞评论收藏😊
如果对你实现某类业务有所启发,麻烦多点赞评论收藏😊
如果...,麻烦多点赞评论收藏😊
如果大家有其他的方案,欢迎留言交流哦!
转载自:https://juejin.cn/post/7267108796987752507