概述
better-scroll 它的 html 结构
<div class="wrapper">
<ul class="content">
<li>...</li>
<li>...</li>
...
</ul>
<!-- 这里可以放一些其它的 DOM,但不会影响滚动 -->
</div>
上面的代码中:
- better-scroll 是作用在外层 wrapper 容器上的
- 滚动的部分是 content 元素
- 这里要注意的是,better-scroll 只处理容器(wrapper)的第一个子元素(content)的滚动其它的元素都会被忽略。
better-scroll 初始化【既获取外层 wrapper 容器,并将 better-scroll 是作用在它上面】:
import BScroll from 'better-scroll'
let wrapper = document.querySelector('.wrapper')
let scroll = new BScroll(wrapper,{})
- better-scroll 提供了一个类,
- 实例化的第一个参数是一个原生的 DOM 对象。例如上面的 document.querySelector(‘.wrapper’);这也说明了 better-scroll 是作用在外层 wrapper 容器上,第二个参数是配置选项
- 如果传递的是一个字符串,better-scroll 内部会尝试调用 querySelector 去获取这个 DOM 对象,所以初始化代码也可以是这样:
import BScroll from ‘better-scroll’
let scroll = new BScroll(‘.wrapper’,{})
滚动原理
浏览器的滚动条:
当页面内容的高度超过视口高度的时候,会出现纵向滚动条;当页面内容的宽度超过视口宽度的时候,会出现横向滚动条。也 就是 当我们的视口展示不下内容的时候,会通过滚动条的方式让用户滚动屏幕看到剩余的内容。
better-scroll 也是一样的原理,我们可以用一张图更直观的感受一下:
better-scroll 的滚动原理:
- 绿色部分为 wrapper,也就是父容器,它会有固定的高度【这一点很重要,很多时候我们在 better-scroll 初始化了,但是没法滚动】
- 黄色部分为 content,它是父容器的第一个子元素,它的高度会随着内容的大小而撑高。
- 当 content 的高度不超过父容器的高度,是不能滚动的,而它一旦超过了父容器的高度,我们就可以滚动内容区了
- better-scroll 的初始化时机很重要,因为它在初始化的时候,会计算父元素和子元素的高度和宽度,来决定是否可以纵向和横向滚动。
- 我们在初始化它的时候,必须确保父元素和子元素的内容已经正确渲染了。
- 如果子元素或者父元素 DOM 结构发生改变的时候,必须重新调用 scroll.refresh() 方法重新计算来确保滚动效果的正常。
- 所以 better-scroll 不能滚动的原因多半是初始化 better-scroll 的时机不对,或者是当 DOM 结构发送变化的时候并没有重新计算 better-scroll。
参数配置
better-scroll 支持很多参数配置,可以在初始化的时候传入第二个参数,比如:
let scroll = new BScroll(‘.wrapper’,{
scrollY: true,
click: true
})
默认情况:
纵向滚动,横向不滚动。
对工作中配置用到的一些介绍:
probeType:可选值:1、2、3。
有时候我们 需要知道滚动的位置:
- 当 probeType 为 1 的时候,会非实时(屏幕滑动超过一定时间后)派发 scroll 事件;
- 当 probeType 为 2 的时候,会在 屏幕滑动的过程中 实时的派发 scroll 事件;
- 当 probeType 为 3 的时候,不仅在屏幕滑动的过程中,而且在 momentum 滚动动画运行过程中实时派发 scroll 事件。
- 如果没有设置该值,其默认值为 0,即不派发 scroll 事件。
momentum:当快速在屏幕上滑动一段距离的时候,会根据滑动的距离和时间计算出动量,并生成滚动动画。设置为 true 则开启动画
better-scroll 遇见 Vue
参考文章:https://zhuanlan.zhihu.com/p/27407024
异步数据的处理
在我们的实际工作中,列表的 数据往往都是异步获取 的,因此我们 初始化 better-scroll 的时机需要在数据获取后,代码如下:
<template>
<div class="wrapper" ref="wrapper">
<ul class="content">
<li v-for="item in data">{{item}}</li>
</ul>
</div>
</template>
<script>
import BScroll from 'better-scroll'
export default {data() {
return {data: []
}
},
created() {requestData().then((res) => {
this.data = res.data
this.$nextTick(() => {this.scroll = new Bscroll(this.$refs.wrapper, {})
})
})
}
}
</script>
在 better-scroll 滚动原理中我们知道,在初始化 better-scroll 时必须确保父元素和子元素的内容已经正确渲染了,那么初始化【this.scroll = new Bscroll(this.$refs.wrapper, {})】的时候不是应该在 mounted 中吗,但是这里的初始化为什么是在 created 中呢?
为什么这里在 created 这个钩子函数里请求数据而不是放到 mounted 的钩子函数里?
- 因为 requestData 是发送一个网络请求,这是一个异步过程,
- 当拿到响应数据的时候,Vue 的 DOM 早就已经渲染好了,
- 但是数据改变 —> DOM 重新渲染仍然是一个异步过程,所以即使在我们拿到数据后,也要异步初始化 better-scroll。
数据的动态更新
除了数据异步获取,还有一些场景可以动态更新列表中的数据,比如常见的下拉加载,上拉刷新等,这些动作都有一个共同点,就是数据发生了变化,进而造成了,dom 也会跟着变化
better-scroll 配合 Vue 实现下拉加载功能
<template>
<div class="wrapper" ref="wrapper">
<ul class="content">
<li v-for="item in data">{{item}}</li>
</ul>
<div class="loading-wrapper"></div>
</div>
</template>
<script>
import BScroll from 'better-scroll'
export default {data() {
return {data: []
}
},
created() {this.loadData()
},
methods: {loadData() {requestData().then((res) => {this.data = res.data.concat(this.data)
this.$nextTick(() => {if (!this.scroll) {this.scroll = new Bscroll(this.$refs.wrapper, {})
this.scroll.on('touchend', (pos) => {
// 下拉动作
if (pos.y > 50) {this.loadData()
}
})
} else {this.scroll.refresh()
}
})
})
}
}
}
</script>
上面代码的总结:
- 滑动列表松开手指时候【touchend 事件:鼠标 / 手指离开】,better-scroll 会对外派发一个 touchend 事件并对外抛出一个参数 pos【{x, y} 位置坐标】;
- 监听了这个事件,并且判断了 pos.y > 50(我们把这个行为定义成一次下拉的动作)。如果是下拉的话我们会重新请求数据【this.loadData()】,并且把新的数据和之前的 data 做一次 concat,也就 更新了列表的数据,那么数据的改变就会映射到 DOM 的变化。
- 重新请求数据就会对 this.scroll 重新做判断了,如果没有初始化过我们会通过 new BScroll 初始化【例如上面的:this.scroll = new Bscroll(this.$refs.wrapper, {})】,并且绑定一些事件【比如上面的 this.scroll.on(‘touchend’, (pos) => {}】,否则我们会调用 this.scroll.refresh 方法重新计算,来确保滚动效果的正常
scroll 组件的抽象和封装
很多类似滚动的组件,我们就需要写很多类似的命令式且重复性的代码,而且我们把数据请求和 better-scroll 也做了强耦合;但是在 Vue 项目中我们希望代码之间是低耦合;
croll 组件本质上就是一个可以滚动的列表组件,至于列表的 DOM 结构,只需要满足 better-scroll 的 DOM 结构规范即可,具体用什么标签,有哪些辅助节点(比如下拉刷新上拉加载的 loading 层),这些都不是 scroll 组件需要关心的。因此,scroll 组件的 DOM 结构十分简单,如下所示:
<template>
<div ref="wrapper">
<slot></slot>
</div>
</template>
mounted() {
// 保证在 DOM 渲染完毕后初始化 better-scroll
setTimeout(() => {this._initScroll()
}, 20)
},
// 是否派发顶部下拉事件,用于下拉刷新
if (this.pulldown) {this.scroll.on('touchend', (pos) => {
// 下拉动作
if (pos.y > 50) {this.$emit('pulldown',pos)
}
})
}
// 监听数据的变化,延时 refreshDelay 时间后调用 refresh 方法重新计算,保证滚动效果正常
watch: {data() {setTimeout(() => {this.refresh()
}, this.refreshDelay)
}
}
- JS 部分实际上就是对 better-scroll 做一层 Vue 的封装,
- 通过 props 的形式,把一些对 better-scroll 定制化的控制权交给父组件;
- 通过 methods 暴露的一些方法对 better-scroll 的方法做一层代理;
- 通过 watch 传入的 data,当 data 发生改变的时候,在适当的时机调用 refresh 方法重新计算 better-scroll 确保滚动 效果正常,
- 这里之所以要有一个 refreshDelay 的设置是考虑到如果我们对列表操作用到了 transition-group 做动画效果,那么 DOM 的渲染完毕时间就是在动画完成之后。
利用封装的 scroll 组件实现上面的下拉加载功能
html 代码:
<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>
js 代码:
<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>
- 可以很明显的看到我们的 JS 部分精简了非常多的代码,
- 没有对 better-scroll 再做命令式的操作了【这些操作都在 srcoll 组件中的 methods 的方法中写好了,我们需要哪些操作只需要调用就好了,比如我们想实现下拉那么我们就用 @pulldown=”loadData”】,
- 同时把数据请求和 better-scroll 也做了剥离【在这个页面中我们只想看到起请求数据,自己的数据 this.data 有更新就好了,其他的交给 srcoll 组件就好了】
- 父组件只需要把数据 data 通过 prop 传给 scroll 组件,就可以保证 scroll 组件的滚动效果。
- 同时,如果想实现下拉刷新的功能,只需要通过 prop 把 pulldown 设置为 true,并且监听 pulldown 的事件去做一些数据获取并更新的动作即可,整个逻辑也是非常清晰的。