H5 端分页组件 Yscroll

本文主要介绍一下在 h5 端如何实现列表的分页加载, 更简单的说就是实现,页面顶部下拉: 刷新当前列表; 页面底部上拉: 加载更多。
主要使用的技术栈: Touch event; scrollTop; scrollHeight
外部依赖: http请求使用的是api reqwest,
yscroll

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、初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 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 list
list = []
} else { //根据结果渲染列表
// render list
//...
}
})

4、Yscroll 的 reset 方法, 主要用于 url 或 请求参数改变的场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//首先new 一个 Yscroll 对象 如上例
yscroll.reset(
'/api/list',
{keywords: key}, //请求参数, 无参数传空对象
function(err, resp) {
if(err) { //报错
console.log('err', err)
return
}
if(resp == 'refresh') {
//code clear list
list = []
} else { //根据结果渲染列表
// render list
...
}
}
)

Yscroll 原理

大致流程:
1、初始化: 在列表的父类下插入一个子元素, 作为列表元素的父元素, 如下图所示(id=”scroll-wrapper”)
scroll-wrapper
并为该元素注册 touch 事件 ( touchstart, touchmove, touchend, touchcancel )
2、页面滑动时:

  • 当页面滑动到顶部时, 如果继续往上拉页面, 刷新当前列表
  • 当页面滑动到底部时, 如果继续下拉页面, 请求下一页列表

Yscroll 源码

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
/**
* author: feifeiyu
* verion: 2.1
* @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/front-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 = this
self.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 !== undefined
self.cb = cb //回调
//是否实例化后就加载数据
var loadImmediate = opt.loadImmediate !== undefined ? opt.loadImmediate : true

if(opt.param) {
self.next = self.assembleQuery(self.next, opt.param)
}

//生成所需要dom
self.genDom(opt.target)
//注册事件
self.touchEvt()
//加载数据
loadImmediate && self.fetchData()
}

YS.prototype.genDom = function(target) {
var self = this
//Yscroll Wrap
self.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.parentNode
tarParent.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 = this
console.log('next', self.next)
if(self.next === 'refresh') {
self.loadEnable = true
self.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.next
self.loadMoreNode.style.display = 'none'
self.loadMoreNode.innerHTML = '上拉加载更多'
} else {
self.loadEnable = false
self.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 = this
var startY = 0 //滑动起点
var offsetY = 0 //拉动距离

//touch事件处理
var touch = function(e) {
// e.preventDefault() //加了 scroll 事件没了
var evt = e || window.event
var offsetTop = self.ysWrap.scrollHeight - self.height //相对与顶部最大偏移距离
var scrollTop = self.ysWrap.scrollTop
var touch = e.touches[0]

switch(evt.type) {
case 'touchstart':
startY = touch.pageY
break
case 'touchmove':
if(touch.pageY < 200) { //avoid scroll leakage
evt.preventDefault
} else {
offsetY = touch.pageY - startY
if(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 > 0
evt.preventDefault()
self.ysWrap.style.top = offsetY + 'px'
self.loadMoreNode.style.display = 'block'
self.refreshNode.style.display = 'none'
}
}
break
case '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.next
self.next = 'refresh'
self.fetchData() //请求清空
self.next = urlTmp.replace(/&?page=\d+/, '')
self.fetchData() //重新加载数据
}
}
}
offsetY = 0
break
case '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.next
self.next = 'refresh'
self.fetchData() //请求清空
self.next = urlTmp.replace(/&?page=\d+/, '')
self.fetchData() //重新加载数据
}
}
}
offsetY = 0
break
default:
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 = this
if(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.loadEnable = true
//请求数据
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

END