Vue滚动插件之better-scroll

引用自 https://zhuanlan.zhihu.com/p/27407024html

在咱们平常的移动端项目开发中,处理滚动列表是再常见不过的需求了。 以滴滴为例,能够是这样竖向滚动的列表,如图所示:vue

也能够是横向滚动的导航栏,如图所示:ios

能够打开“微信 —> 钱包—>滴滴出行”体验效果。git

咱们在实现这类滚动功能的时候,会用到我写的第三方库,better-scroll。github

什么是 better-scroll

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

better-scroll 的滚动原理

很多同窗可能用过 better-scroll,我收到反馈最多的问题是:axios

个人 better-scroll 初始化了, 可是无法滚动。小程序

不能滚动是现象,咱们得搞清楚这其中的根本缘由。在这以前,咱们先来看一下浏览器的滚动原理:
浏览器的滚动条你们都会遇到,当页面内容的高度超过视口高度的时候,会出现纵向滚动条;当页面内容的宽度超过视口宽度的时候,会出现横向滚动条。也就是当咱们的视口展现不下内容的时候,会经过滚动条的方式让用户滚动屏幕看到剩余的内容。promise

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

 

<div class="wrapper">
  <ul class="content">
    <li>...</li>
    <li>...</li>
 ... </ul>
</div>

 

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

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

那么,咱们怎么初始化 better-scroll 呢,若是是上述 html 结构,那么初始化代码以下:

 

import BScroll from 'better-scroll' let wrapper = document.querySelector('.wrapper') 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 中的使用方法以下:

<template>
  <div class="wrapper" ref="wrapper">
    <ul class="content">
      <li>...</li>
      <li>...</li>
 ... </ul>
  </div>
</template>
<script> import BScroll from 'better-scroll' export default { mounted() { this.$nextTick(() => { this.scroll = new Bscroll(this.$refs.wrapper, {}) }) } } </script>
View Code

 

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

 

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

 

这段代码比以前稍微复杂一些, 当咱们在滑动列表松开手指时候, 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>

 

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

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

 

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

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

<template>
  <scroll class="wrapper" :data="data" :pulldown="pulldown" @pulldown="loadData">
    <ul class="content">
      <li v-for="item in data">{{item}}</li>
    </ul>
    <div class="loading-wrapper"></div>
  </scroll>
</template>
<script> import BScroll from 'better-scroll' export default { data() { return { data: [], pulldown: true } }, created() { this.loadData() }, methods: { loadData() { requestData().then((res) => { this.data = res.data.concat(this.data) }) } } } </script>
View Code

 

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

相关文章
相关标签/搜索