为什么要写这个组件。因为网上已有的滚动加载组件不能满足我的要求。
我的需求:页面前端使用的是bootstrap+knockout 开发。前端采用的是MVVM绑定自服务器中获取的数据,原来是采用分页实现的,因为要开发手机可以访问的网页,所以修改为滚动加载形式,一旦下拉到页面底部,就用jquery的$.post 加载下一页。
首先想到的就是在github上获取别人已经造好的轮子。我先选取的是https://github.com/fa-ge/Scrollload 这个轮子,按照demo操作测试,鼠标滚动到底部没有什么动静,折腾了两个多小时,还没成功,放弃了。然后又选了一个轮子https://github.com/coffeedeveloper/loadmaster ,操作测试仍然不能成功。按照人家自己的demo,不修改任何代码是可以运行成功的,但是若与我的结合,则不能成功。我分析是因为我的需求特殊,我的是MVVM形式的绑定,不需要append html代码。这里无意贬低这两个滚动加载组件。因为我选取的都是用的人多的。只能说不适合我的项目,也或者我还不太会用他们写的轮子解决我的问题。
由于项目进度的问题,我刚开始是直接在页面中用jquery来实现的,并没有开发组件。只是由于用的地方比较多,项目中有四五个页面在用。而近期项目没什么活了。所以萌发了自己开发一个组件的想法。在开发之前当然是恶补组件开发的相关知识。这些包括了javascript闭包,作用域,原型,继承及面向对象开发的知识。
第一版代码是这个样子的scrollload.js:
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
|
//滚动加载闭包写法
var
ScrollLoad = (
function
() {
var
_range = 80;
//距下边界长度/单位px
var
_loaded =
false
;
//是否所有数据已加载完
var
_pageIndex = 1;
var
_loading =
false
;
//是否正在加载
var
_totalheight = 0;
function
_Scroll() {
//console.info("a1");
if
(_loaded)
return
;
//加载完毕,不在执行scroll函数
//console.info("scroll1");
var
srollPos = $(window).scrollTop();
//滚动条距顶部距离(页面超出窗口的高度)
_totalheight = parseFloat($(window).height()) + parseFloat(srollPos);
//console.info("a2");
if
(($(document).height() - _range) <= _totalheight ) {
if
(_loading)
return
;
_loading =
true
;
setTimeout(
function
() {
scrollLoad(_pageIndex);
//此为外部函数调用
//_loading = false;
}, 1000);
}
//console.info("a3");
};
return
function
() {
//console.info(this);
this
.Range = _range;
this
.Loaded = _loaded;
this
.PageIndex = _pageIndex;
//设置距下边界长度/单位px
this
.SetRange =
function
(range) {
_range = range;
};
//设置滚动加载页码
this
.SetPageIndex =
function
(num) {
_pageIndex = num;
};
//设置滚动加载是否已经加载全部数据
this
.SetLoaded =
function
(loaded) {
_loaded = loaded;
};
//设置_loading为false
this
.SetLoading =
function
() {
_loading =
false
;
};
this
.Scroll =
function
() {
_Scroll();
};
}
})();
|
第一版主要是解决外部获取闭包中内部变量问题,比如this.Load获取闭包中的是否已加载变量_loaded的值。修改内部变量的值问题:比如this.SetPageIndex修改异步调用的页码。滚动时执行的内部函数_Scroll()的实现。这里使用了一个闭包的外部函数,scrollLoad(pageIndex),为什么要把这个放到闭包内部执行,是因为若放在外部执行,则_Scroll()就要返回true,或false以便在调用这个闭包组件的时候来判断是否可以执行,但在实际测试中,滚动加载页码变动很快,而且有重复加载的情况,并且容易造成所有要加载的数据已经加载完了,而页码还会一直增加的情况。
另一个要解决的问题是采用
return function(){
this.Range = _range;
this.Loaded = _loaded;
this.PageIndex = _pageIndex;
}
这样的形式返回可以在外部调用的方法。这种写法支持像C#调用类实例形式用 new 调用
页面中cshtml的写法:
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
|
<
tbody
class
=
"list"
data-bind
=
"with:CustomerVM"
>
<
tr
>
<
td
>
<
table
class
=
"table table-bordered table-striped"
>
<
tbody
>
<
tr
>
<
td
class
=
"nowrap"
>姓名
|
使用的是knockout的mvvm绑定形式,由于代码过多,这里略写
页面中相应js代码:
page.VM.CustomerVM = ko.mapping.fromJS([]);
$(function () {
scrollLoad(1);
});
function Search(item,e)
{
scrollLoad(1);
}
//滚动加载/
var scroll = new ScrollLoad();
$(window).scroll(function () {
scroll.Scroll();
});
//console.info(CarouselMock);
//滚动加载/
function scrollLoad(pagexx)
{
//console.info("scrollLoad");
$.post("/m/operator/GetMyCustomerList", { pageIndex: pagexx, name: $("#name").val(), level1: $("#level1").val(),level2:$("#level2").val() }, function (data) {
//console.info(data);
if(pagexx==1)
{
ko.mapping.fromJS(data, mappingOption, page.VM.CustomerVM);
//console.info("page1scroll:" + scroll.PageIndex);
}
if(pagexx>=2)
{
if (data ==null||data== "") {
scroll.SetLoaded(true);
return;
}
//main.append(data);
//把viewModel即page.VM.CustomerVM重新转换回data对象
var unmapped = ko.mapping.toJS(page.VM.CustomerVM);
//然后转换回的data对象与自服务器读取的相加
var newData=unmapped.concat(data);
//然后再重新转换为viewmodel对象
ko.mapping.fromJS(newData, mappingOption, page.VM.CustomerVM);
//console.info("newdata:"+pagexx);
console.info(newData);
}
scroll.SetPageIndex(scroll.PageIndex++);
scroll.SetLoading();
//类名加载
//略.....
}
page.VM.CustomerVM = ko.mapping.fromJS([]);
此为customerVM knockout绑定初始化
var
scroll =
new
ScrollLoad();
$(window).scroll(
function
() {
scroll.Scroll();
});
此为自写的滚动加载组件调用代码
先用new 实例化,然后在window 的 scroll事件中直接执行 scroll.Scroll()。 这里需要注意的是,此部分代码无须写在$(function(){}) 中
function
scrollLoad(pagexx)
此为分页异步加载函数,pagexx就是页码。其他的在$.post部分的则为页面中的搜索条件。这个函数在上文已经提到,是我写的ScrollLoad.js中被调用的一个外部函数。
在scrollLoad函数中,有两个地方加了对scrollload组件的调用代码。第一个地方:
if
(data ==
null
||data==
""
) {
scroll.SetLoaded(
true
);
return
;
}
这个是当自服务器获取的data为null,表示已经到了最后一页,这个时候直接设置_loaded为加载完成。这样如果再滚动,就不会再进行加载操作。
第二个地方:
scroll.SetPageIndex(scroll.PageIndex++);
scroll.SetLoading();
当异步加载完成,则设置页码自增1.然后设置_loading变量为初始的false。表示可以继续加载其他页
$(
function
() {
scrollLoad(1);
});
这个代码是在第一次载入页面时加载第一页
function
Search(item,e)
{
scrollLoad(1);
}
而这个代码则是在点击搜索按钮时使用
写完后对整个页面进行测试,滚动加载可以正常使用。初步成果了。但是对我写的第一版是不太满意的。因为在组件中调用了外部函数,这耦合性太大。然后就进行第二版的开发。解决耦合性问题。这个时候首先想到的就是settimeout方法,因为这个方法是可以把一个函数传入settimeout中来执行的。于是在网上扒资料,发现javascript对函数传入参数没有什么限制,既可以传入值,也可以传入对象,方法等。于是我对我写的滚动组件进行改造,使其支持外部函数以参数形式传入。
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
|
//滚动加载闭包写法
var
ScrollLoad = (
function
() {
var
_range = 80;
//距下边界长度/单位px
var
_loaded =
false
;
//是否所有数据已加载完
var
_pageIndex = 1;
var
_loading =
false
;
//是否正在加载
var
_totalheight = 0;
function
_Scroll(callback) {
//console.info("a1");
if
(_loaded)
return
;
//加载完毕,不在执行scroll函数
//console.info("scroll1");
var
srollPos = $(window).scrollTop();
//滚动条距顶部距离(页面超出窗口的高度)
_totalheight = parseFloat($(window).height()) + parseFloat(srollPos);
//console.info("a2");
if
(($(document).height() - _range) <= _totalheight ) {
if
(_loading)
return
;
_loading =
true
;
setTimeout(
function
() {
//console.info(typeof callback);
if
(
typeof
callback !==
'function'
)
{
throw
new
TypeError(
'"callback" argument must be a Function'
);
}
console.info(_pageIndex);
callback(_pageIndex);
//此为外部函数调用
//_loading = false;
}, 1000);
}
//console.info("a3");
};
return
function
() {
//console.info(this);
this
.Range = _range;
this
.Loaded = _loaded;
this
.PageIndex = _pageIndex;
//设置距下边界长度/单位px
this
.SetRange =
function
(range) {
_range = range;
};
//设置滚动加载页码
this
.SetPageIndex =
function
(num) {
_pageIndex = num;
};
//设置滚动加载是否已经加载全部数据
this
.SetLoaded =
function
(loaded) {
_loaded = loaded;
};
//设置_loading为false
this
.SetLoading =
function
() {
_loading =
false
;
};
this
.Scroll =
function
(callback) {
_Scroll(callback);
};
}
})();
|
这里对_Scroll函数增加了一个传入参数callback,并在使用的时候对其类型进行判断。若判断为非函数。则直接抛出错误。若是函数,则执行callback(_pageIndex); 此行代码替换了原来需要调用的外部函数:scrollLoad(pagexx) 。而使外部函数scrollLoad以参数的形式可以传入。这样外部函数就可以不是scrollLoad这个名字,而可以是其他函数名。更改后,组件的调用方法也做了相应的更改。
1
2
3
4
5
|
var
scroll =
new
ScrollLoad();
$(window).scroll(
function
() {
var
callback =
function
(index) { scrollLoad(index); }
scroll.Scroll(callback);
});
|
其余页面调用不变。
这次组件更改后还是觉得不满意,因为
$(window).scroll(
function
() {
var
callback =
function
(index) { scrollLoad(index); }
scroll.Scroll(callback);
});
这种调用导致代码过多。打算把这部分代码移入到组件内部。通过多次尝试,是可行的。更改时这样的:
1
2
3
|
$(window).scroll(
function
() {
_Scroll(callback);
});
|
这样改变后,外部对组件调用的代码更少,可以使用三行代码实现初始调用:
1
2
3
|
var
scroll =
new
ScrollLoad();
var
callback =
function
(index) { scrollLoad(index); }
scroll.Scroll(callback);
|
甚至如果觉得这样也多,也可以使用两行代码实现初始调用:
1
2
|
var
scroll =
new
ScrollLoad();
scroll.Scroll(
function
(index) { scrollLoad(index); });
|
而在随后的使用过程当中又陆续发现了几个问题。
第一个问题:this.Range = _range; 获取内部_range值是不准确的,一旦使用SetRange更改_range值,而随后再使用this.Range获取到的仍然是初始化的80。而不是更改后的值。这个解决的办法是也采用匿名函数方式获取:
1
2
3
|
this
.GetRange =
function
() {
return
_range;
};
|
第二个问题:在家里我维护的另一个网站微信开发使用滚动加载组件中使用微信的web开发工具测试过程当中,发觉竟然滚动到底后由于我的数据库中有几千条数据,居然一页一页不停加载,根本停不下来。我经过查找原因发现,是因为微信滚动加载时,滚动条一直在触发滚动的距离底部80像素内。这个的解决办法是每次加载一页数据后重新设置滚动条距离底部的位置:
1
|
$(window).scrollTop($(window).scrollTop() - _range);
|
第三个问题:发现在第一次加载或刷新时实际都是加载了两页。经过分析是因为在$(funciotn(){})中执行了一次scrollLoad(1);
而组件初始化过程中实际也会加载一页造成。解决办法就是$(funciotn(){})不再执行scrollLoad(1);
第四个问题:在点击搜索按钮时,本来按实际情况来说应该是自动加载搜索后的第一页,可是却发现搜索的页码在原来已经加载的页码情况下还会自动增加。这个问题的解决是增加了一个重设页码方法。而在搜索方法中进行了引用:
1
2
3
4
5
|
function
Search(item,e)
{
scroll.ResetPageIndex();
scrollLoad(1);
}
|
经过对这三个问题的处理。最终我的滚动组件代码修改为了:
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
|
//滚动加载闭包写法
var
ScrollLoad =
function
() {
var
_range = 80;
//距下边界高度/单位px
var
_loaded =
false
;
//是否所有数据已加载完
var
_pageIndex = 1;
var
_loading =
false
;
//是否正在加载
var
_totalheight = 0;
$(window).scroll(
function
() {
_Scroll(callback);
});
function
_Scroll(callback) {
//console.info("a1");
if
(_loaded)
return
;
//加载完毕,不在执行scroll函数
//console.info("scroll1");
var
srollPos = $(window).scrollTop();
//滚动条距顶部距离(页面超出窗口的高度)
_totalheight = parseFloat($(window).height()) + parseFloat(srollPos);
//console.info("a2");
if
(($(document).height() - _range) <= _totalheight ) {
if
(_loading)
return
;
_loading =
true
;
//加载数据过程中调整滚动条到窗口顶部的距离,防止滚动条一直在触发加载区从而导致不停的一页一页加载情况
$(window).scrollTop($(window).scrollTop() - _range);
//console.info("_range:" + _range);
//console.info("a2:"+$(window).scrollTop());
setTimeout(
function
() {
//console.info(typeof callback);
if
(
typeof
callback !==
'function'
)
{
throw
new
TypeError(
'"callback" argument must be a Function'
);
}
//console.info(_pageIndex);
callback(_pageIndex);
//此为外部函数调用
//_loading = false;
}, 1000);
}
//console.info("a3");
};
return
function
() {
//获取距下边界高度
this
.GetRange =
function
() {
return
_range;
};
//设置距下边界长度/单位px
this
.SetRange =
function
(value) {
_range = value;
//console.info("myrange:" + _range);
};
/*获取是否已加载值*/
this
.GetLoaded =
function
() {
return
_loaded;
};
//设置滚动加载是否已经加载全部数据
this
.SetLoaded =
function
(value) {
_loaded = value;
};
/*获取页码*/
this
.GetPageIndex =
function
() {
return
_pageIndex;
};
/*设置页码*/
this
.SetPageIndex =
function
(value) {
_pageIndex = value;
};
/*重置页码为初始值,在输入搜索条件搜索时使用*/
this
.ResetPageIndex =
function
() {
_pageIndex = 1;
};
//设置_loading为false
this
.SetLoading =
function
() {
_loading =
false
;
};
this
.Scroll =
function
(callback) {
_Scroll(callback);
};
}
}();
|
到这里滚动加载组件开发也告一段落。满足了我现在的需求,后续若再有问题,那再进行修改。需要说明此组件依赖于jquery。若要使用,需要添加jquery的引用。我开发使用的jquery版本是1.10