likes
comments
collection
share

element plus隐含的小技巧,从一个hook说起

作者站长头像
站长
· 阅读数 15

问题场景

近期在扒拉elment plus的源代码,也在写写组件,在写Dialog组件的时候,我就在参考element的dialog组件中的选项进行实现,然后我就看到了一个叫lock-screen的选项,我就很好奇这个功能是怎么实现的。大概方向上我是猜到了可能是给body添加overflow:hidden,但是我还是忍不住去查看了源代码,然后就迎面撞上一个hook:useLockscreen。

useLockscreen解析

以下是对element plus如何实现在打开Dialog之后锁定滚动条的一个解析: 看懂以下代码的前置知识是比较简单的。

  1. useNamespace 这个函数返回的是包含一系列方法的对象,并且根据调用时传入的参数,产生不同的class名称,从而应用不同的样式,而bm方法也是一样的,返回的也是一个class名。
  2. hasClass、removeClass、getStyle、getScrollBarWidth则见名知意,在这里不必过于纠结代码是什么。但是最后我们会说,element plus是如何计算滚动条宽度的。
  3. 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
}

总结:

  1. 滚动条的计算——手动设置包含滚动条的容器div,同时再添加一个宽度为100%的子容器div,并且对这两个div的offsetWidth进行相减,就能得出滚动条的宽度。
  2. 使用overflow:hidden来锁定屏幕,并没有什么花里胡哨的锁定屏幕的方法。但是同时要注意body的外边距可能会影响body内容区的计算。