豆瓣电影:SPA
一、项目介绍
使用原生JS和Jquery做了一个移动端豆瓣电影单页面应用,通过点击切换Top250、北美排行榜、电影搜索三个面板。
Top250面板实现了触底懒加载数据,使用Ajax新增列表项目,数据加载中有Loading动画提示,所有数据加载完后显示底部盒子,点击可回到顶部。电影搜索面板通过<input>标签接收用户搜索内容,通过click()、keyup()方法实现单击搜索按钮或敲打enter键触发事件函数,使用Ajax发送请求处理数据。
使用html模板利用jQuery(html)创建DOM元素,提取公共方法提高了代码简洁性和可复用性。
二、所涉知识
(一)自适应网页设计
在网页代码的头部,加入一行viewport元标签。
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1">
参考:自适应网页设计
(二)JavaScript
(1)join()
join()方法以指定参数作为分隔符,将所有数组成员连接为一个字符串返回。如果不提供参数,默认用逗号分隔。
$node.find('.introduction .geners').text(movie.genres.join('、'));
(2)map()
map方法将数组的所有成员依次传入参数函数,然后把每一次的执行结果组成一个新数组返回。
$node.find('.introduction .directors').text(movie.directors.map(v => v.name).join('、'));
(3)forEach()
forEach方法与map方法很相似,也是对数组的所有成员依次执行参数函数。但是,forEach方法不返回值,只用来操作数据。
data.subjects.forEach((movie) => {
movie = movie.subject ? movie.subject : movie;
this.$ul.append(utils.createElementItem(movie));
});
(4)setTimeout()
setTimeout函数用来指定某个函数或某段代码,在多少毫秒之后执行。它返回一个整数,表示定时器的编号,以后可以用来取消这个定时器。
_this.clock = setTimeout(function(){
if (_this.$container.height() - 400 <= _this.$top250.height() + _this.$top250.scrollTop()) {
_this.renderData();
}
},100)
(5)clearTimeout()
setTimeou返回一个整数值,表示计数器编号。将该整数传入clearTimeout函数,就可以取消对应的定时器。
if (_this.clock) clearTimeout(_this.clock);
(三)jQuery
1.jQuery.ajax( [settings ] )
执行一个异步的HTTP(Ajax)的请求。
常用参数:
url (默认: 当前页面地址
),类型: String,发送请求的地址。
type (默认: 'GET'
),类型: String,请求方式 ("POST" 或 "GET")。注意:其它 HTTP 请求方法,如 PUT 和 DELETE 也可以使用,但仅部分浏览器支持。
data,类型: [Object, String],发送到服务器的数据,对象必须为"{键:值}"格式。
dataType,类型: String,预期服务器返回的数据类型。
jqXHR 对象:
$.ajax() 返回XMLHttpRequest(jqXHR)对象,该对象是浏览器的原生的XMLHttpRequest对象的一个超集。
从 jQuery 1.5 开始,$.ajax()返回的jqXHR对象 实现了 Promise 接口, 使它拥有了 Promise 的所有属性,方法和行为。
this在所有的回调中的引用,是这个对象在传递给$.ajax的设置中上下文;如果没有指定context(上下文),this 引用的是Ajax设置的本身。
- jqXHR.done(function(data, textStatus, jqXHR) {}):
一个可供选择的 success 回调选项的构造函数,.done()方法取代了的过时的jqXHR.success()方法。 - jqXHR.fail(function(jqXHR, textStatus, errorThrown) {}):
一种可供选择的 error 回调选项的构造函数,.fail()
方法取代了的过时的.error()
方法。 - jqXHR.always(function(data|jqXHR, textStatus, jqXHR|errorThrown) { }):
一种可供选择的 complete 回调选项的构造函数,.always()方法取代了的过时的.complete()方法。在响应一个成功的请求后,该函数的参数和.done()的参数是相同的:data, textStatus, 和 jqXHR 对象;对于失败的请求,参数和.fail()的参数是相同的:jqXHR 对象, textStatus, 和 errorThrown。 - jqXHR.then(function(data, textStatus, jqXHR) {}, function(jqXHR, textStatus, errorThrown) {}):
包含了 .done() 和 .fail()方法的功能,(从 jQuery 1.8 开始)允许底层被操纵。
$.ajax({
url: 'https://api.douban.com/v2/movie/top250',
data: {
start: _this.index,
count: _this.count,
},
dataType: 'jsonp',
}).done((ret) => {
this.index += this.count;
if (this.index > ret.total ) this.isOver = true;
this.setData(ret);
}).fail(function(){
alert('获取数据失败');
}).always(() => {
this.isLoading = false;
this.$iconLoading.css('display','none');
});
2. jQuery()
CSS选择器作为参数
jQuery()本质上是一个构造函数,主要作用是返回jQuery对象的实例,简写为$()。比如,var listItems = $("li");
表面上是选中li元素,实际上是返回对应于li元素的jQuery实例。因为只有这样,才能在DOM对象上使用jQuery提供的各种方法。
$node.find('a').attr('href',movie.alt);
HTML字符串作为参数
如果直接在jQuery构造函数中输入HTML字符串,返回的也是jQuery实例。它与从CSS选择器生成的jQuery实例完全一样,唯一的区别就是,它对应的DOM结构不属于当前文档。
var tpl =
`<div class="iconfont icon-loading"></div>
<div class="bottom">
<a class="returnTop" href="#">回到顶部</a>
</div> `
this.$container.append($(tpl));
3.jQuery构造函数返回的结果集
jQuery的核心思想是“先选中某些网页元素,然后对其进行某种处理”(find something, do something),也就是说,先选择后处理,这是jQuery的基本操作模式。所以,绝大多数jQuery操作都是从选择器开始的,返回一个选中的结果集。
(1)index()
index() 方法返回指定元素相对于其他指定元素的 index 位置。这些元素可通过 jQuery 选择器或 DOM 元素来指定。
获得第一个匹配元素相对于其同胞元素的 index 位置:$(selector).index()
获得元素相对于选择器的 index 位置:$(selector).index(element)
_this.$panels.eq($(this).index()).show().siblings().hide();
(2)eq()
如果想要在结果集取出一个jQuery对象的实例,不需要取出DOM对象,则使用eq方法,它的参数是实例在结果集中的位置(从0开始)。
_this.$panels.eq($(this).index()).show().siblings().hide();
4. 结果集的过滤方法
选择器选出一组符合条件的网页元素以后,jQuery提供了许多方法,可以过滤结果集,返回更准确的目标。
(1)siblings(),nextAll(),prevAll()
siblings方法返回当前元素的所有同级元素,nextAll方法返回当前元素其后的所有同级元素,prevAll方法返回当前元素前面的所有同级元素。
$(this).addClass('active').siblings().removeClass('active');
(2)closest(),find()
closest方法返回当前元素,以及当前元素的所有上级元素之中,第一个符合条件的元素。find方法返回当前元素的所有符合条件的下级元素。
$node.find('a').attr('href',movie.alt);
5. DOM相关方法
许多方法可以对DOM元素进行处理。
(1)html()和text()
html()返回该元素包含的HTML代码,text()返回该元素包含的文本。jQuery的许多方法都是取值器(getter)与赋值器(setter)的合一,即取值和赋值都是同一个方法,不使用参数的时候为取值器,使用参数的时候为赋值器。
html()和text()如果没有参数,就会当作取值器使用,取回结果集的第一个元素所包含的内容。如果对这两个方法提供参数,就是当作赋值器使用,修改结果集所有成员的内容,并返回原来的结果集,以便进行链式操作。
$node.find('.introduction .title').text(movie.title);
(2)addClass(),removeClass(),toggleClass()
addClass()用于添加一个类,removeClass()用于移除一个类,toggleClass()用于切换一个类(如果无就添加,如果有就移除)。
$(this).addClass('active').siblings().removeClass('active');
(3)css()
css()用于获取、改变css的值。css()的参数是css属性名,CSS属性名的CSS写法和DOM写法都可以接受,比如font-size和fontSize都行。
$panel.find('.bottom').css('display','block');
(4)val()
val()返回结果集第一个元素的值,或者设置当前结果集所有元素的值。
_this.keyword = _this.$input.val();
(5)attr()和prop()
首先,这里要区分两种属性。一种是网页元素的属性,比如a元素的href属性、img元素的src属性,这要使用attr()读写。另一种是DOM元素的属性,比如tagName、nodeName、nodeType等等,这要使用prop()读写。
<input type="checkbox" checked="checked" />
对于checked属性,attr()读到的是checked,prop()读到的是true。可以看到,attr()读取的是网页上该属性的值,而prop()读取的是DOM元素的该属性的值,根据规范,element.checked应该返回一个布尔值。所以,判断单选框是否选中,要使用prop()
$node.find('a').attr('href',movie.alt);
$node.find('.cover img').attr('src',movie.images.small);
(6)removeAttr()和removeProp()
removeAttr()移除某个HTML属性,removeProp()移除某个DOM属性。
(7)empty()
empty() 从被选元素移除所有内容,包括所有文本和子节点。
_this.$ul.empty();
(8)data()
data()用于在一个DOM对象上储存数据。
6.添加、复制和移动网页元素的方法
jQuery方法提供一系列方法,可以改变元素在文档中的位置。
(1)append方法,appendTo方法
append方法将参数中的元素插入当前元素的尾部,appendTo方法将当前元素插入参数中的元素尾部。
this.$container.append($(tpl));
(2)prepend方法,prependTo方法
prepend方法将参数中的元素,变为当前元素的第一个子元素。如果prepend方法的参数不是新生成的元素,而是当前页面已存在的元素,则会产生移动元素的效果。
prependTo方法将当前元素变为参数中的元素的第一个子元素。
(3)after方法,insertAfter方法
after方法将参数中的元素插在当前元素后面,insertAfter方法将当前元素插在参数中的元素后面。
(4)before方法,insertBefore方法
before方法将参数中的元素插在当前元素的前面,insertBefore方法将当前元素插在参数中的元素的前面。
(5)wrap方法,wrapAll方法,unwrap方法,wrapInner方法
wrap方法将参数中的元素变成当前元素的父元素,wrap方法的参数还可以是一个函数。wrapAll方法为结果集的所有元素,添加一个共同的父元素。unwrap方法移除当前元素的父元素。wrapInner方法为当前元素的所有子元素,添加一个父元素。
(6)clone方法
clone方法克隆当前元素。对于那些有id属性的节点,clone方法会连id属性一起克隆。所以,要把克隆的节点插入文档的时候,务必要修改或移除id属性。
(7)remove方法,detach方法,replaceWith方法
remove方法移除并返回一个元素,取消该元素上所有事件的绑定。detach方法也是移除并返回一个元素,但是不取消该元素上所有事件的绑定。replaceWith方法用参数中的元素,替换并返回当前元素,取消当前元素的所有事件的绑定。
7. 动画效果方法
jQuery提供一些方法,可以很容易地显示网页动画效果。但是,总体上来说,它们不如CSS动画强大和节省资源,所以应该优先考虑使用CSS动画。
如果将jQuery.fx.off设为true,就可以将所有动画效果关闭,使得网页元素的各种变化一步到位,没有中间过渡的动画效果。
动画效果的简便方法
jQuery提供以下一些动画效果方法。
show:显示当前元素。
hide:隐藏当前元素。
toggle:显示或隐藏当前元素。
_this.$panels.eq($(this).index()).show().siblings().hide();
7. 事件处理
(1)事件绑定的简便方法
jQuery提供一系列方法,允许直接为常见事件绑定回调函数:
click,keypress,keyup,keyup,mouseover,mouseout,mouseenter,mouseleave,hover,scroll,focus,blur,resize
如果不带参数调用这些方法,就是触发相应的事件,从而引发回调函数的运行。需要注意的是,通过这种方法触发回调函数,将不会引发浏览器对该事件的默认行为。比如,对a元素调用click方法,将只触发事先绑定的回调函数,而不会导致浏览器将页面导向href属性指定的网址。
hover方法需要特别说明,它接受两个回调函数作为参数,分别代表mouseenter和mouseleave事件的回调函数。
注意,在回调函数内部,this关键字指的是发生该事件的DOM对象。为了使用jQuery提供的方法,必须将DOM对象转为jQuery对象,因此写成$(this)。
$panel.find('.returnTop').click(function(){
$panel.scrollTop(0);
});
this.$input.keyup(function(e){
if(e.keyCode === 13) {
_this.$button.click();
};
});
this.$top250.scroll(function(){
if (_this.clock) clearTimeout(_this.clock);
_this.clock = setTimeout(function(){
if (_this.$container.height() - 400 <= _this.$top250.height() + _this.$top250.scrollTop()) {
_this.renderData();
}
},100)
});
(2)on方法,trigger方法,off方法
除了简便方法,jQuery还提供事件处理的通用方法。
on方法是jQuery事件绑定的统一接口。
事件绑定的那些简便方法,其实都是on方法的简写形式。on方法接受两个参数,第一个是事件名称,第二个是回调函数。
on方法允许一次为多个事件指定同样的回调函数。
$('input[type="text"]').on('focus blur', function (){
console.log('focus or blur');
});
on方法还可以为当前元素的某一个子元素,添加回调函数。
以下代码为ul的子元素li绑定click事件的回调函数。采用这种写法时,on方法接受三个参数,子元素选择器作为第二个参数,夹在事件名称和回调函数之间。
这种写法有两个好处。首先,click事件还是在ul元素上触发回调函数,但是会检查event对象的target属性是否为li子元素,如果为true,再调用回调函数。这样就比为li元素一一绑定回调函数,节省了内存空间。其次,这种绑定的回调函数,对于在绑定后生成的li元素依然有效。
$('ul').on('click', 'li', function (e){
console.log(this);
});
on方法还允许向回调函数传入数据。
以下代码在发生click事件之后,会通过event对象的data属性,在控制台打印出所传入的数据(即“张三”)。
$("ul" ).on("click", {name: "张三"}, function (event){
console.log(event.data.name);
});
trigger方法
trigger方法用于触发回调函数,它的参数就是事件的名称。以下代码触发li元素的click事件回调函数。与那些简便方法一样,trigger方法只触发回调函数,而不会引发浏览器的默认行为。
$('li').trigger('click')
off方法
off方法用于移除事件的回调函数,以下代码移除li元素所有的click事件回调函数。
$('li').off('click')
(3)event对象
当回调函数被触发后,它们的参数通常是一个事件对象event。以下代码的回调函数的参数e,就代表事件对象event。
$(document).on('click', function (e){
// ...
});
event对象有以下属性:
type:事件类型,比如click。
which:触发该事件的鼠标按钮或键盘的键。
target:事件发生的初始对象。
data:传入事件对象的数据。
pageX:事件发生时,鼠标位置的水平坐标(相对于页面左上角)。
pageY:事件发生时,鼠标位置的垂直坐标(相对于页面左上角)。
event对象有以下方法:
preventDefault:取消浏览器默认行为。
stopPropagation:阻止事件向上层元素传播。
(四)ES6
箭头函数(arrow)
(1)定义:
ES6允许使用“箭头”(=>)定义函数。如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回。由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号。
(2)实例:回调函数的简化
箭头函数的一个用处是简化回调函数。
(3)注意点:
- 函数体内的this对象,绑定定义时所在的对象,而不是使用时所在的对象。
- 不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
- 不可以使用arguments对象,该对象在函数体内不存在。
data.subjects.forEach((movie) => {
movie = movie.subject ? movie.subject : movie;
this.$ul.append(utils.createElementItem(movie));
});
(五)CSS3
CSS Animation
(1)基本用法
首先,CSS Animation需要指定动画一个周期持续的时间,以及动画效果的名称。
div:hover {
animation: 1s rainbow;
}
上面代码表示,当鼠标悬停在div元素上时,会产生名为rainbow的动画效果,持续时间为1秒。为此,我们还需要用keyframes关键字,定义rainbow效果。
@keyframes rainbow {
0% { background: #c00; }
50% { background: orange; }
100% { background: yellowgreen; }
}
(2)animation的各项属性
animation 属性是一个简写属性,用于设置六个动画属性:
animation: name duration timing-function delay iteration-count direction;
属性 | 描述 |
---|---|
animation-name | 规定需要绑定到选择器的 keyframe 名称。 |
animation-duration | 规定完成动画所花费的时间,以秒或毫秒计。 |
animation-timing-function | 规定动画的速度曲线。 |
animation-delay | 规定在动画开始之前的延迟。 |
animation-iteration-count | 规定动画应该播放的次数。 |
animation-direction | 规定是否应该轮流反向播放动画。 |
注释:请始终规定 animation-duration 属性,否则时长为 0,就不会播放动画了。
main section .container .icon-loading:before {
display: inline-block;
font-size: 34px;
animation: rotate 1s linear infinite;
}
@keyframes rotate {
0% {transform: rotate(0deg);}
100% {transform: rotate(360deg);}
}
三、重要方法
(一)公用方法
//准备提示节点
preparePrompt: function() {
var tpl =
`<div class="iconfont icon-loading"></div>
<div class="bottom">
<a class="returnTop" href="#">回到顶部</a>
</div> `
this.$container.append($(tpl));
},
//创建并设置电影节点
createElementItem: function(movie){
var tpl =
`<li class="item">
<a href="">
<div class="cover">
<img src="">
</div>
<div class="introduction">
<h3 class="title"></h3>
<p><span class="score"> </span> 分 / <span class="collect"></span> 收藏</p>
<p><span class="year"></span> / <span class="geners"></span></p>
<p>导演:<span class="directors"></span></p>
<p>主演:<span class="casts"></span></p>
</div>
</a>
</li>`
var $node = $(tpl);
$node.find('a').attr('href',movie.alt);
$node.find('.cover img').attr('src',movie.images.small);
$node.find('.introduction .title').text(movie.title);
$node.find('.introduction .score').text(movie.rating.average);
$node.find('.introduction .collect').text(movie.collect_count);
$node.find('.introduction .year').text(movie.year);
$node.find('.introduction .geners').text(movie.genres.join('、'));
$node.find('.introduction .directors').text(movie.directors.map(v => v.name).join('、'));
$node.find('.introduction .casts').text(movie.casts.map(v => v.name).join('、'));
return $node;
},
//设置数据
setData: function(data){
data.subjects.forEach((movie) => {
movie = movie.subject ? movie.subject : movie;
this.$ul.append(utils.createElementItem(movie));
});
},
//展示底部
showBottom: function($panel){
$panel.find('.bottom').css('display','block');
},
//回到顶部
returnTop: function($panel) {
$panel.find('.returnTop').click(function(){
$panel.scrollTop(0);
});
},
(二)TOP250方法
//处理触底加载
this.$top250.scroll(function(){
if (_this.clock) clearTimeout(_this.clock);
_this.clock = setTimeout(function(){
if (_this.$container.height() - 400 <= _this.$top250.height() + _this.$top250.scrollTop()) {
_this.renderData();
}
},100)
});
//获取数据
getData: function(){
var _this = this;
this.$iconLoading = this.$top250.find('.icon-loading');
this.isLoading = true;
this.$iconLoading.css('display','block');
//发送请求
$.ajax({
url: 'https://api.douban.com/v2/movie/top250',
data: {
start: _this.index,
count: _this.count,
},
dataType: 'jsonp',
}).done((ret) => {
this.index += this.count;
if (this.index > ret.total ) this.isOver = true;
this.setData(ret);
}).fail(function(){
alert('获取数据失败');
}).always(() => {
this.isLoading = false;
this.$iconLoading.css('display','none');
});
},
(三)搜索方法
//获取数据
getData: function(callback){
if (this.isLoading) return;
var _this = this;
this.$iconLoading = this.$search.find('.icon-loading');
this.$iconLoading.css('display','block');
this.isLoading = true;
$.ajax({
url: 'https://api.douban.com/v2/movie/search',
data: {
q: _this.keyword
},
dataType: 'jsonp',
}).done((ret) => {
this.setData(ret);
callback(this.$search);
}).fail(function(){
alert('获取数据失败');
}).always(() => {
this.isLoading = false;
this.$iconLoading.css('display','none');
})
},
(四)面板方法
//处理tab切换
bind: function(){
let _this = this;
this.$tabs.click(function() {
$(this).addClass('active').siblings().removeClass('active');
_this.$panels.eq($(this).index()).show().siblings().hide();
});
},