问题场景
近期在公司优化组件库的时候,看到了一篇解析elment plus源代码文章,当时的我正好也在写类似组件,在写Dialog组件的时候,我就在参考element的dialog组件中的选项进行实现,然后我就看到了一个叫lock-screen的选项,我就很好奇这个功能是怎么实现的。大概方向上我是猜到了可能是给body添加overflow:hidden,但是我还是忍不住去查看了源代码,然后就迎面撞上一个hook:useLockscreen。
useLockscreen解析
以下是对element plus如何实现在打开Dialog之后锁定滚动条的一个解析:
看懂以下代码的前置知识是比较简单的。
- useNamespace 这个函数返回的是包含一系列方法的对象,并且根据调用时传入的参数,产生不同的class名称,从而应用不同的样式,而bm方法也是一样的,返回的也是一个class名。
- hasClass、removeClass、getStyle、getScrollBarWidth则见名知意,在这里不必过于纠结代码是什么。但是最后我们会说,element plus是如何计算滚动条宽度的。
- element-plus 的问题,在存在滚动条的情况下,重新赋值body的宽度 ,但是却不考虑body的外边距,导致可能存在溢出,见下方关于滚动条的分析。
export const useLockscreen = (
trigger: Ref<boolean>,
options: UseLockScreenOptions = {}
) => {
if (!isRef(trigger)) {
throwError(
'[useLockscreen]',
'You need to pass a ref param to this function'
)
}
// 获得一个ns对象,包含着一系列方法用于产出class名称
const ns = options.ns || useNamespace('popup')
// 通过ns对象的bm方法,产生了一个class名称,叫'el-popup-parent--hidden'
const hiddenCls = computed(() => ns.bm('parent', 'hidden'))
// 如果不是浏览器环境,或者body上已经有了这个class,则return。
if (!isClient || hasClass(document.body, hiddenCls.value)) {
return
}
let scrollBarWidth = 0
let withoutHiddenClass = false
let bodyWidth = '0'
// 这个方法用于移除el-popup-parent--hidden,同时恢复body的宽度。
const cleanup = () => {
setTimeout(() => {
removeClass(document?.body, hiddenCls.value)
if (withoutHiddenClass && document) {
document.body.style.width = bodyWidth
}
}, 200)
}
watch(trigger, (val) => {
if (!val) {
cleanup()
return
}
// body上如果没有该class,那么则withoutHiddenClass则为true
withoutHiddenClass = !hasClass(document.body, hiddenCls.value)
if (withoutHiddenClass) {
// 同时更新这个bodyWidth的值为body的width
bodyWidth = document.body.style.width
}
// 获取滚动条的宽度
scrollBarWidth = getScrollBarWidth(ns.namespace.value)
// 明确当前body的元素高度是否超出body的可见高度
const bodyHasOverflow =
document.documentElement.clientHeight < document.body.scrollHeight
const bodyOverflowY = getStyle(document.body, 'overflowY')
// 如果body的元素高度确实超出了body的可见高度,且设置了滚动条,同时又没有设置该class:el-popup-parent--hidden
if (
scrollBarWidth > 0 &&
(bodyHasOverflow || bodyOverflowY === 'scroll') &&
withoutHiddenClass
) {
// 这个计算是在有滚动条的情况下,也就是当前body的100%是肯定会溢出的,溢出的就是滚动条的部分
// 但是其实,如果原来的body是有外边距的,并且超出17px,这时候这个计算反而可能导致溢出,所以这里比较好的做法应该是要获取body的外边距同时一并减去才能得到boder-box的宽度
document.body.style.width = `calc(100% - ${scrollBarWidth}px)`
}
// 添加该类名到body上
addClass(document.body, hiddenCls.value)
})
onScopeDispose(() => cleanup())
}
查询了el-popup-parent–hidden类的内容,发现确实是加了overflow:hidden。用来控制body不能滚动。
getScrollBarWidth解决方案
看到这个计算的思路我刚开始也是大为惊叹,居然是结合js和css去相减的
export const getScrollBarWidth = (namespace: string): number => {
if (!isClient) return 0
if (scrollBarWidth !== undefined) return scrollBarWidth
// 创建一个div,名为outer,这个div给了一个类,这个类设置了outer必须含有滚动条,同时给了固定宽度100px
const outer = document.createElement('div')
outer.className = `${namespace}-scrollbar__wrap`
outer.style.visibility = 'hidden'
outer.style.width = '100px'
outer.style.position = 'absolute'
outer.style.top = '-9999px'
document.body.appendChild(outer)
const widthNoScroll = outer.offsetWidth
outer.style.overflow = 'scroll'
// 创建另一个divinner,同时宽度100%,那么这100%,只能是父级容器不包含滚动条的部分。
const inner = document.createElement('div')
inner.style.width = '100%'
outer.appendChild(inner)
const widthWithScroll = inner.offsetWidth
outer.parentNode?.removeChild(outer)
// 剩下的就很简单了。两者相减就能得出滚动条宽度了
scrollBarWidth = widthNoScroll - widthWithScroll
return scrollBarWidth
}
总结:
- 滚动条的计算——手动设置包含滚动条的容器div,同时再添加一个宽度为100%的子容器div,并且对这两个div的offsetWidth进行相减,就能得出滚动条的宽度。
- 使用overflow:hidden来锁定屏幕,并没有什么花里胡哨的锁定屏幕的方法。但是同时要注意body的外边距可能会影响body内容区的计算。
评论区