本文主要介绍一下在 h5 端如何实现列表的分页加载, 更简单的说就是实现,页面顶部下拉: 刷新当前列表; 页面底部上拉: 加载更多。
主要使用的技术栈: Touch event; scrollTop; scrollHeight
外部依赖: http请求使用的是api reqwest,
Yscroll 使用
1、在使用页面先引入 reqwest, 然后引入 yscroll.js
2、配置参数
- target: 列表元素的父元素 id, 类型string, 必填
- refresh: 是否允许刷新功能: true 可以刷新, false 禁止刷新。 类型 boolean, 选填, default: true
- url: 请求列表的地址。 选填
- headers: http请求头部的字段
- param: 请求参数, 选填, 类型 对象
- height: 整个列表可视区域的高度(单位px)。类型 number, default: 屏幕高度,即 window.screen.height
- loadImmedate: 是否在初始化的时候就请求数据, 如果为false则在上拉后请求数据。类型 boolean, 选填, default: true。
- preload: 是否使能预加载,即浏览器拉到底部一定位置时自动请求下一页。类型 boolean, 选填, default: true
- cb: 回调函数, 选填, 类型 function
3、reset 方法
- url: 请求地址, 选填
- param: 请求参数, 选填, 类型 对象
- cb: 回调函数, 选填, 类型 function
缺点: 加载提示图标未作优化
3、初始化:
12345678910111213141516 var options = {} //配置参数, 所有参数组成一个对象options.target = 'list-wrap'options.url = '/api/list/'var yscroll = new Yscroll(options, function(err, resp){ //回调函数if(err) { //报错console.log('err', err)return}if(resp == 'refresh') {//code clear listlist = []} else { //根据结果渲染列表// render list//...}})4、Yscroll 的 reset 方法, 主要用于 url 或 请求参数改变的场景
123456789101112131415161718 //首先new 一个 Yscroll 对象 如上例yscroll.reset('/api/list',{keywords: key}, //请求参数, 无参数传空对象function(err, resp) {if(err) { //报错console.log('err', err)return}if(resp == 'refresh') {//code clear listlist = []} else { //根据结果渲染列表// render list...}})
Yscroll 原理
大致流程:
1、初始化: 在列表的父类下插入一个子元素, 作为列表元素的父元素, 如下图所示(id=”scroll-wrapper”)
并为该元素注册 touch 事件 ( touchstart, touchmove, touchend, touchcancel )
2、页面滑动时:
- 当页面滑动到顶部时, 如果继续往上拉页面, 刷新当前列表
- 当页面滑动到底部时, 如果继续下拉页面, 请求下一页列表
Yscroll 源码
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321 /*** author: feifeiyu* verion: 2.0* @param target // list container DOM id, not null* @param refresh // enable or disable page refresh, type: boolean, default: true* @param url // request api address, type: url string,* @param headers //http request headers, type: object* @param param //http request query(parameters), type: object* @param height //list container height, type: number, default: window.screen.height* @param loadImmedate // whether send request on page init or not, type: boolean, default: true* @param preload // enable or disable page list preload, default: true× @param cb // callback type: function* parameters for reset function* @param url // reset request api address, type: url string* @param param // reset request query, type: object* @param cb // callback type: function* Yscroll is a page split component for h5* Yscroll blog: https://feifeiyum.github.io/2016/09/26/yscroll-for-h5/*///添加依赖(配合 export 使用)//const reqwest = require('reqwest')var Yscroll = (function() {var YS = function(opt, cb) {if(!opt.target) {throw '[error]: Yscroll - target id can\'t be null'}if(typeof opt.target !== 'string') {throw '[error]: Yscroll - type of target id is string'}if(opt.url !== undefined && typeof opt.url !== 'string') {throw '[error]: Yscroll - type of request url is string '}if(opt.headers !== undefined && typeof opt.headers !== 'object') {throw '[error]: Yscroll - type of headers (http headers) is object '}if(opt.refresh !== undefined && typeof opt.refresh !== 'boolean') {throw '[error]: Yscroll - type of refresh is boolean'}if(opt.loadImmediate !== undefined && typeof opt.loadImmediate !== 'boolean') {throw '[error]: Yscroll - type of loadImmediate is boolean'}if(opt.preload !== undefined && typeof opt.preload !== 'boolean') {throw '[error]: Yscroll - type of preload is boolean'}if(opt.height !== undefined && typeof opt.height !== 'number') {throw '[error]: Yscroll - type of height is number'}if(opt.param !== undefined && typeof opt.param != 'object') {throw '[error]: Yscroll - type of param is object'}if(!cb) {throw '[error]: Yscroll - callback function cant not be null'}if(typeof cb !== 'function') {throw '[error]: Yscroll - callback function type is not function'}var self = thisself.refresh = opt.refresh !== undefined ? opt.refresh : true //是否使能下拉刷新opt.headers && (self.headers = opt.headers) //http 请求头部self.height = opt.height || window.screen.height //列表高度opt.url && (self.next = opt.url) //请求地址self.preload = opt.preload !== undefined ? opt.preload : true //是否使能预加载self.loadEnable = opt.url !== undefinedself.cb = cb //回调//是否实例化后就加载数据var loadImmediate = opt.loadImmediate !== undefined ? opt.loadImmediate : trueif(opt.param) {self.next = self.assembleQuery(self.next, opt.param)}//生成所需要domself.genDom(opt.target)//注册事件self.touchEvt()//加载数据loadImmediate && self.fetchData()}YS.prototype.genDom = function(target) {var self = this//Yscroll Wrapself.ysWrap = document.createElement('div')self.ysWrap.id = 'yscroll-wrapper'self.ysWrap.style.cssText = 'position:absolute;'+ 'width:100%;'+ 'overflow-x:hidden;'+ 'overflow-y:scroll;'+ '-webkit-overflow-scrolling:touch;'+ 'height:' + self.height + 'px;'//加载更多图标self.loadMoreNode = document.createElement('p')self.loadMoreNode.id = 'yscroll-more'self.loadMoreNode.style.cssText = 'display:none;'+ 'margin:0;'+ 'bottom:-28px;'+ 'text-align:center;'+ 'height:28px;'+ 'line-height:28px;'+ 'font-size:12px;'+ 'color:#323232;'self.loadMoreNode.innerHTML = '上拉加载更多'//下拉刷新图标self.refreshNode = document.createElement('p')self.refreshNode.id = 'yscroll-more'self.refreshNode.style.cssText = 'display:none;'+ 'margin:0;'+ 'text-align:center;'+ 'height:28px;'+ 'line-height:28px;'+ 'font-size:12px;'+ 'color:#323232;'self.refreshNode.innerHTML = '下拉刷新'var targetNode = document.getElementById(target)var tarParent = targetNode.parentNodetarParent.style.position = 'relative'if(!targetNode) {throw '[error]: Yscroll - can not find list target DOM'}self.ysWrap.appendChild(self.refreshNode)self.ysWrap.appendChild(targetNode)self.ysWrap.appendChild(self.loadMoreNode)tarParent.appendChild(self.ysWrap)}YS.prototype.fetchData = function() {var self = thisconsole.log('next', self.next)if(self.next === 'refresh') {self.loadEnable = trueself.cb(null, 'refresh')} else {self.loadMoreNode.innerHTML = '加载中 . . .'self.refreshNode.innerHTML = '加载中 . . .'reqwest({url: self.next,method: 'GET',headers: self.headers,crossOrigin: true}).then(function(resp) {if(resp.next) {self.next = resp.nextself.loadMoreNode.style.display = 'none'self.loadMoreNode.innerHTML = '上拉加载更多'} else {self.loadEnable = falseself.loadMoreNode.style.display = 'block'self.loadMoreNode.innerHTML = '没有更多了'}self.refreshNode.style.display = 'none'self.refreshNode.innerHTML = '下拉刷新'self.cb(null, resp)})}}YS.prototype.touchEvt = function() {var self = thisvar startY = 0 //滑动起点var offsetY = 0 //拉动距离//touch事件处理var touch = function(e) {// e.preventDefault() //加了 scroll 事件没了var evt = e || window.eventvar offsetTop = self.ysWrap.scrollHeight - self.height //相对与顶部最大偏移距离var scrollTop = self.ysWrap.scrollTopvar touch = e.touches[0]switch(evt.type) {case 'touchstart':startY = touch.pageYbreakcase 'touchmove':if(touch.pageY < 200) { //avoid scroll leakageevt.preventDefault} else {offsetY = touch.pageY - startYif(self.refresh && scrollTop === 0 && offsetY > 0) {//在顶部,下拉evt.preventDefault()self.ysWrap.style.top = offsetY + 'px'self.refreshNode.style.display = 'block'self.loadMoreNode.style.display = 'none'} else if((offsetTop - scrollTop) < 3 && offsetTop > 0 && offsetY < 0) {//在底部, 上拉//页面存在折叠 即 offsetTop > 0evt.preventDefault()self.ysWrap.style.top = offsetY + 'px'self.loadMoreNode.style.display = 'block'self.refreshNode.style.display = 'none'}}breakcase 'touchend':self.ysWrap.style.top = '0px'if(!self.loadEnable && offsetY < 0) {//不加载更多,下拉时,把页面拉到底部if((offsetTop - scrollTop) < 100) {document.body.scrollTop = 10000 //把body移到底部offsetTop && (self.ysWrap.scrollTop = self.ysWrap.scrollHeight)}} else if(self.loadEnable && offsetY < 0) {//下滑if((offsetTop - scrollTop) < 100) {document.body.scrollTop = 10000 //把body移到底部}if(self.preload && (offsetTop - scrollTop) / self.height < 0.3) {//提前加载, 在页面滚到到底部之前加载self.fetchData() //请求接口} else if((offsetTop - scrollTop) < 3 && offsetY < -100) {//下拉加载 在页面滚到到底部加载self.fetchData() //请求接口}} else if(self.refresh && offsetY > 0) {//上滑document.body.scrollTop = 0 //把body移到顶部if(scrollTop == 0) {if (offsetY > 120) {let urlTmp = self.nextself.next = 'refresh'self.fetchData() //请求清空self.next = urlTmp.replace(/&?page=\d+/, '')self.fetchData() //重新加载数据}}}breakcase 'touchcancel':self.ysWrap.style.top = '0px'if(!self.loadEnable && offsetY < 0) {//不加载更多,下拉时,把页面拉到底部if((offsetTop - scrollTop) < 100) {document.body.scrollTop = 10000 //把body移到底部offsetTop && (self.ysWrap.scrollTop = self.ysWrap.scrollHeight)}} else if(self.loadEnable && offsetY < 0) {//下滑if((offsetTop - scrollTop) < 100) {document.body.scrollTop = 10000 //把body移到底部}if(self.preload && (offsetTop - scrollTop) / self.height < 0.3) {//提前加载, 在页面滚到到底部之前加载self.fetchData() //请求接口} else if((offsetTop - scrollTop) < 3 && offsetY < -100) {//下拉加载 在页面滚到到底部加载self.fetchData() //请求接口}} else if(self.refresh && offsetY > 0) {//上滑document.body.scrollTop = 0 //把body移到顶部if(scrollTop == 0) {if (offsetY > 120) {let urlTmp = self.nextself.next = 'refresh'self.fetchData() //请求清空self.next = urlTmp.replace(/&?page=\d+/, '')self.fetchData() //重新加载数据}}}breakdefault:break}}self.ysWrap.addEventListener('touchstart', touch, false)self.ysWrap.addEventListener('touchmove', touch, false)self.ysWrap.addEventListener('touchend', touch, false)self.ysWrap.addEventListener('touchcancel', touch, false)}YS.prototype.reset = function(url, param, cb) {console.log('in reset')var self = thisif(url !== undefined && typeof url !== 'string') {throw '[error] Yscroll - type of url in Yscroll.reset is string'}if(param !== undefined && typeof param !== 'object') {throw '[error] Yscroll - type of param in Yscroll.reset is object'}url && (self.next = url)if(param) {self.next = self.assembleQuery(self.next, param)}cb && (self.cb = cb)//请求数据self.fetchData()}//组装参数YS.prototype.assembleQuery = function(url, param) {var query = ''for(var key in param) {query += '&' + key + '=' + param[key]}if(/\?\w+/.test(url)) {url += query} else {url += '?' + query.slice(1)}return url}return YS}())//采用模块化引入时使用该句//module.exports = Yscroll