4.首页下BetterScroll
Better-scroll使用
BetterScroll 是一款重点解决移动端(已支持 PC)各种滚动场景需求的插件。它的核心是借鉴的 iscroll 的实现,它的 API 设计基本兼容 iscroll,在 iscroll 的基础上又扩展了一些 feature 以及做了一些性能优化。
BetterScroll 是使用纯 JavaScript 实现的,这意味着它是无依赖的。
因为网页在PC端上使用主要是用鼠标滚轮进行的,不会很卡,而如果你换到手机端,进行页面滚动的话就会发现明显的卡顿,这个时候就需要引入插件(或者自己写一个)。这里使用的是better-scroll,下面分两部分介绍它的基本使用(html页面和vue项目)
html使用
npm下载
1 | npm i better-scroll --save |
或者GitHub下载
然后找到dist文件夹,将该文件拖出来,你也可以直接js引用到该文件
1 | <script src="./better-scroll.min.js"></script> |
使用该插件之前需要了解一下知识
你需要将加入到滚动的标签统一放在一个div(其它单独的双标签元素也可)下,之后在此基础下再加上一个div标签进行包裹,如下图所示
页面结构代码演示如下(你可以加上更多’汉堡包’,拖动体验感更佳),使用wrapper包裹content,再加上你需要滚动的元素
1 | <div class="wrapper"> |
接下来就是具体的js逻辑实现了
首先我们要new一个better-scroll,我把它称为better-scroll初始化
这里,需要添加配置项
probeType
- 类型:
number
- 默认值:
0
- 可选值:
1|2|3
- 作用:有时候我们需要知道滚动的位置。当 probeType 为 1 的时候,会非实时(屏幕滑动超过一定时间后)派发scroll 事件;当 probeType 为 2 的时候,会在屏幕滑动的过程中实时的派发 scroll 事件;当 probeType 为 3 的时候,不仅在屏幕滑动的过程中,而且在 momentum 滚动动画运行过程中实时派发 scroll 事件。如果没有设置该值,其默认值为 0,即不派发 scroll 事件
click
- 类型:
boolean
- 默认值:
false
- 作用:BetterScroll 默认会阻止浏览器的原生 click 事件。当设置为 true,BetterScroll 会派发一个 click 事件,我们会给派发的 event 参数加一个私有属性
_constructed
,值为 true。
pullUpLoad
- 类型:
boolean
- 默认值:false
- 作用:动态监测你是否滚动到最底部(在最新版,你已经看不到该配置项了,需要通过引入插件去使用,当然你也可以通过下载完整版去使用该插件)
click
这里要补充一下,它对于本来就该具有的点击事件的元素是不会阻止的(button
),它会阻止div
、img
等标签的点击事件,其它配置项可以去官网查看并试试
1 | let bs = BetterScroll.createBScroll(document.querySelector('.wrapper'), { |
然后你就可以试试滚动啦,你可以通过下列代码动态监测当前滚动的位置(position为当前所在坐标
),然后做出对应处理(前提是必须probeType
为2或3)
1 | bs.on("scroll", (position) => { |
pullingUp
可以检测你是否到底了(前提pullUpLoad: true
),但只能检测一次,下次滚动到最低就没有了,你可以通过better-scroll
的finishPullUp()
方法多次检测
1 | bs.on('pullingUp', () => { |
vue使用
这个用npm下载完使用会方便点(使用CLI4创建vue项目)
页面结构要求同html的使用方式
引入
1 | //添加scroll插件 |
在vue的生命周期函数mounted中去使用该插件,不能在created中使用,为什么呢,因为它刚初始化完,那些元素标签还没加载,直接用就会出现undefined或者null,同时,因为mounted的函数执行完就会boom的没了(函数栈还是内存的栈和堆的关系),所以你需要定义一个属性去接收你new出来的betterScroll对象
1 | export default { |
好好享用吧
封装better-scroll
B站老师有着疯狂的封装想法,哈哈哈开玩笑,其实是为了项目的后期更新和维护,我们使用插件前一般都是将其封装完后再去各个组件中使用,避免后面插件不维护更换插件引起的代码修改困难(可能要面临重构,很苦的)
根据上面所学的better-scroll,我们为其定义所需的基本结构,后面在使用的时候就直接往插槽添加标签即可
1 | <template> |
其它和之前所述vue类似,这里定义两个prop属性probeType和pullUpLoad,使使用bs的组件可以自定义需不需要开启这些功能(避免不必要的运行缓慢)
1 | data() { |
同时将scroll
和pullingUp
事件发送出去,让组件自定义需要完成的功能
1 | mounted() { |
封装scrollTo()和finishPullUp()方法,让需要的组件自行调用
这里scrollTo使用到三个参数→x,y,time,分别对应x坐标、y坐标以及滚动的时间(平滑度)
1 | methods: { |
接下来就是在父组件中使用该组件,引入该组件并添加到components对象中就可以使用啦
1 | <scroll class='wrapper' :probe-type="0" :pull-up-load="true"> |
还有记得给wrapper一个高度,然后就是相关样式的修改,这里学到了一个新知识点,视口单位(vh),id为home的标签高度要设置为vh单位才能实现该滚动功能(我也不知道为什么)还有计算的(cal(num1 - num2)),记得运算符左右都要有空格,否则不生效
1 | <style scoped> |
回到顶部BackTop
这里要做一个小功能,页面滚动到一定程度会弹出小按钮,点击该小按钮可以回到顶部
首先在components的common里新建一个backTop文件夹存放backTop组件,代码比较简单,随心所欲搭建样式
1 | <template> |
接下来就是相关逻辑的实现,首先要在主组件(父组件)给backTop组件注册点击事件,一般来说,是无法直接给创造的组件动态注册点击事件的,所以这里有两种方法,一种是backTop组件内部注册点击事件,再通过$emit发送给父组件使用,第二种直接在父组件的backtop标签中@click.native
直接绑定事件,很明显,第二种方法更简单方便。
1 | <back-top @click.native="backTopClick"></back-top> |
接下来就在父组件的methods中实现该方法,在使用前,我们要首先得到scroll组件才能对其进行滚动操作,操作非常简单在scroll标签添加ref属性即可ref="scroll"
,然后因为上面封装better-scroll的时候耶封装了scrollTo方法,所以直接可以通过this.$refs.scroll
得到该对象后使用自己封装好的scrollTo方法
1 | backTopClick() { |
这样就实现了点击回到顶部的功能,但还有一个逻辑没实现,它是滚动到一定距离才会实现的,所以我们就要首先实时得到我们当前滚动的位置
上面封装betterScroll已经定义了发射事件scroll了,这里通过监听该事件来实现具体操作,在scroll标签定义属性@scroll="contentScroll"
动态注册点击事件,同时给back-top标签添加属性v-show="backTopShow"
用来决定显示隐藏,backTopShow默认为false,当滚动超过一定距离才显示
然后就是具体实现,当position.y大于1000时则显示,但如果细心的话会发现,实际的y值是负的(因为是往上滑的嘛)所以在其前面加个-号
1 | contentScroll(position) { |
上拉加载
同样使用上面封装的pullingUp发射事件,来给scroll注册事件@pullingUp="pulling"
然后具体实现,每次拉到最低就请求一次数据
1 | //上拉加载 |
在请求商品数据具体实现中,添加可多次下拉加载的代码,并为每次请求完图片后的异步操作执行动态刷新
1 | getHomeGoodss(type) { |
这里为什么要动态刷新,原因是better-scroll(以下简称BS)会计算它要滚动的数据页长度,然后定下来,比如多个商品数据加起来一共1000px,但是,这个时候图片还没有请求过来(异步请求),所以这1000px中是没有包括图片的长度的,所以在异步请求完图片后,原本是1000px的长度可能就变成2000px,但是BS是不会去改变其值的,也就是说,你就只能刷1000px,然后就滚不下去,所以添加refresh()方法是为了动态刷新,每次有变动就动态刷新一遍,使得数据正常显示
better-scroll优化
上拉加载做完后,偶尔会发现bug出现——下拉的时候,会有一定几率无法下拉,不能正常的加载出图片(和上面的动态刷新同理),所以这里对动态刷新进一步优化–即在加载完图片后进行bs刷新
首先可以看看当前可滚动页面的长度,通过实例化后的better-scroll的
scrollerHeight
属性查看
监听GoodsListItem组件中的image图片加载事件并发送出去
ps:原生监听加载事件:
el.onload = function() {}
1
2
3
4
5
6
7
8<div class="goods-item" @click="itemClick">
<img :src="goodsItem.show.img" alt="" @load="imageLoad">
<div class="goods-info">
<p>{{goodsItem.title}}</p>
<span class="price">{{goodsItem.price}}</span>
<span class="collect">{{goodsItem.cfav}}</span>
</div>
</div>1
2
3imageLoad() {
this.$emit('itemImageLoad')
}非父子组件通信问题
在这里,我们需要获取每个itemImage的加载事件,其与home父组件的关系如下图所示(有点丑)
在这里,我们需要在home操作GoodsListItem,这咋办,让item发送等待事件给GoodsList组件,再通过GoodsList组件发给Home吗,这看起来感觉有点麻烦会不会,那通过vueX可以不,可以啊,不错的,但是这里要提另一种解决方案,vue提供的
事件总线
(如下图所示)来解决该问题下面是具体的代码解决方案
第一步即是在main.js中初始化事件总线($bus)
1
Vue.prototype.$bus = new Vue()
接着,修改之前的发送事件,即GoodsListItem组件中的image图片加载事件
1
2
3imageLoad() {
this.$bus.$emit('itemImageLoad')
}然后,让我们回到scroll.vue中,为了避免图片数据已经过来而这时候的scroll组件还未创建完成,从而导致该scroll实例的方法调用失败。所以我们首先要判断该组件methods中的方法在执行的时候scroll是否已经创建,如果创建的话再去调用其内部方法(利用&&逻辑与进行判断)如下代码所示
1
2
3
4
5
6
7
8
9
10
11
12
13
14methods: {
//跳转某位置
scrollTo(x,y,time = 666) {
this.bs && this.bs.scrollTo(x,y,time)
},
//刷新
refresh() {
this.bs && this.bs.refresh()
},
//完成底部滚动
finishPullUp() {
this.bs && this.bs.finishPullUp()
}
}然后在home组件中使用该事件(这里是在mounted中去调用该方法,为什么不在created中呢,因为在这个时候的各个组件还未创建完成,可能会导致执行失败)
1
2
3
4
5mounted() {
this.$bus.$on('itemImageLoad', () => {
this.$refs.scroll.refresh()
})
}这样,在每次的图片加载后页面便会刷新一次
debounce(防抖)
这里对上述操作做一个防抖处理(JS高级→提升性能),因为每次加载一张图片就要做一次bs刷新,是不是有点浪费性能,我们能不能在加载一定数量的图片后再刷新?答案是肯定的,我们首先要封装一个防抖函数debounce(func,delay),函数的思路是提供两个参数,第一个参数是执行的函数,第二个参数便是规定的时间(即检测该函数在该时间内是否又需要执行一次,是则重置时间),我们在common文件夹下新建一个ultis.js文件,代码如下(你需要掌握以下js原生函数setTimeOut()、clearTimeOut()、apply())
1 | export function debounce(func, delay) { |
函数的思路
首先你要定义一个变量接收该函数返回值
1 | const func = debounce(func, delay) |
然后该函数执行思路如下
- 传入一个需要的执行函数func(注意不要加(),因为需要的是一个变量,不然你传进去的就是函数的返回值了)和一个时间delay
- 内部函数timer初始值为null
- 返回一个函数,也就是上面的func,同时使用解构赋值(…args)将funcd参数接收
- 返回的函数内部进行判断,设置一个你开始赋予delay的定时器,定时器启动,在该时间后执行你传入的函数(func.apply(this, args)),如若这时候你又需要执行一次该函数,那么这个时候timer就会有值,从而执行clearTimeOut函数清除定时器,重新开始计时,如此反复下去进行判断,直到超过该时间或是不再有重复的执行函数进来
- 注意:该防抖只是防止频繁执行,并不保证频繁执行次数会化为一次,如果超过该时间,则会增加一次执行次数
项目的使用
首先导入该模块
1 | //防抖操作 |
使用
1 | mounted() { |
scroll参数函数优化
使用if进行判断,判断设置参数是否符合函数的执行
1 | mounted() { |
吸顶效果
项目进行到这里,你会发现,之前给tab-control设置的css吸顶效果失效了,也就是position:sticky
失效了,所以这里我们要用js的方式实现吸顶效果,思路如下
按照图片加载速度,选择加载最久的哪个图片进行监听load事件(这里选择swipeItem),发送该事件
1
<img :src="item.image" alt="" @load="imgLoad">
1
2
3
4
5methods: {
imgLoad() {
this.$emit('swipeImgLoad')
}
}Home绑定该事件,当监听到该图片加载完后,得到TabControl以上的高度($el.offsetTop)并存储在data中(tabOffsetTop)
1
2
3
4//吸顶
swipeImgLoad() {
this.tabOffset = this.$refs.tabControl2.$el.offsetTop
}在监听滚动事件(contentScroll())中监听当前位置是否与data中tabOffsetTop的值一致,一致则将data中isTabFixed的值改为true(自己添加,默认为false)
1
2
3
4
5//监听滚动
contentScroll(position) {
this.backTopShow = (-position.y) > 1000
this.isTabfixed = (-position.y) > this.tabOffset //判断是否需要吸顶
}吸顶
- 根据属性动态改变tabControl的样式(css:fixed,行不通,因为它处在better-scroll中,better-scroll使用的是css的transfrom属性,没有效果)
- 复制一份tab-control。将其与scroll中的content脱离,同时将css属性设置为
position:relative
和index:10
,以显示在顶部
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19<div id="home">
<nav-bar class="home-nav"><div slot="center">购物街</div></nav-bar>
<tab-control
:titles="['流行', '新款', '精选']"
class="tab-control"
@tabClick="tabClick"
ref="tabControl1"
v-show="isTabfixed"></tab-control>
<scroll
class='wrapper'
ref="scroll"
:probe-type="3"
@scroll="contentScroll"
:pull-up-load="true"
@pullingUp="pulling">
。。。
</scroll>
<back-top @click.native="backTopClick" v-show="backTopShow"></back-top>
</div>使用v-show决定脱离scroll的tab-control是否显示(v-show=”isTabFixed”)
解决拖动过程中两个tab-control选项不同的问题:将其ref分别改为tabControl1和tabControl2,并在tabClick事件中将两个的currentIndex改为当前选项index
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16//首页数据处理
tabClick(index) {
switch (index) {
case 0:
this.currentType = 'pop'
break
case 1:
this.currentType = 'new'
break
case 2:
this.currentType = 'sell'
break
}
this.$refs.tabControl1.currentIndex = index
this.$refs.tabControl2.currentIndex = index
}跳转保留首页状态
核心思路:使用keep-alive标签解决当前页面状态,以及scroll.y保留scroll当前位置
在App.vue中使用keep-alive标签
1
2
3
4
5
6<div id="app">
<keep-alive>
<router-view></router-view>
</keep-alive>
<main-tab-bar></main-tab-bar>
</div>封装scroll的y位置函数(methods中)
1
2
3toY() {
return this.bs ? this.bs.y : 0
d}Home组件中,在组件非活跃状态(即跳转其它路由)时保留当前位置(data声明saveY变量接收该位置)
1
2
3deactivated() {
this.saveY = this.$refs.scroll.toY()
}在组件活跃(即路由恢复至首页)时,跳转之前保留的位置信息并再次刷新(避免异常卡顿)
1
2
3
4activated() {
this.$refs.scroll.scrollTo(0,this.saveY,1000)
this.$refs.scroll.refresh()
},当然,这一切,在better-scroll2.0是不需要做的,它已经帮我们做好了,hiahiahia