设计一个Scrollbar的终极解决方案
一、前言
很久没有更新文章了,是因为一直没有找到好的素材,正好最近工作也在交接了,总结了一下今年的项目,发现有这样一个有趣的问题,与大家分享;
二、背景
在项目中我们常常会遇到滚动条的场景,一般出现在这样的场景,外面有一个盒子,将其css属性 overflow
设置为scroll
或者auto
,而其内容超出了盒子的承载范围,那么这个时候就会出现横向或者纵向的滚动条;
如下面的代码所示:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>滚动示例</title>
<style>
.container {
width: 300px;
height: 500px;
overflow: auto;
border: 1px solid #eee;
}
</style>
</head>
<body>
<div class="container">
<ul class="content"></ul>
</div>
<script>
const content = document.querySelector(".content")
const fragment = document.createElement("fragment")
new Array(20).fill(true).forEach((item, i) => { // 设计超出外围盒子的承载范围
const li = document.createElement("li")
li.innerHTML = `<h1>${'item' + i}</h1>`
fragment.appendChild(li)
})
content.appendChild(fragment)
</script>
</body>
</html>
复制代码
效果如下图所示:
谷歌浏览器效果
edge浏览器效果
safari浏览器效果
我们可以明显感受的到不同的浏览器的差异就体现出来了;不仅如此,我们以谷歌浏览器为例;有的时候我们可能会觉得,这个滚动条样式也太丑了吧;似乎想要去修改一下样式;
三、如何解决
对于谷歌浏览器我们可以这样做:
/* 将以下样式写在全局 */
::-webkit-scrollbar-track { /* 滚动条的滑轨背景颜色 */
background-color: #b46868;
}
::-webkit-scrollbar-thumb { /* 滑块颜色 */
background-color: rgba(0, 0, 0, 0.2);
}
::-webkit-scrollbar-button { /* 滑轨两头的监听按钮颜色 */
background-color: #7c2929;
}
::-webkit-scrollbar-corner { /* 横向滚动条和纵向滚动条相交处尖角的颜色 */
background-color: black;
}
复制代码
可以看到如下效果:
可以看到我们真的成功改掉了浏览器滚动条的样式,并且由于是写在全局下的,因此项目中所有的滚动条的样式都将改变;
事实上,我在实际项目中也经常用这样的方式做定制化的滚动条样式;但是问题真的解决了吗?其实并没有?
因为这个样式只支持以-webkit-
为内核的浏览器,将同样的项目跑在火狐,或者edge浏览器上,我们试试效果如何?
可以看到,滚动条又变成原来的样子了,如果是在大屏场景的项目的话,大屏的美观度直接就会被破坏,因此我们需要想办法解决这个问题!
五、冥思苦想
如果是一名经验丰富的开发者,我们不难想到,可以通过针对每一种浏览器,我们都去写它对应的css解决方案
;
这里我将不同的浏览器针对滚动条暴露的css属性整理一下;
ie浏览器
/* IE 浏览器 */
.scrollbar{
/*三角箭头的颜色*/
scrollbar-arrow-color: #fff;
/*滚动条滑块按钮的颜色*/
scrollbar-face-color: #0099dd;
/*滚动条整体颜色*/
scrollbar-highlight-color: #0099dd;
/*滚动条阴影*/
scrollbar-shadow-color: #0099dd;
/*滚动条轨道颜色*/
scrollbar-track-color: #0066ff;
/*滚动条3d亮色阴影边框的外观颜色——左边和上边的阴影色*/
scrollbar-3dlight-color:#0099dd;
/*滚动条3d暗色阴影边框的外观颜色——右边和下边的阴影色*/
scrollbar-darkshadow-color: #0099dd;
/*滚动条基准颜色*/
scrollbar-base-color: #0099dd;
}
复制代码
谷歌浏览器和safari浏览器都是-webkit-内核
所以是一样的
/* 谷歌浏览器和safari浏览器 */
::-webkit-scrollbar-track { /* 滚动条的滑轨背景颜色 */
background-color: #b46868;
}
::-webkit-scrollbar-thumb { /* 滑块颜色 */
background-color: rgba(0, 0, 0, 0.2);
}
::-webkit-scrollbar-button { /* 滑轨两头的监听按钮颜色 */
background-color: #7c2929;
}
::-webkit-scrollbar-corner { /* 横向滚动条和纵向滚动条相交处尖角的颜色 */
background-color: black;
}
复制代码
笔者暂时并没有找到火狐浏览器的方案;据说是目前火狐浏览器没有提供这样的css属性供开发者修改;
对于大部分应用似乎可以通过这种全量css
的方法去解决;但是为了用户体验,我们还是希望能够有更好的解决方案;
我并不推荐以上做法的原因是因为滚动条样式在某些浏览器上用户是可以自定义修改的;此外css的方式还是不确定性因素太多,为了保证滚动条在多类浏览器
下的统一性,我们还是需要一种稳定、统一、易于维护的方式;
六、绞尽脑汁
在探索的过程中,我在element-ui上找到了思路,我发现element左侧菜单栏的滚动条,无论在什么浏览器下,都能够很好的保持一致性,并且UI样式,交互都很好看;基于此,我点进去看了一下;
才发现原来element-ui
并非用的浏览器内置的滚动条,而是自己实现了一个滚动条;
确实有一下几点好处:
第一:稳定,无论在任何情况下都可以保持UI的一致性,因为将对内置的滚动条样式的控制,转换为对普通div元素的样式控制,可以保证其更稳定的样式一致性
第二:可控性更强,因为css样式都是自己设计,交互也是自己设计的,无论拓展新的交互都更加容易
第三:更方便维护,对于滚动条,我们很容易想到可以将其封装成一个组件;
当然有优点就有一定的缺点,根据目前我的了解,这种方式依然无法完全依赖于css实现,因此js的消耗代价是其致命弱点
既然element-ui
有这样的实现,我想他肯定有这样的组件呀,我只要使用就好了,但是,遗憾的是我并没有在element-ui
找到类似的组件实现,因此我想,那就自己实现一个;综合考虑,我选择先封装一个基于vue2
的scrollbar的解决方案(因为element-ui就是基于vue2的哈);
七、实现组件
这个组件的目录结构思路如下
|-- scrollbar
| |-- index.js
| |-- helper
| | |-- calc.js
| | |-- event.js
| | |-- index.js
| |-- src
| |-- index.vue
复制代码
其实整个内容还是比较简单的,但是我想分享几个写的过程中,我觉得对大家有帮助的点;
1.如何实现
为了尽可能的利用浏览器滚动的原生特性,我选择将浏览器内置的滚动条,隐藏起来(遮盖住);将我们自己写的滚动条样式暴露在外部;
如下图所示
以下将可视区域称为 scrollbox 、实际内容为 contentbox 、滚动条为 scrollbar、
他们应该具备以下关系:
scrollbar的高度 / scrollbox的高度 = scrollbox的高度 / contentbox的高度
scrollbox的scrollTop / contentbox的高度 = scrollbar的top / scrollbox的高度
遵循以上关系,就能完全模拟真实的滚动效果;
2.事件管理
在书写这个组件的时候,我发现我们经常需要书写类似下面的代码
// 在某个时机
element.addEventListener("事件类型" , ()=>{
// do something ...
})
// 在某个时机
element.removeEventListener("事件类型" , 原函数引用)
复制代码
这种事件高频的业务场景,尤其是一个事件的监听是在另一个事件触发的回调里这种情况是,管理事件就显得比较消耗心智了,通常情况话,在写代码时总显的力不从心;这个时候,我们其实可以选择写一个保存应用的类,只管注册,而不管取消,最后统一取消,这样的方式会极大的减少我们的心智负担;我们来看一下;
export class ScrollEvent {
constructor(el) {
this.el = el;
this.events = {}
}
listen (type, fn) {
(this.events[type] || (this.events[type] = [])).push(fn);
this.el.addEventListener(type, fn, { passive: false })
}
remove (type) {
const callbacks = this.events[type]
callbacks && callbacks.forEach((fn) => {
this.el.removeEventListener(type, fn)
})
}
removeAll () {
Object.keys(this.events).forEach(type => {
this.remove(type)
})
}
removeThat (type, fn) {
const callbacks = this.events[type]
const index = callbacks.indexOf(fn)
if (index === -1) {
return false
}
this.el.removeElementListener(type, callbacks[index])
return true
}
}
复制代码
每一个ScrollEvent
实例都代表一个DOM元素,所监听的所有事件,管理在自己的events中;
这个时候哪怕是通过
scrollevent.listen("事件类型" , ()=>{
// to do something..
})
复制代码
这样的函数字面量的方式去进行的注册函数,我们在取消的时候,也只用这样写就好了;
scrollevent.remove("原事件类型")
复制代码
根本就不用去管需是否需要将函数具名的问题;
八、发布组件
通过一顿操作,我将这个组件发布在了npm上,具体如何发布,我推荐看这篇文章
所以使用的时候可以这样使用
npm i story-scrollbar -S
复制代码
然后在全局中注册一下
// 目前还在完善当中,详情请关注npm的readme哈
import SScrollbar from "story-scrollbar/index"
Vue.use(SScrollbar)
复制代码
在组件中可以这样使用
<template>
<div>
<h1>原始滚动条</h1>
<div class="container">
<ul>
<li v-for="item in 100" :key="item">item {{item}}</li>
</ul>
</div>
<h1>组件滚动条</h1>
<SScrollbar class="scroll">
<ul>
<li v-for="item in 100" :key="item">item {{item}}</li>
</ul>
</SScrollbar>
</div>
</template>
<script>
export default {
data () {
return {}
}
}
</script>
<style scoped>
.container {
width: 300px;
height: 300px;
border: 1px solid #333;
overflow: auto;
}
.scroll {
width: 300px;
height: 300px;
border: 1px solid #333;
}
</style>
复制代码
效果如下
因为我这个是mac系统,所以体现的不是很明显,如果是windows系统,那么就可以明显的看到差异了,并且无论在任何浏览器上面,滚动条的样式都还是表现的比较稳定的,并且提供了定义滚动条样式的style供开发者自定义;
下面贴上git地址:如果觉得不错,还希望给个star哈;多谢啦;也欢迎提交issues;
https://github.com/sonxiaopeng/story-scrollbar.git
复制代码
本文首发时间:2022年04月05日,后续将不断完善和更新;