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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div  class="wrapper">
<ul class="content">
<li>🍔</li>
<li>🍔</li>
<li>🍔</li>
<li>🍔</li>
<li>🍔</li>
<li>🍔</li>
<li>🍔</li>
<li>🍔</li>
<li>🍔</li>
<li>🍔</li>
<li>🍔</li>
<li>🍔</li>
<li>🍔</li>
<li>🍔</li>
</ul>
</div>

接下来就是具体的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),它会阻止divimg等标签的点击事件,其它配置项可以去官网查看并试试

1
2
3
4
5
let bs = BetterScroll.createBScroll(document.querySelector('.wrapper'), {
probeType: 1,
click: true,
pullUpLoad: true
})

然后你就可以试试滚动啦,你可以通过下列代码动态监测当前滚动的位置(position为当前所在坐标),然后做出对应处理(前提是必须probeType为2或3)

1
2
3
bs.on("scroll", (position) => {
console.log(position)
})

pullingUp可以检测你是否到底了(前提pullUpLoad: true),但只能检测一次,下次滚动到最低就没有了,你可以通过better-scrollfinishPullUp()方法多次检测

1
2
3
4
5
bs.on('pullingUp', () => {
console.log('你已经拉到底了')

bs.finishPullUp()
})

vue使用

这个用npm下载完使用会方便点(使用CLI4创建vue项目)

页面结构要求同html的使用方式

引入

1
2
//添加scroll插件
import BScroll from 'better-scroll'

在vue的生命周期函数mounted中去使用该插件,不能在created中使用,为什么呢,因为它刚初始化完,那些元素标签还没加载,直接用就会出现undefined或者null,同时,因为mounted的函数执行完就会boom的没了(函数栈还是内存的栈和堆的关系),所以你需要定义一个属性去接收你new出来的betterScroll对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export default {
name:'Category',
data() {
return {
bs: null
}
},
mounted() {
this.bs = new BScroll(document.querySelector('.wrapper'), {
probeType: 3,
pullUpLoad: true //该属性添加后,probeType的值直接为3(修改成别的也没用)
})
this.bs.on('scroll', (position) => {
console.log(position)
})
this.bs.on('pullingUp', ()=> {
console.log('达到最低了')
this.bs.finishPullUp()
})
}
}

好好享用吧

封装better-scroll

B站老师有着疯狂的封装想法,哈哈哈开玩笑,其实是为了项目的后期更新和维护,我们使用插件前一般都是将其封装完后再去各个组件中使用,避免后面插件不维护更换插件引起的代码修改困难(可能要面临重构,很苦的)

根据上面所学的better-scroll,我们为其定义所需的基本结构,后面在使用的时候就直接往插槽添加标签即可

1
2
3
4
5
6
7
<template>
<div class="wrapper" ref="wrapper">
<div class="content">
<slot></slot>
</div>
</div>
</template>

其它和之前所述vue类似,这里定义两个prop属性probeType和pullUpLoad,使使用bs的组件可以自定义需不需要开启这些功能(避免不必要的运行缓慢)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
   data() {
return {
bs: null
}
},
props: {
probeType: {
type: Number,
default: 0
},
pullUpLoad: {
type: Boolean,
default: false
}
}

同时将scrollpullingUp事件发送出去,让组件自定义需要完成的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mounted() {
this.bs = new BScroll(this.$refs.wrapper, {
click: true,
probeType: this.probeType,
pullUpLoad: this.pullUpLoad,
observeDOM: true
})
//滚动过程实时监听
this.bs.on("scroll", (position) => {
this.$emit('scroll', position)

})
//监听滚动到底部
this.bs.on("pullingUp", ()=> {
this.$emit('pullingUp')
})
}

封装scrollTo()和finishPullUp()方法,让需要的组件自行调用

这里scrollTo使用到三个参数→x,y,time,分别对应x坐标、y坐标以及滚动的时间(平滑度)

1
2
3
4
5
6
7
8
9
10
methods: {
//跳转某位置
scrollTo(x,y,time = 666) {
this.bs.scrollTo(x,y,time)
},
//继续下次滚动到底部响应
finishPullUp() {
this.bs.finishPullUp()
}
}

接下来就是在父组件中使用该组件,引入该组件并添加到components对象中就可以使用啦

1
2
3
4
5
6
7
<scroll class='wrapper' :probe-type="0" :pull-up-load="true">
<home-swiper :banners='banners'></home-swiper>
<home-recommend-view :recommends="recommends"></home-recommend-view>
<feature-view></feature-view>
<tab-control :titles="['流行', '新款', '精选']" class="tab" @tabClick="tabClick"></tab-control>
<goods-list :goods="showGoods"></goods-list>
</scroll>

还有记得给wrapper一个高度,然后就是相关样式的修改,这里学到了一个新知识点,视口单位(vh),id为home的标签高度要设置为vh单位才能实现该滚动功能(我也不知道为什么)还有计算的(cal(num1 - num2)),记得运算符左右都要有空格,否则不生效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<style scoped>
#home {
/* padding-top: 44px; */
/* 视口vh viewport height */
height: 100vh;
position: relative;
}
.home-nav {
position: fixed;
top: 0;
left: 0;
right: 0;
color: white;
z-index: 10;
background-color: var(--color-tint);
}
.tab-control {
position: sticky;
top: 43px;
z-index: 10;
}
.wrapper {
overflow: hidden;
position: absolute;
top: 44px;
bottom: 49px;
left: 0;
right: 0;
}
/* .wrapper {
height: calc(100% - 93px);
overflow: hidden;
margin-top: 44px;
} */
</style>

回到顶部BackTop

这里要做一个小功能,页面滚动到一定程度会弹出小按钮,点击该小按钮可以回到顶部

首先在components的common里新建一个backTop文件夹存放backTop组件,代码比较简单,随心所欲搭建样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<div class="backTop">
<img src="~assets/img/common/top.png" alt="">
</div>
</template>

<script>
export default {
name: "BackTop"
}
</script>

<style scoped>
.backTop {
position: fixed;
right: 10px;
bottom: 55px;
}
.backTop img {
width: 43px;
height: 43px;
}
</style>

接下来就是相关逻辑的实现,首先要在主组件(父组件)给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
2
3
backTopClick() {
this.$refs.scroll.scrollTo(0,0)
},

这样就实现了点击回到顶部的功能,但还有一个逻辑没实现,它是滚动到一定距离才会实现的,所以我们就要首先实时得到我们当前滚动的位置

上面封装betterScroll已经定义了发射事件scroll了,这里通过监听该事件来实现具体操作,在scroll标签定义属性@scroll="contentScroll"动态注册点击事件,同时给back-top标签添加属性v-show="backTopShow"用来决定显示隐藏,backTopShow默认为false,当滚动超过一定距离才显示

然后就是具体实现,当position.y大于1000时则显示,但如果细心的话会发现,实际的y值是负的(因为是往上滑的嘛)所以在其前面加个-号

1
2
3
contentScroll(position) {
this.backTopShow = (-position.y) > 1000
}

上拉加载

同样使用上面封装的pullingUp发射事件,来给scroll注册事件@pullingUp="pulling"

然后具体实现,每次拉到最低就请求一次数据

1
2
3
4
//上拉加载
pulling() {
this.getHomeGoodss(this.currentType)
}

在请求商品数据具体实现中,添加可多次下拉加载的代码,并为每次请求完图片后的异步操作执行动态刷新

1
2
3
4
5
6
7
8
9
10
11
getHomeGoodss(type) {
const page = this.goods[type].page + 1
getHomeGoods(type, page).then(res => {
this.goods[type].list.push(...res.data.list)
this.goods[type].page +=1
//多次上拉加载
this.$refs.scroll.finishPullUp()
//动态刷新
this.$refs.scroll.bs.refresh()
})
}

这里为什么要动态刷新,原因是better-scroll(以下简称BS)会计算它要滚动的数据页长度,然后定下来,比如多个商品数据加起来一共1000px,但是,这个时候图片还没有请求过来(异步请求),所以这1000px中是没有包括图片的长度的,所以在异步请求完图片后,原本是1000px的长度可能就变成2000px,但是BS是不会去改变其值的,也就是说,你就只能刷1000px,然后就滚不下去,所以添加refresh()方法是为了动态刷新,每次有变动就动态刷新一遍,使得数据正常显示

better-scroll优化

上拉加载做完后,偶尔会发现bug出现——下拉的时候,会有一定几率无法下拉,不能正常的加载出图片(和上面的动态刷新同理),所以这里对动态刷新进一步优化–即在加载完图片后进行bs刷新

首先可以看看当前可滚动页面的长度,通过实例化后的better-scroll的scrollerHeight属性查看

  1. 监听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
    3
    imageLoad() {
    this.$emit('itemImageLoad')
    }
  2. 非父子组件通信问题

    在这里,我们需要获取每个itemImage的加载事件,其与home父组件的关系如下图所示(有点丑)

    image-20210126215510747

    在这里,我们需要在home操作GoodsListItem,这咋办,让item发送等待事件给GoodsList组件,再通过GoodsList组件发给Home吗,这看起来感觉有点麻烦会不会,那通过vueX可以不,可以啊,不错的,但是这里要提另一种解决方案,vue提供的事件总线(如下图所示)来解决该问题

    image-20210126215433792

    下面是具体的代码解决方案

    第一步即是在main.js中初始化事件总线($bus)

    1
    Vue.prototype.$bus = new Vue()

    接着,修改之前的发送事件,即GoodsListItem组件中的image图片加载事件

    1
    2
    3
    imageLoad() {
    this.$bus.$emit('itemImageLoad')
    }

    然后,让我们回到scroll.vue中,为了避免图片数据已经过来而这时候的scroll组件还未创建完成,从而导致该scroll实例的方法调用失败。所以我们首先要判断该组件methods中的方法在执行的时候scroll是否已经创建,如果创建的话再去调用其内部方法(利用&&逻辑与进行判断)如下代码所示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    methods: {
    //跳转某位置
    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
    5
     mounted() {
    this.$bus.$on('itemImageLoad', () => {
    this.$refs.scroll.refresh()
    })
    }

    这样,在每次的图片加载后页面便会刷新一次

debounce(防抖)

这里对上述操作做一个防抖处理(JS高级→提升性能),因为每次加载一张图片就要做一次bs刷新,是不是有点浪费性能,我们能不能在加载一定数量的图片后再刷新?答案是肯定的,我们首先要封装一个防抖函数debounce(func,delay),函数的思路是提供两个参数,第一个参数是执行的函数,第二个参数便是规定的时间(即检测该函数在该时间内是否又需要执行一次,是则重置时间),我们在common文件夹下新建一个ultis.js文件,代码如下(你需要掌握以下js原生函数setTimeOut()、clearTimeOut()、apply())

1
2
3
4
5
6
7
8
9
export function debounce(func, delay) {
let timer = null
return function(...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
func.apply(this, args)
}, delay)
}
}

函数的思路

首先你要定义一个变量接收该函数返回值

1
2
const func = debounce(func, delay)
func(args1,args2) //执行

然后该函数执行思路如下

  1. 传入一个需要的执行函数func(注意不要加(),因为需要的是一个变量,不然你传进去的就是函数的返回值了)和一个时间delay
  2. 内部函数timer初始值为null
  3. 返回一个函数,也就是上面的func,同时使用解构赋值(…args)将funcd参数接收
  4. 返回的函数内部进行判断,设置一个你开始赋予delay的定时器,定时器启动,在该时间后执行你传入的函数(func.apply(this, args)),如若这时候你又需要执行一次该函数,那么这个时候timer就会有值,从而执行clearTimeOut函数清除定时器,重新开始计时,如此反复下去进行判断,直到超过该时间或是不再有重复的执行函数进来
  5. 注意:该防抖只是防止频繁执行,并不保证频繁执行次数会化为一次,如果超过该时间,则会增加一次执行次数

项目的使用

首先导入该模块

1
2
//防抖操作
import {debounce} from 'common/utils.js'

使用

1
2
3
4
5
6
mounted() {
const refresh = debounce(this.$refs.scroll.refresh, 500) //注意不要加括号,否则传入的是函数返回值
this.$bus.$on('itemImageLoad', () => {
refresh()
})
}

scroll参数函数优化

使用if进行判断,判断设置参数是否符合函数的执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
mounted() {
this.bs = new BScroll(this.$refs.wrapper, {
click: true,
probeType: this.probeType,
pullUpLoad: this.pullUpLoad,
observeDOM: true
})
//滚动过程实时监听
if(this.probeType === 2 || this.probeType === 3) {
this.bs.on("scroll", (position) => {
this.$emit('scroll', position)

})
}
//监听滚动到底部
if(this.pullUpLoad === true) {
this.bs.on("pullingUp", ()=> {
this.$emit('pullingUp')
})
}
}

吸顶效果

项目进行到这里,你会发现,之前给tab-control设置的css吸顶效果失效了,也就是position:sticky失效了,所以这里我们要用js的方式实现吸顶效果,思路如下

  1. 按照图片加载速度,选择加载最久的哪个图片进行监听load事件(这里选择swipeItem),发送该事件

    1
    <img :src="item.image" alt="" @load="imgLoad">
    1
    2
    3
    4
    5
    methods: {
    imgLoad() {
    this.$emit('swipeImgLoad')
    }
    }
  2. Home绑定该事件,当监听到该图片加载完后,得到TabControl以上的高度($el.offsetTop)并存储在data中(tabOffsetTop)

    1
    2
    3
    4
    //吸顶
    swipeImgLoad() {
    this.tabOffset = this.$refs.tabControl2.$el.offsetTop
    }
  3. 在监听滚动事件(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 //判断是否需要吸顶
    }
  4. 吸顶

    1. 根据属性动态改变tabControl的样式(css:fixed,行不通,因为它处在better-scroll中,better-scroll使用的是css的transfrom属性,没有效果)
    2. 复制一份tab-control。将其与scroll中的content脱离,同时将css属性设置为position:relativeindex: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>
  5. 使用v-show决定脱离scroll的tab-control是否显示(v-show=”isTabFixed”)

  6. 解决拖动过程中两个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
    3
    toY() {
    return this.bs ? this.bs.y : 0
    d}

    Home组件中,在组件非活跃状态(即跳转其它路由)时保留当前位置(data声明saveY变量接收该位置)

    1
    2
    3
    deactivated() {
    this.saveY = this.$refs.scroll.toY()
    }

    在组件活跃(即路由恢复至首页)时,跳转之前保留的位置信息并再次刷新(避免异常卡顿)

    1
    2
    3
    4
    activated() {
    this.$refs.scroll.scrollTo(0,this.saveY,1000)
    this.$refs.scroll.refresh()
    },

    当然,这一切,在better-scroll2.0是不需要做的,它已经帮我们做好了,hiahiahia