纯JavaScript简单实现移动端网页的上拉加载、下拉刷新

文章目录

  • 创建一个类PullData
    • 获取所需的dom节点
    • 监听touchstart、touchmove、touchend事件
    • 修正方法的this指向
    • touchstart、touchmove、touchend监听回调事件
    • 类PullData的完整代码:
  • 使用PullData

公司要在安卓软件中,添加H5网页包,实现订单卡片列表,要求有上拉加载、下拉刷新的功能。
经过搜索资料后,实现如下:

创建一个类PullData

class PullData() {
	/**
	  * 类的构造函数,在new PullData({xx: 'xxx'})时,参数会从consctructor中传入PullData类中
	  * options就是传入的{xx: 'xxx'},
	  * 先用一个变量将传入的参数存起来
	  */ 
	constructor(options) {
		this.data = options;
	}
}

获取所需的dom节点

实现订单列表的上拉、下拉,需要操作 dom 节点,所以我们要传入一个dom进行操作。

class PullData() {
	constructor(options) {
		this.data = options;
		// 用domWrapper来存储dom节点,如果没有传入就手动设置为body节点
		if(!options.dom) {
	      // 如果没有传入dom,默认是body
	      this.domWrapper = document.body;
	    } else {
	      // 传入的dom必须是 Nodelist 节点,或者是属性选择器字符串
	      this.domWrapper = typeof options.dom === 'string' ? document.querySelector(options.dom) : options.dom;
	    }
	}
}

除了订单列表的dom节点,还要初始化显示刷新中...加载中...文案的dom节点,因为我的订单列表是铺整页的,所以写的刷新、加载用 position: fixed 分别在页面顶部、页面底部。
纯JavaScript简单实现移动端网页的上拉加载、下拉刷新_第1张图片

class PullData() {
	constructor(options) {
		this.data = options;
		// 用domWrapper来存储dom节点,如果没有传入就手动设置为body节点
		if(!options.dom) {
	      // 如果没有传入dom,默认是body
	      this.domWrapper = document.body;
	    } else {
	      // 传入的dom必须是 Nodelist 节点,或者是属性选择器字符串
	      this.domWrapper = typeof options.dom === 'string' ? document.querySelector(options.dom) : options.dom;
	    }
	    // 调用初始化方法,进行数据、节点初始化等
	    this.init();
	}
	// 初始化
	init() {
	   this.renderTextDom();
	 }
	 // 创建渲染【刷新】【加载】文案所在的节点
	 renderTextDom() {
	   // 刷新提示语的节点
	   let refreshTextDom = document.createElement('div');
	   refreshTextDom.id = "refresh-text";
	   refreshTextDom.innerHTML = "刷新中...";
	   refreshTextDom.style = "display:none;position:fixed;top:0.2rem;left: 50%;transform: translateX(-50%);";
	   // 加载提示语的节点
	   let loadTextDom = document.createElement('div');
	   loadTextDom.id = "load-text";
	   loadTextDom.innerHTML = "加载中...";
	   loadTextDom.style = "display:none;position:fixed;bottom:0.2rem;left: 50%;transform: translateX(-50%);";
	
	   document.body.appendChild(refreshTextDom);
	   document.body.appendChild(loadTextDom);
	 }
}

监听touchstart、touchmove、touchend事件

上拉、下拉要通过touchstarttouchmovetouchend去监听用户的触摸动作。通过scroll事件记录当前滚动的位置、滚动的总高度,在触顶触底的时候,再修改translateY。在初始化的时候,同时开启监听事件

init() {
	// 滚动事件
    this.domWrapper.addEventListener('scroll', function (e) {
		// 监听操作
	}, false);
	// 触摸开始事件
    this.domWrapper.addEventListener('touchstart', function (e) {
		// 监听操作
	}, false);

    // 触摸移动事件
    this.domWrapper.addEventListener('touchmove', function (e) {
		// 监听操作
	}, false);

    // 触摸结束事件
    this.domWrapper.addEventListener("touchend", function (e) {
		// 监听操作
	}, false);
}

用户不需要使用这个上拉下拉功能后,需要清除监听事件,否则监听事件只增不减,就会影响浏览器性能。所以在开启监听事件时,方法不能使用匿名函数,否则无法销毁对应的监听事件。

init() {
    this.renderTextDom();
    // 滚动事件
    this.domWrapper.addEventListener('scroll', this.scroll, false);
    // 触摸开始事件
    this.domWrapper.addEventListener('touchstart', this.start, false);
    // 触摸移动时间
    this.domWrapper.addEventListener('touchmove', this.move, false);
    // 触摸结束事件
    this.domWrapper.addEventListener("touchend", this.end , false)
  }
  // 滚动事件回调
  scroll(e) {}
  // 触摸开始事件回调
  start(e) {}
  // 触摸移动事件回调
  move (e) {}
  // 触摸结束事件回调
  end(e) {}
  // 销毁监听函数
  destroy() {
	this.domWrapper.removeEventListener('scroll', this.scroll);
    this.domWrapper.removeEventListener('touchmove', this.move);
    this.domWrapper.removeEventListener('touchstart', this.start);
    this.domWrapper.removeEventListener('touchend', this.end);
  }

修正方法的this指向

注意: addEventListener监听 this.domWrapper.addEventListener('touchstart', this.start, false);,此时的 this.start 函数内部的this指向的是 this.domWrapper,如果我要在this.start内部调用PullData内部的方法 this.beforeAction,就会报错Uncaught TypeError: this.beforeAction is not a function。所以我们要修正这些方法的this指向

class PullData() {
	constructor(options) {
		// ...
		
		// 将this.scroll 的this指向 PullData 这个类(原来指向nodelist节点)
    	this.scroll = this.scroll.bind(this); 
		// 将this.start 的this指向 PullData 这个类(原来指向nodelist节点)
    	this.start = this.start.bind(this); 
    	// 将this.move 的this指向 PullData 这个类(原来指向nodelist节点)
    	this.move = this.move.bind(this); 
    	// 将this.end 的this指向 PullData 这个类(原来指向nodelist节点)
    	this.end = this.end.bind(this); 
		
	    // 调用初始化方法,进行数据、节点初始化等
	    this.init();
	}
	// ...
}

touchstart、touchmove、touchend监听回调事件

开始时,记录手指/鼠标点击触碰到屏幕的位置,移动时,通过translateY修改下拉/上拉时卡片移动的距离,呈现拉动效果,手指/鼠标松开后,将拉动的距离逐渐恢复到translateY(0px)

// 构造函数
constructor(options) {
	// ...
	this.rule = options.rule || 50; // 定义触发加载/刷新事件的拉伸长度
	this.startY = 0; // 记录鼠标/手指点击的位置
	this.moveY = 0; // 记录鼠标/手指移动的位置
	this.distance = 0; // 记录鼠标/手指移动的距离
	this.clientHeight = 0; // 滚动的可视区
	this.scrollTop = 0; // 滚动的距离
	this.scrollHeight = 0; // 滚动的总高度
	// 上拉加载事件回调函数
	this.pullUpCallback = options.pullUpCallback.bind(this) || function() {};
	// 下拉刷新事件回调函数
	this.pullDownCallback = options.pullDownCallback.bind(this) || function() {};
	// ...
}
// 触摸开始事件回调
  start (e) {
    this.startY = e.touches[0].screenY - this.distance;
  }

  // 触摸移动事件回调
  move (e) {
    e.stopPropagation(); // 防止tranlateY拉动的时候,列表内部也在scroll滚动
    this.moveY = e.touches[0].screenY;
    this.distance = Math.floor(this.moveY - this.startY) / 5; // 除以5,控制手指滑动距离和页面被拉下来的的距离之间的比例
    // 显示/隐藏“刷新中”“加载中”等文案
    this.beforeAction(this.distance);
    // 只有在触顶或者触底时,才操作domWrapper的样式
    if(this.scrollTop <= 0 || this.scrollTop + this.clientHeight + this.rule >= this.scrollHeight) {
      // 这一步才让人有被“拉”下来/上去的视觉效果
      this.domWrapper.style.transform = `translateY(${this.distance}px)`
    }
  }

  // 触摸结束事件回调
  end(e) {
    if(this.timer) clearInterval(this.timer);
    this.timer = setInterval(() => {
      if(Math.floor(this.distance) < 0 || Math.ceil(this.distance) < 10) {
        // 移动距离恢复到 10 以内时,将domWrapper的translateY设置为0,清除定时器
        this.distance = 0;
        // 这一步是“回弹到原位”的视觉效果
        this.domWrapper.style.transform = `translateY(0px)`;
        clearInterval(this.timer);
      } else {
        // 每10ms 减去 this.distance/10的距离
        this.distance -= this.distance / 10;
        // 只有在触顶或者触底时,才操作domWrapper的样式
        if(this.scrollTop <= 0 || this.scrollTop + this.clientHeight + this.rule >= this.scrollHeight) {
          // 这一步是“回弹”过程的视觉效果
          this.domWrapper.style.transform = `translateY(${this.distance}px)`;
        }
      }
    }, 10);

    // 触顶时才触发刷新回调,避免touchmove滚动过程中显示相关提示、接口请求
    if(this.scrollTop <= 0) {
      if(this.distance > this.rule) {
        this.refresh && this.refresh(this.pullDownCallback);
      }
    }
    // 触底时才触发加载回调,避免touchmove滚动过程中显示相关提示、接口请求
    if(this.scrollTop + this.clientHeight + this.rule >= this.scrollHeight) {
      if(this.distance < -this.rule) {
        this.loading && this.loading(this.pullUpCallback);
      }
    }
  }
  // UI处理
  beforeAction ( distance) {
    // 在列表触顶时,才显示【刷新中】的文案
    if(this.scrollTop - this.rule <= 0) {
      if (distance > this.rule) {
          var el = document.getElementById('refresh-text')
          el.innerHTML = '

刷新中...

'
el.style.display = 'block' } else { document.getElementById('refresh-text').style.display = 'none' } } // 在列表触底(距离底部this.rule高度)时,才显示【加载中】的文案 if(this.scrollTop + this.clientHeight + this.rule >= this.scrollHeight) { if (distance < -this.rule) { document.getElementById('load-text').style.display = 'block' } else { document.getElementById('load-text').style.display = 'none' } } }

类PullData的完整代码:

在 utils.js 中将 PullData 暴露出去

// 上拉加载,wrapper里必须只有一个子元素,值举例:"#xxid"
export class PullData {
  constructor(options) {
    this.data = options;
    if(!options.dom) {
      // 如果没有传入dom,默认是body
      this.domWrapper = document.body;
    } else {
      // 传入的dom必须是 Nodelist 节点,或者是属性选择器字符串
      this.domWrapper = typeof options.dom === 'string' ? document.querySelector(options.dom) : options.dom;
    }
    this.rule = options.rule || 50; // 定义触发加载/刷新事件的拉伸长度
    this.startY = 0; // 记录鼠标/手指点击的位置
    this.moveY = 0; // 记录鼠标/手指移动的位置
    this.distance = 0; // 记录鼠标/手指移动的距离
    this.clientHeight = 0; // 滚动的可视区
    this.scrollTop = 0; // 滚动的距离
    this.scrollHeight = 0; // 滚动的总高度
    // 上拉加载事件回调函数
    this.pullUpCallback = options.pullUpCallback.bind(this) || function() {};
    // 下拉刷新事件回调函数
    this.pullDownCallback = options.pullDownCallback.bind(this) || function() {};
    
    // 将this.scroll 的this指向 PullData 这个类(原来指向nodelist节点)
    this.scroll = this.scroll.bind(this); 
    // 将this.start 的this指向 PullData 这个类(原来指向nodelist节点)
    this.start = this.start.bind(this); 
    // 将this.move 的this指向 PullData 这个类(原来指向nodelist节点)
    this.move = this.move.bind(this); 
    // 将this.end 的this指向 PullData 这个类(原来指向nodelist节点)
    this.end = this.end.bind(this); 

    this.timer = null; // 上拉/下拉样式回弹的定时器

    this.init(); // 初始化PullData的数据和节点
  }
  // 初始化
  init() {
    this.renderTextDom();
    this.scrollTop = this.domWrapper.scrollTop;
    this.scrollHeight = this.domWrapper.scrollHeight;
    this.clientHeight = this.domWrapper.clientHeight;
    // 滚动事件
    this.domWrapper.addEventListener('scroll', this.scroll, false);

    // 触摸开始事件
    this.domWrapper.addEventListener('touchstart', this.start, false);

    // 触摸移动时间
    this.domWrapper.addEventListener('touchmove', this.move, false);

    // 触摸结束事件
    this.domWrapper.addEventListener("touchend", this.end , false)
  }
  // 创建渲染【刷新】【加载】文案所在的节点
  renderTextDom() {
    // 刷新提示语的节点
    let refreshTextDom = document.createElement('div');
    refreshTextDom.id = "refresh-text";
    refreshTextDom.innerHTML = "刷新中...";
    refreshTextDom.style = "display:none;position:fixed;top:0.2rem;left: 50%;transform: translateX(-50%);";
    // 加载提示语的节点
    let loadTextDom = document.createElement('div');
    loadTextDom.id = "load-text";
    loadTextDom.innerHTML = "加载中...";
    loadTextDom.style = "display:none;position:fixed;bottom:0.2rem;left: 50%;transform: translateX(-50%);";

    document.body.appendChild(refreshTextDom);
    document.body.appendChild(loadTextDom);
  }
  // 滚动事件回调
  scroll(e) {
    this.scrollTop = e.target.scrollTop;
    this.scrollHeight = e.target.scrollHeight;
  }
  // 触摸开始事件回调
  start (e) {
    this.startY = e.touches[0].screenY - this.distance;
  }

  // 触摸移动事件回调
  move (e) {
    e.stopPropagation();
    this.moveY = e.touches[0].screenY;
    this.distance = Math.floor(this.moveY - this.startY) / 5;
    this.beforeAction(this.distance);
    // 只有在触顶或者触底时,才操作domWrapper的样式
    if(this.scrollTop <= 0 || this.scrollTop + this.clientHeight + this.rule >= this.scrollHeight) {
      this.domWrapper.style.transform = `translateY(${this.distance}px)`
    }
  }

  // 触摸结束事件回调
  end(e) {
    if(this.timer) clearInterval(this.timer);
    this.timer = setInterval(() => {
      if(Math.floor(this.distance) < 0 || Math.ceil(this.distance) < 10) {
        // 移动距离恢复到 10 以内时,将domWrapper的translateY设置为0,清除定时器
        this.distance = 0;
        this.domWrapper.style.transform = `translateY(0px)`;
        clearInterval(this.timer);
      } else {
        // 每10ms 减去 this.distance/10的距离
        this.distance -= this.distance / 10;
        // 只有在触顶或者触底时,才操作domWrapper的样式
        if(this.scrollTop <= 0 || this.scrollTop + this.clientHeight + this.rule >= this.scrollHeight) {
          this.domWrapper.style.transform = `translateY(${this.distance}px)`;
        }
      }
    }, 10);

    // 触顶时才触发刷新回调,避免touchmove滚动过程中显示相关提示、接口请求
    if(this.scrollTop <= 0) {
      if(this.distance > this.rule) {
        this.refresh && this.refresh(this.pullDownCallback);
      }
    }
    // 触底时才触发加载回调,避免touchmove滚动过程中显示相关提示、接口请求
    if(this.scrollTop + this.clientHeight + this.rule >= this.scrollHeight) {
      if(this.distance < -this.rule) {
        this.loading && this.loading(this.pullUpCallback);
      }
    }
  }
  // 刷新逻辑在此处理
  refresh (callback) {
    callback && callback();
    var el = document.getElementById('refresh-text')
    el.innerHTML = '

刷新成功!

'
setTimeout(() => {el.style.display = 'none'}, 300) } // 加载逻辑在此处理 loading (callback) { callback && callback(); setTimeout(() => { document.getElementById('load-text').style.display = 'none' },300) } // UI处理 beforeAction ( distance) { // 在列表触顶时,才显示【刷新中】的文案 if(this.scrollTop - this.rule <= 0) { if (distance > this.rule) { var el = document.getElementById('refresh-text') el.innerHTML = '

刷新中...

'
el.style.display = 'block' } else { document.getElementById('refresh-text').style.display = 'none' } } // 在列表触底(距离底部this.rule高度)时,才显示【加载中】的文案 if(this.scrollTop + this.clientHeight + this.rule >= this.scrollHeight) { if (distance < -this.rule) { document.getElementById('load-text').style.display = 'block' } else { document.getElementById('load-text').style.display = 'none' } } } // 销毁监听函数 destroy() { this.domWrapper.removeEventListener('scroll', this.scroll); this.domWrapper.removeEventListener('touchmove', this.move); this.domWrapper.removeEventListener('touchstart', this.start); this.domWrapper.removeEventListener('touchend', this.end); } }

使用PullData

在 list.vue 文件中引入 PullData。

<template>
  <div class="order-view">
    <loading v-if="!loaded">loading>
    <div class="tabs-panel" v-else>
      
      <ul class="order-list" v-if="orderList.length > 0">
        <li class="list-item" v-for="item in orderList" :key="item.orderid">
          
          <OrderCard :info="item">OrderCard>
        li>
      ul>
      
      <div class="no-list" v-else>
        
        <p class="no-list-text">暂无订单p>
      div>
    div>
  div>
template>
import OrderCard from './components/orders/order-card.vue';
import Loading from 'components/loading';
import { PullData } from 'assets/js/utils.js'
export default {
	components: {
		OrderCard,
		Loading,
	},
	data() {
	  return {
	    orderListMap: new Map(), // 订单列表索引,避免加载时重复添加订单
	    orderList: [], // 订单列表
	    page:1,
	    pagesize: 20,
	    total: 0, // 订单总数
	    pullDataClass: null, // 加载刷新类
	    loaded: false, // 是否加载完毕
	  };
	},
	methods: {
		 // 获取订单列表
		getOrderList(callback) {
		   let params = {
		     page: this.page,
		     limit: this.pagesize,
		   }
		   this.isTokenExpired(() => {
		     this.$http.get(
		       "https://wataru.xxx.com/orders"
		       {
		         params,
		       }
		     ).then(res => {
		       this.total = res && res.data && res.data.total || 0;
		       if(res && res.data && res.data.data) {
		         // 将订单数据存入 Map 中
		         if(Array.isArray(res.data.data)) {
		           res.data.data.forEach(item => {
		           	 // Map数据类型,如果是相同的key值,就会覆盖原来的值
		             this.orderListMap.set(item.orderid, item)
		           });
		           // 清空orderlist再push,避免重复,直接清空会导致下拉刷新时先显示【无数据】再显示列表。所以用新变量存储数据,最后一步到位赋值给this.orderlist
		           let orderList = [];
		           for (const mapitem of this.orderListMap) {
		             orderList.push(mapitem[1])
		           }
		           this.orderList = orderList;
		         }
		         if(callback) {
		           this.$nextTick(() => {
		             callback(res.data);
		           });
		         }
		       }
		     });
		   });
		 },
		 // 上拉加载,加载更多数据或显示没有更多数据
		 pullUpLoadHandler() {
		   if(this.page * this.pagesize < this.total) {
		     this.page++;
		   } else {
		     let loadTextDom = document.getElementById('load-text');
		     if(loadTextDom) {
		       loadTextDom.innerHTML = "没有更多数据"
		     }
		   }
		   this.getOrderList();
		 },
		 // 下拉刷新,请求第一页的数据,如果之前上拉加载的文案是“没有更多数据”,在这里要重置回“加载中”
		 pullDownRefreshHandler() {
		   this.page = 1;
		   this.getOrderList();
		   // 清空数组后,要把上拉加载的文案也重置为“加载中”
		   let loadTextDom = document.getElementById('refresh-text');
		   if(loadTextDom) {
		     loadTextDom.innerHTML = "加载中..."
		   }
		 },
	},
	created() {
		// 获取订单数据
		this.getOrderlist(() => {
			this.loaded = true;
	        if(!this.pullDataClass) {
	          this.$nextTick(() => {
	            // 实例化上拉加载/下拉刷新的类
	            this.pullDataClass = new PullData({
	              dom: '.tabs-panel',
	              pullUpCallback: this.pullUpLoadHandler, // 上拉加载
	              pullDownCallback: this.pullDownRefreshHandler // 下拉刷新
	            });
	          })
	        };
		})
	}
}

你可能感兴趣的:(javascript,开发语言,ecmascript)