Vue.js移动端实现上拉加载与下拉刷新:基于better-scroll的实战封装

作者:小布丁时间:2026-10-22 08:02:28浏览:140

本文详解在Vue.js移动端开发中,如何利用better-scroll库实现上拉加载和下拉刷新功能。文章重点解析了pullDownRefresh与pullUpLoad的配置参数,针对淘宝flexible.js适配导致的屏幕缩放问题提供了动态计算阈值的方法,并封装了可复用的滚动组件,包含事件监听、防抖开关及插槽分发机制,帮助开发者高效构建流畅的列表交互体验。

移动端列表交互功能概述

在移动端应用开发中,上拉加载更多数据与下拉刷新列表是极为常见的交互功能,广泛应用于搜索结果页或各类分类列表页面中。

移动端列表交互功能概述

基于better-scroll的配置与适配处理

与横向滚动效果的实现方式类似,本文继续采用better-scroll库来完成相关功能。鉴于better-scroll已更新至较新版本(如1.2.6),相较于早期的0.x版本,新版提供了更为便捷的API,因此本文基于新API重新编写了实现代码。

首先需完成基础样式的编写,此处省略具体样式代码,随后引入better-scroll库即可开始功能开发。


import BScroll from 'better-scroll'

接着,在Vue组件的mounted生命周期钩子中实例化scroll对象。开发者可以选择在获取完数据后再进行new操作,或者先实例化scroll,待数据获取完成后调用refresh方法更新状态。

实例化过程中需传入配置参数,由于参数选项较多,具体细节建议参考官方文档。此处仅重点讲解两个关键配置项。


//是否开启下拉刷新,可传入true或者false,如果需要更多配置可以传入一个对象

pullDownRefresh:{
  threshold:80,
  stop:40
}

//是否开启上拉加载,同上,上拉无stop参数,这里需要注意是负数
pullUpLoad:{
  threshold:-80,
}

/**
 * 
 * @param threshold 触发事件的阀值,即滑动多少距离触发
 * @param stop 下拉刷新后回滚距离顶部的距离(为了给loading留出一点空间)
 */

上述数值经个人测试认为较为合适,但存在一个兼容性问题:由于项目采用淘宝flexible.js进行屏幕适配,导致在不同设备上表现不一致。例如,在安卓设备上80像素的距离触发点较为适宜,但在iPhone 6s等设备上,因屏幕被放大了3倍,实际触发距离仅约为27像素。

因此,针对不同缩放比例的屏幕,必须乘以相应的缩放比来动态调整触发阈值。

淘宝flexible.js源码中已包含获取屏幕缩放比的方法,此处直接复用该逻辑。


//在util.js里面加一个方法
export function getDeviceRatio(){
  var isAndroid = window.navigator.appVersion.match(/android/gi);
  var isIPhone = window.navigator.appVersion.match(/iphone/gi);
  var devicePixelRatio = window.devicePixelRatio;
  var dpr;
  if (isIPhone) {
    // iOS下,对于2和3的屏,用2倍的方案,其余的用1倍方案
    if (devicePixelRatio >= 3) {        
      dpr = 3;
    } else if (devicePixelRatio >= 2){
      dpr = 2;
    } else {
      dpr = 1;
    }
  } else {
    // 其他设备下,仍旧使用1倍的方案
    dpr = 1;
  }
  return dpr
}


import{ DEVICE_RATIO} from '../base/js/api.js'
/*获取当前缩放比*/
const DEVICE_RATIO=getDeviceRatio();


 /*下拉配置*/
const DOWN_CONFIG={
 threshold:80*DEVICE_RATIO,
 stop:40*DEVICE_RATIO
}
/*上拉配置*/
const UP_CONFIG={
 threshold:-80*DEVICE_RATIO,
}

this.scroller = new BScroll(scrollWrap,{
 click:true,
 probeType:3,
 pullDownRefresh:DOWN_CONFIG,
 pullUpLoad:UP_CONFIG
});

事件监听与流程控制

完成实例化后,下一步是监听上拉和下拉事件。better-scroll新增了一系列事件接口,主要涉及以下两类。


/*下拉事件*/
this.scroller.on('pullingDown',()=> {});

/*上拉事件*/
this.scroller.on('pullingUp',()=>{});

当触发上拉或下拉事件后,必须调用this.scroller.finishPullDown()或this.scroller.finishPullUp()方法来通知better-scroll事件处理完毕,以便恢复滚动状态。

整体业务流程大致如下所示。


this.scroller.on('pullingDown',()=> {
  
  <!-- 1. 发送请求获取数据 -->
  
  <!-- 2. 获取成功后,通知事件完成 -->
  
  <!-- 3. 修改data数据,在nextTick调用refresh -->
});

通常,操作完成后需手动触发refresh方法以重新计算可滚动区域的高度。为此,可以编写一个watch监听器来监控数据变化,这样只需修改数据源,无需在每次操作后手动调用refresh方法。


watch:{
 dataList(){
  this.$nextTick(()=>{
   this.scroller.refresh(); 
  }) 
 }
},

若使用的版本较旧,未包含新增API,则可在scroll事件中通过判断位置来实现相同功能。


this.scroller.on("scroll",(pos)=>{ 
  //获取整个滚动列表的高度
  var height=getStyle(scroller,"height");

  //获取滚动外层wrap的高度
  var pageHeight=getStyle(scrollWrap,"height");

  //触发事件需要的阀值
  var distance=80*DEVICE_RATIO;

  //参数pos为当前位置

  if(pos.y>distance){ 

    //console.log("下拉");
    //do something
   
  }else if(pos.y-pageHeight<-height-distance){

    //console.log("上拉");
    //do something
  }

为防止事件被多次连续触发,需要引入两个布尔类型的开关变量。


var onPullUp=true;

var onPullDown=true;

每次触发事件时,将对应的开关置为false;待操作完成后,再将其重置为true。这种机制可确保同一时间仅有一个事件在处理,避免重复触发下拉或上拉逻辑。

滚动组件封装与使用

最后,将上述逻辑封装为一个独立的Vue组件。


 <template>
  <div ref="wrapper" class="list-wrapper"> 
    <div class="scroll-content">    
      <slot></slot>     
    </div>   
  </div>
</template>

由于不同页面需要滚动的具体内容各异,因此使用插槽(slot)来分发内容,以支持多样化的业务场景。

组件所需的配置参数由父组件传入,通过props接收并设置默认值。


 export default {
  props: {
   dataList:{
    type: Array,
    default: []
   },
   probeType: {
    type: Number,
    default: 3
   },
   click: {
    type: Boolean,
    default: true
   },  
   pullDownRefresh: {
    type: null,
    default: false
   },
   pullUpLoad: {
    type: null,
    default: false
   },  
  }

组件挂载后,在事件触发时并不直接处理业务逻辑,而是向父组件发射自定义事件。父组件通过模板中的v-on指令接收这些事件,并负责处理后续的具体逻辑。


mounted() {
  this.scroll = new BScroll(this.$refs.wrapper, {
      probeType: this.probeType,
      click: this.click,    
      pullDownRefresh: this.pullDownRefresh,
      pullUpLoad: this.pullUpLoad,
    })

  this.scroll.on('pullingUp',()=> {
    if(this.continuePullUp){
      this.beforePullUp();
      this.$emit("onPullUp","当前状态:上拉加载");
    }
  });

  this.scroll.on('pullingDown',()=> {
    this.beforePullDown();
    this.$emit("onPullDown","当前状态:下拉加载更多");
  }); 
}

父组件在使用该组件时,需传入配置参数Props,监听子组件发射的事件,并使用具体内容替换slot标签。


  <Scroller 
    id="scroll"
    ref="scroll" 
    :dataList="filmList"
    :pullDownRefresh="DOWN_CONFIG"
    :pullUpLoad="UP_CONFIG"
    @onPullUp="pullUpHandle"
    @onPullDown="pullDownHandle"
   >

    <ul>
       <router-link class="film-list" v-for="(v,i) in filmList" :key="v.id" tag="li" :to='{path:"/film-detail/"+v.id}'>
          <div class="film-list__img">
             <img v-lazy="v.images.small" alt="" />        
          </div>
          <div class="film-list__detail">
            <p class="film-list__detail__title">{{v.title}}</p>
            <p class="film-list__detail__director">导演:{{filterDirectors(v.directors)}}</p>
            <p class="film-list__detail__year">年份:{{v.year}}<span>{{v.stock}}</span></p>
            <p class="film-list__detail__type">类别:{{v.genres.join(" / ")}}<span></span></p>
            <p class="film-list__detail__rank">评分:<span>{{v.rating.average}}分</span></p>
          </div>             
        </router-link>
     </ul>     
   </Scroller>

父组件可通过this.$refs.xxx获取子组件实例,进而调用子组件内部暴露的方法。


 computed:{
    scrollElement(){
      return this.$refs.scroll
    }
  }

完整的scroller组件代码如下所示。


   <template>
     <div ref="wrapper" class="list-wrapper"> 
       <div class="scroll-content">    
         <slot></slot>
         <div>
           <PullingWord v-show="!inPullUp&&dataList.length>0" :loadingWord="beforePullUpWord"></PullingWord>
           <Loading v-show="inPullUp" :loadingWord='PullingUpWord'></Loading>
         </div>    
       </div> 

       <transition name="pullDown">
         <Loading class="pullDown" v-show="inPullDown" :loadingWord='PullingDownWord'></Loading>
       </transition> 
     </div>
   </template>


   <script >
    import BScroll from 'better-scroll'
    import Loading from './loading.vue'
    import PullingWord from './pulling-word'

    const PullingUpWord="正在拼命加载中...";
    const beforePullUpWord="上拉加载更多";
    const finishPullUpWord="加载完成";

    const PullingDownWord="加载中...";

    export default {
     props: {
      dataList:{
       type: Array,
       default: []
      },
      probeType: {
       type: Number,
       default: 3
      },
      click: {
       type: Boolean,
       default: true
      },  
      pullDownRefresh: {
       type: null,
       default: false
      },
      pullUpLoad: {
       type: null,
       default: false
      },  
     },
     data() {
       return { 
         scroll:null,
         inPullUp:false,
         inPullDown:false,
         beforePullUpWord,
         PullingUpWord,
         PullingDownWord,
         continuePullUp:true
       }
     },
      
     mounted() {
       setTimeout(()=>{
         this.initScroll();

         this.scroll.on('pullingUp',()=> {
           if(this.continuePullUp){
             this.beforePullUp();
             this.$emit("onPullUp","当前状态:上拉加载");
           }
         });

         this.scroll.on('pullingDown',()=> {
           this.beforePullDown();
           this.$emit("onPullDown","当前状态:下拉加载更多");
         });

       },20)
       
     },
     methods: {
       initScroll() {
         if (!this.$refs.wrapper) {
           return
         }
         this.scroll = new BScroll(this.$refs.wrapper, {
           probeType: this.probeType,
           click: this.click,    
           pullDownRefresh: this.pullDownRefresh,
           pullUpLoad: this.pullUpLoad,
         })
       },
       beforePullUp(){
         this.PullingUpWord=PullingUpWord;
         this.inPullUp=true;
       }, 
       beforePullDown(){
         this.disable();
         this.inPullDown=true;
       },
       finish(type){
         this["finish"+type]();
         this.enable();
         this["in"+type]=false; 
       },
       disable() {
         this.scroll && this.scroll.disable()
       },
       enable() {
         this.scroll && this.scroll.enable()
       },
       refresh() {
         this.scroll && this.scroll.refresh()
       }, 
       finishPullDown(){
         this.scroll&&this.scroll.finishPullDown()
       },
       finishPullUp(){
         this.scroll&&this.scroll.finishPullUp()
       },   
     },
        
     watch: {
       dataList() {        
         this.$nextTick(()=>{
           this.refresh();            
         }) 
       }
     },
     components: {
       Loading,
       PullingWord
     }
    }

   </script>

具体实现细节可访问GitHub查看,项目地址为:https://github.com/linrunzheng/vueApp。

以上就是本文的全部内容,希望能为各位的学习提供帮助,也欢迎大家继续支持本站。

作者最新文章