Better Scroll 滚动优化实战与常见问题避坑指南

慕容钰岩 交互 阅读 1,521
赞 15 收藏
二维码
手机扫码查看
反馈

为什么我又在 Better Scroll 里折腾了三天?

上周我接了个需求:一个带下拉刷新、上拉加载、横向滚动 tab 的移动端列表页。UI 给的原型里,交互细节特别多,比如“下拉到 80px 才触发刷新”“滚动到底部自动加载但不能闪动”“横向 tab 滚动后要高亮当前项”。我第一反应是:用原生 scroll 肯定搞不定,得上 Better Scroll。

Better Scroll 滚动优化实战与常见问题避坑指南

但问题来了——Better Scroll 现在有三种主流用法:直接 new BScroll()、用 Vue 官方封装的 vue-better-scroll、或者自己写个 hooks 封装。我以前都混着用,这次索性把三个方案都试了一遍,结果踩了不少坑,也摸清了各自的脾气。

谁更灵活?谁更省事?

先说结论:我比较喜欢自己封装 hooks。别急着喷,听我慢慢说。

最省事的肯定是 vue-better-scroll,官方出的,文档齐全,开箱即用。比如实现一个简单的垂直滚动:

<template>
  <div class="wrapper" ref="wrapper">
    <div class="content">
      <div v-for="item in list" :key="item.id">{{ item.text }}</div>
    </div>
  </div>
</template>

<script>
import { ref, onMounted } from 'vue'
import BScroll from '@better-scroll/core'
import PullDown from '@better-scroll/pull-down'
import PullUp from '@better-scroll/pull-up'

BScroll.use(PullDown).use(PullUp)

export default {
  setup() {
    const wrapper = ref(null)
    let bs = null

    onMounted(() => {
      bs = new BScroll(wrapper.value, {
        pullDownRefresh: true,
        pullUpLoad: true
      })

      bs.on('pullingDown', () => {
        // 模拟刷新
        setTimeout(() => {
          bs.finishPullDown()
        }, 1000)
      })
    })
  }
}
</script>

看起来挺清爽,对吧?但问题很快来了:当我想监听“滚动到某个位置高亮 tab”时,发现 vue-better-scroll 的 props 设计太死板,很多底层事件(比如 scrollEnd)根本透不出来。折腾了半天,最后还是得手动调用 bs.on('scrollEnd', ...),那封装的意义在哪?

而直接 new BScroll() 呢?自由度最高,但重复代码多得要命。每个页面都要写 refonMountedonBeforeUnmount 销毁实例……我项目里有七八个滚动页,复制粘贴都快吐了。

所以最后我选了第三条路:自己写个 useBetterScroll hooks。核心代码就这几行:

// composables/useBetterScroll.js
import { onMounted, onBeforeUnmount, ref } from 'vue'
import BScroll from '@better-scroll/core'
import PullDown from '@better-scroll/pull-down'
import PullUp from '@better-scroll/pull-up'
import ObserveDOM from '@better-scroll/observe-dom'

BScroll.use(PullDown).use(PullUp).use(ObserveDOM)

export function useBetterScroll(wrapperRef, options) {
  const bs = ref(null)

  onMounted(() => {
    if (!wrapperRef.value) return
    bs.value = new BScroll(wrapperRef.value, {
      observeDOM: true,
      ...options
    })
  })

  onBeforeUnmount(() => {
    bs.value?.destroy()
  })

  return bs
}

在组件里用起来贼简单:

// 在组件中
const wrapper = ref(null)
const bs = useBetterScroll(wrapper, {
  pullDownRefresh: true,
  pullUpLoad: true
})

// 直接操作 bs.value
bs.value?.on('pullingDown', handlePullDown)

这里注意我踩过好几次坑:一定要加 observeDOM: true 插件!否则动态添加内容后,Better Scroll 不会自动更新高度,导致滚动区域错乱。这个插件虽然有点性能损耗,但比起手动调 refresh(),我宁愿牺牲这点性能。

又踩坑了,touchmove 滚动失效

不管用哪种方案,都会遇到一个经典问题:某些安卓机上 touchmove 被浏览器默认行为拦截,滚动直接卡死。我一开始以为是 Better Scroll 的锅,后来发现是页面根元素没加 CSS。

解决方案很简单,但很容易被忽略:

/* 必须加! */
.wrapper {
  height: 100vh;
  overflow: hidden;
}
.content {
  /* 内容高度由 Better Scroll 控制,这里不用设 */
}

如果 .wrapper 没有明确的高度,或者父级有 overflow: auto,Better Scroll 就无法正确计算滚动区域。我曾经在一个嵌套路由里忘了设高度,折腾了两个小时才定位到问题。

另外,如果你用了 pullDownRefresh,记得在 finishPullDown() 后手动调一次 refresh(),否则新内容可能显示不全。官方文档里提了一句,但藏得特别深。

我的选型逻辑

现在我基本固定了选型规则:

  • 简单滚动(比如纯展示列表):直接 new BScroll(),不折腾
  • 复杂交互(下拉刷新 + 上拉加载 + 滚动联动):必须用自定义 hooks,方便复用和调试
  • 赶工期且交互简单:可以考虑 vue-better-scroll,但要做好后期重写的准备

性能方面,其实三个方案差距不大,因为底层都是同一个 Better Scroll。真正影响性能的是你有没有滥用 refresh() 或者监听太多滚动事件。我建议只在必要时监听 scroll 事件,其他用 scrollEnd 代替,能省不少 CPU。

对了,如果你用的是 React,也有类似方案,比如 use-better-scroll 这个库,但我不熟,就不展开了。反正思路一样:封装 hooks,避免重复代码。

结尾:以上是我踩坑后的总结

折腾完这波,我算是把 Better Scroll 的几种用法都摸透了。虽然自己封装 hooks 多花了一点时间,但后续维护成本低太多了。现在改个滚动逻辑,只需要改 hooks 文件,所有页面自动生效。

当然,这个方案也不是 100% 完美。比如在 SSR 场景下,Better Scroll 会在服务端报错,得加 if (process.client) 判断。不过这种小问题无伤大雅,总比每个页面都写一堆初始化代码强。

以上是我个人对 Better Scroll 几种技术方案的对比总结,有更优的实现方式欢迎评论区交流。这个技巧的拓展用法还有很多(比如结合 Intersection Observer 做懒加载),后续会继续分享这类博客。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论