在我们日常的移动端项目开发中,处理滚动列表是再常见不过的需求了,可以是竖向滚动的列表,也可以是横向的,用better-scroll可以帮助我们实现这个

什么是 better-scroll

better-scroll 是一个移动端滚动的解决方案,它是基于 iscroll 的重写,它和 iscroll 的主要区别在这里。better-scroll 也很强大,不仅可以做普通的滚动列表,还可以做轮播图、picker 等等。

better-scroll的滚动原理

不少同学可能用过 better-scroll,出现最多的问题是:

我的 better-scroll 初始化了, 但是没法滚动。

不能滚动是现象,我们得搞清楚这其中的根本原因。在这之前,我们先来看一下浏览器的滚动原理:

浏览器的滚动条大家都会遇到,当页面内容的高度超过视口高度的时候,会出现纵向滚动条;当页面内容的宽度超过视口宽度的时候,会出现横向滚动条。也就是当我们的视口展示不下内容的时候,会通过滚动条的方式让用户滚动屏幕看到剩余的内容。

那么对于 better-scroll 也是一样的道理,我们先来看一下 better-scroll 常见的 html 结构:

  1.  
    <div class="wrapper">
  2.  
    <ul class="content">
  3.  
    <li>...</li>
  4.  
    <li>...</li>
  5.  
    ...
  6.  
    </ul>
  7.  
    </div>

为了更加直观,我们再来看一张图:


      绿色部分为 wrapper,也就是父容器,它会有固定的高度。黄色部分为 content,它是父容器的第一个子元素,它的高度会随着内容的大小而撑高。那么,当 content 的高度不超过父容器的高度,是不能滚动的,而它一旦超过了父容器的高度,我们就可以滚动内容区了,这就是 better-scroll 的滚动原理。

那么,我们怎么初始化 better-scroll 呢,如果是上述 html 结构,那么初始化代码如下:

  1.  
    import BScroll from 'better-scroll'
  2.  
    let wrapper = document.querySelector('.wrapper')
  3.  
    let scroll = new BScroll(wrapper, {})

better-scroll 对外暴露了一个 BScroll 的类,我们初始化只需要 new 一个类的实例即可。第一个参数就是我们 wrapper 的 DOM 对象,第二个是一些配置参数,具体参考 better-scroll 的文档

better-scroll 的初始化时机很重要,因为它在初始化的时候,会计算父元素和子元素的高度和宽度,来决定是否可以纵向和横向滚动。因此,我们在初始化它的时候,必须确保父元素和子元素的内容已经正确渲染了。如果子元素或者父元素 DOM 结构发生改变的时候,必须重新调用 scroll.refresh() 方法重新计算来确保滚动效果的正常。所以同学们反馈的 better-scroll 不能滚动的原因多半是初始化 better-scroll 的时机不对,或者是当 DOM 结构发送变化的时候并没有重新计算 better-scroll。

better-scroll 遇见 Vue

相信很多同学对 Vue.js 都不陌生,当 better-scroll 遇见 Vue,会擦出怎样的火花呢?

如何在 Vue 中使用 better-scroll

很多同学开始接触使用 better-scroll 都是受到了黄轶老师的一门教学课程——《Vue.js高仿饿了么外卖App》 的影响。在那门课程中,我们把 better-scroll 和 Vue 做了结合,实现了很多列表滚动的效果。在 Vue 中的使用方法如下:

  1.  
    <template>
  2.  
    <div class="wrapper" ref="wrapper">
  3.  
    <ul class="content">
  4.  
    <li>...</li>
  5.  
    ...
  6.  
    </ul>
  7.  
    </div>
  8.  
    </template>
  9.  
    <script>
  10.  
    import BScroll from 'better-scroll'
  11.  
     
  12.  
    export default {
  13.  
    mounted() {
  14.  
    this.$nextTick(() => {
  15.  
    this.scroll = new Bscroll(this.$refs.wrapper, {})
  16.  
    })
  17.  
    }
  18.  
    }
  19.  
    </script>

Vue.js 提供了我们一个获取 DOM 对象的接口—— vm.$refs。在这里,我们通过了 this.$refs.wrapper 访问到了这个 DOM 对象,并且我们在 mounted 这个钩子函数里,this.$nextTick 的回调函数中初始化 better-scroll 。因为这个时候,wrapper 的 DOM 已经渲染了,我们可以正确计算它以及它内层 content 的高度,以确保滚动正常。

这里的 this.$nextTick 是一个异步函数,为了确保 DOM 已经渲染,感兴趣的同学可以了解一下它的内部实现细节,底层用到了 MutationObserver 或者是 setTimeout(fn, 0)。其实我们在这里把 this.$nextTick 替换成 setTimeout(fn, 20) 也是可以的(20 ms 是一个经验值,每一个 Tick 约为 17 ms),对用户体验而言都是无感知的。

异步数据的处理

在我们的实际工作中,列表的数据往往都是异步获取的,因此我们初始化 better-scroll 的时机需要在数据获取后,代码如下:

  1.  
    <template>
  2.  
    <div class="wrapper" ref="wrapper">
  3.  
    <ul class="content">
  4.  
    <li v-for="item in data">{{item}}</li>
  5.  
    </ul>
  6.  
    </div>
  7.  
    </template>
  8.  
    <script>
  9.  
    import BScroll from 'better-scroll'
  10.  
     
  11.  
    export default {
  12.  
    data() {
  13.  
    return {
  14.  
    data: []
  15.  
    }
  16.  
    },
  17.  
    created() {
  18.  
    requestData().then((res) => {
  19.  
    this.data = res.data
  20.  
    this.$nextTick(() => {
  21.  
    this.scroll = new Bscroll(this.$refs.wrapper, {})
  22.  
    })
  23.  
    })
  24.  
    }
  25.  
    }
  26.  
    </script>

这里的 requestData 是伪代码,作用就是发起一个 http 请求从服务端获取数据,并且这个函数返回的是一个 promise(实际项目中我们可能会用 axios 或者 vue-resource)。我们获取到数据的后,需要通过异步的方式再去初始化 better-scroll,因为 Vue 是数据驱动的, Vue 数据发生变化(this.data = res.data)到页面重新渲染是一个异步的过程,我们的初始化时机是要在 DOM 重新渲染后,所以这里用到了 this.$nextTick,当然替换成 setTimeout(fn, 20) 也是可以的。

为什么这里在 created 这个钩子函数里请求数据而不是放到 mounted 的钩子函数里?因为 requestData 是发送一个网络请求,这是一个异步过程,当拿到响应数据的时候,Vue 的 DOM 早就已经渲染好了,但是数据改变 —> DOM 重新渲染仍然是一个异步过程,所以即使在我们拿到数据后,也要异步初始化 better-scroll。

数据的动态更新

我们在实际开发中,除了数据异步获取,还有一些场景可以动态更新列表中的数据,比如常见的下拉加载,上拉刷新等。比如我们用 better-scroll 配合 Vue 实现下拉加载功能,代码如下:

  1.  
    <template>
  2.  
    <div class="wrapper" ref="wrapper">
  3.  
    <ul class="content">
  4.  
    <li v-for="item in data">{{item}}</li>
  5.  
    </ul>
  6.  
    <div class="loading-wrapper"></div>
  7.  
    </div>
  8.  
    </template>
  9.  
    <script> import BScroll from 'better-scroll'
  10.  
    export default {
  11.  
    data() {
  12.  
    return {data: []}
  13.  
    }, created() {
  14.  
    this.loadData()
  15.  
    }, methods: {
  16.  
    loadData() {
  17.  
    requestData().then((res) => {
  18.  
    this.data = res.data.concat(this.data)
  19.  
    this.$nextTick(() => {
  20.  
    if (!this.scroll) {
  21.  
    this.scroll = new Bscroll(this.$refs.wrapper, {})
  22.  
    this.scroll.on('touchend', (pos) => {
  23.  
    // 下拉动作
  24.  
    if (pos.y > 50) {
  25.  
    this.loadData()
  26.  
    }
  27.  
    })
  28.  
    } else {
  29.  
    this.scroll.refresh()
  30.  
    }
  31.  
    })
  32.  
    })
  33.  
    }
  34.  
    }
  35.  
    }
  36.  
    </script>

这段代码比之前稍微复杂一些, 当我们在滑动列表松开手指时候, better-scroll 会对外派发一个 touchend 事件,我们监听了这个事件,并且判断了 pos.y > 50(我们把这个行为定义成一次下拉的动作)。如果是下拉的话我们会重新请求数据,并且把新的数据和之前的 data 做一次 concat,也就更新了列表的数据,那么数据的改变就会映射到 DOM 的变化。需要注意的一点,这里我们对 this.scroll 做了判断,如果没有初始化过我们会通过 new BScroll 初始化,并且绑定一些事件,否则我们会调用 this.scroll.refresh 方法重新计算,来确保滚动效果的正常。

这里,我们就通过 better-scroll 配合 Vue,实现了列表的下拉刷新功能,上拉加载也是类似的套路,一切看上去都是 ok 的。但是,我们发现这里写了大量命令式的代码(这一点不是 Vue.js 推荐的),如果有很多类似滚动的组件,我们就需要写很多类似的命令式且重复性的代码,而且我们把数据请求和 better-scroll 也做了强耦合,这些对于一个追求编程逼格的人来说,就不 ok 了。

scroll 组件的抽象和封装

因此,我们有强烈的需求抽象出来一个 scroll 组件,类似小程序的 scroll-view 组件,方便开发者的使用。

首先,我们要考虑的是 scroll 组件本质上就是一个可以滚动的列表组件,至于列表的 DOM 结构,只需要满足 better-scroll 的 DOM 结构规范即可,具体用什么标签,有哪些辅助节点(比如下拉刷新上拉加载的 loading 层),这些都不是 scroll 组件需要关心的。因此, scroll 组件的 DOM 结构十分简单,如下所示:

  1.  
    <template>
  2.  
    <div ref="wrapper">
  3.  
    <slot></slot>
  4.  
    </div>
  5.  
    </template>
  • 1
  • 2
  • 3
  • 4
  • 5

这里我们用到了 Vue 的特殊元素—— slot 插槽,它可以满足我们灵活定制列表 DOM 结构的需求。接下来我们来看看 JS 部分:

  1.  
    <script type="text/ecmascript-6">
  2.  
    import BScroll from 'better-scroll'
  3.  
     
  4.  
    export default {
  5.  
    props: {
  6.  
    /*1 滚动的时候会派发scroll事件,会截流。
  7.  
    2 滚动的时候实时派发scroll事件,不会截流。
  8.  
    3 除了实时派发scroll事件,在swipe的情况下仍然能实时派发scroll事件 */
  9.  
    probeType: {
  10.  
    type: Number,
  11.  
    default: 1
  12.  
    },
  13.  
    // 点击列表是否派发click事件
  14.  
    click: {
  15.  
    type: Boolean,
  16.  
    default: true
  17.  
    },
  18.  
    // 是否开启横向滚动
  19.  
    scrollX: {
  20.  
    type: Boolean,
  21.  
    default: false
  22.  
    },
  23.  
    // 是否派发滚动事件
  24.  
    listenScroll: {
  25.  
    type: Boolean,
  26.  
    default: false
  27.  
    },
  28.  
    // 列表的数据
  29.  
    data: {
  30.  
    type: Array,
  31.  
    default: null
  32.  
    },
  33.  
    /** * 是否派发滚动到底部的事件,用于上拉加载 */
  34.  
    pullup: {
  35.  
    type: Boolean,
  36.  
    default: false
  37.  
    },
  38.  
    /** * 是否派发顶部下拉的事件,用于下拉刷新 */
  39.  
    pulldown: {
  40.  
    type: Boolean,
  41.  
    default: false
  42.  
    },
  43.  
    /** * 是否派发列表滚动开始的事件 */
  44.  
    beforeScroll: {
  45.  
    type: Boolean,
  46.  
    default: false
  47.  
    },
  48.  
    /** * 当数据更新后,刷新scroll的延时。 */
  49.  
    refreshDelay: {
  50.  
    type: Number,
  51.  
    default: 20
  52.  
    }
  53.  
    },
  54.  
    mounted() {
  55.  
    // 保证在DOM渲染完毕后初始化better-scroll
  56.  
    setTimeout(() => {
  57.  
    this._initScroll()
  58.  
    }, 20)
  59.  
    },
  60.  
    methods: {
  61.  
    _initScroll() {
  62.  
    if (!this.$refs.wrapper) {
  63.  
    return
  64.  
    }
  65.  
    // better-scroll的初始化
  66.  
    this.scroll = new BScroll(this.$refs.wrapper, {
  67.  
    probeType: this.probeType,
  68.  
    click: this.click,
  69.  
    scrollX: this.scrollX
  70.  
    })
  71.  
    // 是否派发滚动事件
  72.  
    if (this.listenScroll) {
  73.  
    let me = this
  74.  
    this.scroll.on('scroll', (pos) => {
  75.  
    me.$emit('scroll', pos)
  76.  
    })
  77.  
    }
  78.  
    // 是否派发滚动到底部事件,用于上拉加载
  79.  
    if (this.pullup) {
  80.  
    this.scroll.on('scrollEnd', () => { // 滚动到底部
  81.  
    if (this.scroll.y <= (this.scroll.maxScrollY + 50)) {
  82.  
    this.$emit('scrollToEnd')
  83.  
    }
  84.  
    })
  85.  
    }
  86.  
    // 是否派发顶部下拉事件,用于下拉刷新
  87.  
    if (this.pulldown) {
  88.  
    this.scroll.on('touchend', (pos) => { // 下拉动作
  89.  
    if (pos.y > 50) {
  90.  
    this.$emit('pulldown')
  91.  
    }
  92.  
    })
  93.  
    }
  94.  
    // 是否派发列表滚动开始的事件
  95.  
    if (this.beforeScroll) {
  96.  
    this.scroll.on('beforeScrollStart', () => {
  97.  
    this.$emit('beforeScroll')
  98.  
    })
  99.  
    }
  100.  
    },
  101.  
    disable() {
  102.  
    // 代理better-scroll的disable方法
  103.  
    this.scroll && this.scroll.disable()
  104.  
    },
  105.  
    enable() {
  106.  
    // 代理better-scroll的enable方法
  107.  
    this.scroll && this.scroll.enable()
  108.  
    },
  109.  
    refresh() {
  110.  
    // 代理better-scroll的refresh方法
  111.  
    this.scroll && this.scroll.refresh()
  112.  
    },
  113.  
    scrollTo() {
  114.  
    // 代理better-scroll的scrollTo方法
  115.  
    this.scroll && this.scroll.scrollTo.apply(this.scroll, arguments)
  116.  
    },
  117.  
    scrollToElement() {
  118.  
    // 代理better-scroll的scrollToElement方法
  119.  
    this.scroll && this.scroll.scrollToElement.apply(this.scroll, arguments)
  120.  
    }
  121.  
    },
  122.  
    watch: {
  123.  
    // 监听数据的变化,延时refreshDelay时间后调用refresh方法重新计算,保证滚动效果正常
  124.  
    data() {
  125.  
    setTimeout(() => {
  126.  
    this.refresh()
  127.  
    }, this.refreshDelay)
  128.  
    }
  129.  
    }
  130.  
    }
  131.  
    </script>

JS 部分实际上就是对 better-scroll 做一层 Vue 的封装,通过 props 的形式,把一些对 better-scroll 定制化的控制权交给父组件;通过 methods 暴露的一些方法对 better-scroll 的方法做一层代理;通过 watch 传入的 data,当 data 发生改变的时候,在适当的时机调用 refresh 方法重新计算 better-scroll 确保滚动效果正常,这里之所以要有一个 refreshDelay 的设置是考虑到如果我们对列表操作用到了 transition-group 做动画效果,那么 DOM 的渲染完毕时间就是在动画完成之后。

有了这一层 scroll 组件的封装,我们来修改刚刚最复杂的代码(假设我们已经全局注册了 scroll 组件)。

  1.  
    <template>
  2.  
    <scroll class="wrapper" :data="data" :pulldown="pulldown" @pulldown="loadData">
  3.  
    <ul class="content">
  4.  
    <li v-for="item in data">{{item}}</li>
  5.  
    </ul>
  6.  
    <div class="loading-wrapper"></div>
  7.  
    </scroll>
  8.  
    </template>
  9.  
    <script> import BScroll from 'better-scroll'
  10.  
    export default {
  11.  
    data() {
  12.  
    return {
  13.  
    data: [],
  14.  
    pulldown: true
  15.  
    }
  16.  
    },
  17.  
    created() {
  18.  
    this.loadData()
  19.  
    },
  20.  
    methods: {
  21.  
    loadData() {
  22.  
    requestData().then((res) => {
  23.  
    this.data = res.data.concat(this.data)
  24.  
    })
  25.  
    }
  26.  
    }
  27.  
    }
  28.  
    </script>

可以很明显的看到我们的 JS 部分精简了非常多的代码,没有对 better-scroll 再做命令式的操作了,同时把数据请求和 better-scroll 也做了剥离,父组件只需要把数据 data 通过 prop 传给 scroll 组件,就可以保证 scroll 组件的滚动效果。同时,如果想实现下拉刷新的功能,只需要通过 prop 把 pulldown 设置为 true,并且监听 pulldown 的事件去做一些数据获取并更新的动作即可,整个逻辑也是非常清晰的。

插件 Vue 化引发的一些思考

这篇文章不仅仅是要教会大家封装一个 scroll 组件,还想传递一些把第三方插件(原生 JS 实现)Vue 化的思考过程。很多学习 Vue.js 的同学可能还停留在 “XX 效果如何用 Vue.js 实现” 的程度,其实把插件 Vue 化有两点很关键,一个是对插件本身的实现原理很了解,另一个是对 Vue.js 的特性很了解。对插件本身的实现原理了解需要的是一个思考和钻研的过程,这个过程可能困难,但是收获也是巨大的;而对 Vue.js 的特性的了解,是需要大家对 Vue.js 多多使用,学会从平时的项目中积累和总结,也要善于查阅 Vue.js 的官方文档,关注一些 Vue.js 的升级等。

所以,我们拒绝伸手党,但也不是鼓励大家什么时候都要去造轮子,当我们在使用一些现成插件的同时,也希望大家能多多思考,去探索一下现象背后的本质,把 “XX 效果如何用 Vue.js 实现” 这句话从问号变成句号。

https://blog.csdn.net/weixin_37719279/article/details/82084342

better-scroll插件的介绍及使用的更多相关文章

  1. 【转】Eclipse插件大全介绍及下载地址

    转载地址:http://developer.51cto.com/art/200906/127169.htm 尚未一一验证. eclipse插件大全介绍,以及下载地址 Eclipse及其插件下载网址大全 ...

  2. Xcode Alcatraz插件管理介绍和使用

    Xcode Alcatraz插件管理介绍和使用http://www.jianshu.com/p/7a2484123bf6 1.简介 Alcatraz是一个能帮你管理Xcode插件丶模版及颜色配置的工具 ...

  3. 03_Elasticsearch如何安装以及相关插件的介绍

    03_Elasticsearch如何安装以及相关插件的介绍 elasticsearch -d (-d参数是为了让服务后台运行) Elasticsearch 目录结构: 文件夹 作用 /bin 运行El ...

  4. Cordova各个插件使用介绍系列(六)—$cordovaDevice获取设备的相关信息

    详情请看:Cordova各个插件使用介绍系列(六)—$cordovaDevice获取设备的相关信息 在项目中需要获取到当前设备,例如手机的ID,联网状态,等,然后这个Cordova里有这个插件可以用, ...

  5. 开发自己的One Page Scroll插件(二)

    开发自己的One Page Scroll插件(一) 5. 在其他浏览器中的特性 我经常会不停地发布当前的版本,从而可以在GitHub上得到不断的反馈.我的开发模式是不断的迭代.在开始的时候,我不会太关 ...

  6. vue-scroller的使用 && 开发自己的 scroll 插件

    vue-scroller的使用 在spa开发过程中,难免会遇到使用scroll的情况,比如下面的: 即,当用户选择好商品之后,点击购物车,就会有一个购物车弹窗,如果选择的商品小于三个,刚好合适,如果多 ...

  7. [转]jQueryUI中Datepicker(日历)插件的介绍和使用

    http://jqueryui.com/datepicker/ 本文转自:http://blog.csdn.net/redarmy_chen/article/details/7400571 jQuer ...

  8. JQuery jquerysessionjs插件使用介绍

    jquerysessionjs插件使用介绍 by:授客 QQ:1033553122 1.   测试环境 JQuery-3.2.1.min.j 下载地址: https://gitee.com/ishou ...

  9. Wookmark-jQuery-master 瀑布流插件使用介绍,含个人测试DEMO

    要求 必备知识 本文要求基本了解 Html/CSS,  JavaScript/JQuery. 开发环境 Dreamweaver CS6 / Chrome浏览器 演示地址 演示地址 资料下载   测试预 ...

  10. Myeclipse10下载,安装,破解,插件,优化介绍

    一.Myeclipse10下载与破解 Genuitec 公司发布了MyEclipse 10,一款Genuitec旗下的商业化Eclipse集成开发工具的升级版本.MyEclipse 10基于Eclip ...

随机推荐

  1. IDA解析so文件异常(Binary data is incorrect maximum possible value is xx)

    错误信息 Binary data is incorrect maximum possible value is 0 错误原因 so文件损坏 或者ida换成32 解决办法 重新获得so文件,或者调整id ...

  2. 201403-1 相反数 Java

    法1:排序后,首尾两个指针 法2:每个数的绝对值如果出现过,flag置为1,如果再次出现,就计数+1 本文采用法1 import java.util.Arrays; import java.util. ...

  3. electron-builder打包跳过publish

    默认情况下执行 npm run release使用build命令打包时自动将打包好的安装程序发布到仓库,有时候不需要每次打包都上传到仓库,这时我们只需要在build命令后面加上参数-p never 即 ...

  4. git submodule update --init 和 --remote的区别

    git 的submodule 工具方便第三方库的管理,比如gitlab 上的各种开源工具,spdlog等 在项目目录下创建.gitmodule 里可以添加第三方库,然后在更新第三方库时,有两个选项 g ...

  5. 从[Greenplum 6.0] 1分钟安装尝鲜开始

    Greenplum目前6版本目前已经迭代了几个小版本了,随着版本的更新,不断的有bug被修复. 打算试用的朋友可以入手了. 作为开年的第一个工作日的第一个帖子,必须从“开天辟地”的6.0开始.以下内容 ...

  6. node 第三方库总结

    app.post("/todo/add", (request, response) => { request.body //如何拿到前端ajax传来的JSON数据 }) 需要 ...

  7. 关于使用静态链表实现一元多项式的相加&&乘积

    一元多项式的相加类似于两条链表的合并 当然前提是链表中的幂指数是按顺序排列的 此题中的链表采用的是升序排列,输出也是按升序输出的 #include<stdio.h> #include< ...

  8. windows系统下的渗透测试神器 -pentestbox

    Pentestbox介绍 PentestBox官网:https://pentestbox.org/zh/ 这是一个运行在windows环境下的终端,集成了绝大部分渗透测试所需要的环境 如python2 ...

  9. vue结合element实现自定义上传图片、文件

    参考了很多文献,感谢各位帖子,所以也想把自己遇到不会的东西分享出来,菜鸟一枚大家一进步!

  10. 线程池-进程池-io模型

    一.线程池与进程池 什么是池?简单的说就是一个容器,一个范围 在保证计算机硬件安全的情况下最大限度的充分利用计算机, 池其实是降低了程序的运行效率,但是保证了计算机硬件的安全,也是实现了一个并发的效果 ...