目录
背景说明
关于此文的动机,参见上一篇关于Modal控件的博文
这节课的练习内容是:实现一个图片轮播控件,要求封装良好&扩展性强。 控件只封装上面的图片滚动条部分,不包含控制条; 暴露以下接口:
- 指定父容器
- 图片数组
- 是否支持拖拽
所谓“Carousel”就是“旋转木马”;在Web前端领域就表示“轮播头图”。也叫“Slider”;下文将随机混用这两种称谓,不加区分。
实现静态结构
Talk is cheap. Show me the code.
— Torvalds, Linus (2000-08-25)
直接上代码
HTML
m-slider
就是Carousel控件了;整个控件用JS调用,可以指定其寄居的父容器- 三个
slide
分别是 前一张/当前/下一张 图片
<div class="m-slider">
<div class="slide"></div>
<div class="slide"></div>
<div class="slide"></div>
</div>
上述HTML代码是在控件内部定义的,不在全局HTML中直接写; 而是用JS调用出来的。
CSS
/*全局的样式*/
html,
body {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
text-align: center;
}
div#carousel-container {
width: 600px;
height: 400px;
padding: 0;
margin: 10px auto;
outline: 3px solid red; /*这个边框只是为了方便观察HTML结构用的*/
}
/*关于轮播头图的样式*/
.m-slider,
.m-slider .slide {
width: 100%;
height: 100%;
}
.m-slider {
position: relative;
}
.m-slider .slide {
position: absolute;
top: 0;
left: 0;
text-align: center;
vertical-align: middle;
}
.m-slider .slide img {
/*这里先用绝对布局使左上角居中,然后再用transform的方式;比较优雅*/
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
max-width: 90%;
border: 2px solid #fff;
border-radius: 2px;
box-shadow: 0px 0px 2px rgba(0, 0, 0, 0.5);
}
/*关于控制条的样式*/
.m-cursor {
display: inline-block;
margin: 3em auto 0;
padding-left: 0;
/*z-index: 10;*/
}
.m-cursor li {
display: inline-block;
width: 20px;
height: 20px;
line-height: 20px;
margin-right: 4px;
list-style: none;
color: #aaa;
transition: background-color 0.5s;
border: 2px solid #999;
border-radius: 50%;
cursor: pointer;
}
.m-cursor li span {
/*大于号小于号的位置偏低,修复一下*/
position: relative;
top: -0.1em;
}
.m-cursor li:hover,
.m-cursor li.z-active {
color: #fff;
background-color: #999;
}
定义接口
封装完好的Carouosel控件,应该是这样用的:
- 基于类的接口
- 可以传入控件的容器和图片列表&是否允许拖拽 作为构造参数
- 使用事件发射器,方便扩展
- 支持事件传参
- 支持
nav()
,prev()
和next()
方法
var slider = new Slider({
container: document.getElementById("carousel-container"), //容器
// 图片列表
images: [
"./imgs/pic01.jpg",
"./imgs/pic02.jpg",
"./imgs/pic03.jpg",
"./imgs/pic04.jpg",
"./imgs/pic05.jpg",
"./imgs/pic06.jpg"
],
drag: true // 是否允许拖拽
});
cursors.forEach(function(cursor, index){
cursor.addEventListener('click', function(){
slider.nav(index);
})
})
prev.addEventListener('click', function(){
slider.prev()
})
next.addEventListener('click', function(){
slider.next()
})
// 通过监听`nav`事件来完成额外逻辑
slider.on('nav', function( ev ){
var pageIndex = ev.pageIndex;
cursors.forEach(function(cursor, index){
if(index === pageIndex ){
cursor.className = 'z-active';
}else{
cursor.className = '';
}
})
})
// 3s 自动轮播
setInterval(function(){
slider.next();
},3000)
// 直接跳到第二页
slider.nav(2)
实现接口
后面在Carousel
的实现中会用到一些工具函数,简要梳理一下:
函数原型 | 功能 |
---|---|
html2node(str) |
根据str生成DOM元素并返回 |
extent(o1,o2) |
实现“混入”(Mixin)模式;把o2的属性混入到o1中,已有的属性不覆盖 |
emitter |
这个其实不是函数,是一个发射器对象;提供最简单的事件绑定/解绑/触发功能 |
上述辅助函数的原理详见上一篇文章,在此不再赘述;本次使用的方法略有差异,仅就其结构说明如下:
首先是在util.js
中封装了一个util
对象,把辅助函数都放进去
var util = (function() {
return {
html2node: function(str) {
//...
},
extend: function(o1, o2) {
//...
},
addClass: function(node, className) {
//...
},
delClass: function(node, className) {
//...
},
emitter: {
on: function(event, fn) {
//...
},
// 解绑事件
off: function(event, fn) {
//...
},
// 触发事件
emit: function(event) {
//...
}
}
}
})()
然后是在slider.js
中调用它。
这里的技巧是:将util
作为参数传入,形参名叫_
,好写。
(function(_) {
var template =
'<div class="m-slider" >\
<div class="slide"></div>\
<div class="slide"></div>\
<div class="slide"></div>\
</div>'
//定义Slider
function Slider(opt) {
_.extend(this, opt);
}
//扩展Slider.prototype
_.extend(Slider.prototype, _.emitter);
_.extend(Slider.prototype, {
_layout: _.html2node(template),
nav: function(pageIndex) {
//...
},
// 下一页
next: //...
// 上一页
prev: //...
// 单步移动
//...
})
window.Slider = Slider;
})(util);
Slider
OK,重点来了;下面是Slider
的实现
// <!-- slider.js -->
(function(_) {
function Slider(opt) {
_.extend(this, opt);
// 容器节点 以及 样式设置
this.container = this.container || document.body;
this.container.style.overflow = 'hidden';
// 常用的组件节点,缓存起来
this.slider = this._layout.cloneNode(true);
this.slides = [].slice.call(this.slider.querySelectorAll('.slide'));
// 拖拽相关
this.offsetWidth = this.container.offsetWidth;
this.breakPoint = this.offsetWidth / 4;
this.pageNum = this.images.length;
// 内部数据结构
this.slideIndex = 1;
this.pageIndex = this.pageIndex || 0;
this.offsetAll = this.pageIndex;
// 初始化动作
this.container.appendChild(this.slider);
if (this.drag) this._initDrag();
}
_.extend(Slider.prototype, _.emitter);
var template =
'<div class="m-slider" >\
<div class="slide"></div>\
<div class="slide"></div>\
<div class="slide"></div>\
</div>';
_.extend(Slider.prototype, {
_layout: _.html2node(template),
next: function() {
this._step(1);
},
prev: function() {
this._step(-1);
},
// 直接跳转到指定页
nav: function(pageIndex) {
this.pageIndex = pageIndex
this.slideIndex = typeof this.slideIndex === 'number' ? this.slideIndex : (pageIndex + 1) % 3;
this.offsetAll = pageIndex;
// 直接跳转的时候,不支持动画
this.slider.style.transitionDuration = '0s';
this._calcSlide();
},
// 单步移动
_step: function(offset) {
this.offsetAll += offset;
this.pageIndex += offset;
this.slideIndex += offset;
this.slider.style.transitionDuration = '.5s';
this._calcSlide();
},
// 计算Slide
// 每个slide的left = (offsetAll + offset(1, -1)) * 100%;
// 外层容器 (.m-slider) 的偏移 = offsetAll * 宽度
_calcSlide: function() {
var slideIndex = this.slideIndex = this._normIndex(this.slideIndex, 3);
var pageIndex = this.pageIndex = this._normIndex(this.pageIndex, this.pageNum);
var offsetAll = this.offsetAll;
var pageNum = this.pageNum;
var prevSlideIndex = this._normIndex(slideIndex - 1, 3);
var nextSlideIndex = this._normIndex(slideIndex + 1, 3);
// 三个slide的偏移
var slides = this.slides;
slides[slideIndex].style.left = (offsetAll) * 100 + '%'
slides[prevSlideIndex].style.left = (offsetAll - 1) * 100 + '%'
slides[nextSlideIndex].style.left = (offsetAll + 1) * 100 + '%'
// 容器偏移
this.slider.style.transform = 'translateX(' + (-offsetAll * 100) + '%) translateZ(0)'
// 当前slide 添加 'z-active'的className
slides.forEach(function(node) {
_.delClass(node, 'z-active')
})
_.addClass(slides[slideIndex], 'z-active');
this._onNav(this.pageIndex, this.slideIndex);
},
// 标准化下标
_normIndex: function(index, len) {
return (len + index) % len
},
// 跳转时完成的逻辑, 这里是设置图片的url
_onNav: function(pageIndex, slideIndex) {
var slides = this.slides;
for (var i = -1; i <= 1; i++) {
var index = (slideIndex + i + 3) % 3;
var img = slides[index].querySelector('img')
if (!img) {
img = document.createElement('img');
slides[index].appendChild(img);
}
img.src = './imgs/pic0' + (this._normIndex(pageIndex + i, this.pageNum) + 1) + '.jpg';
}
//向外部调用者发射事件
this.emit('nav', {
pageIndex: pageIndex,
slideIndex: slideIndex
})
},
//拖动相关
_initDrag: function() {
this._dragInfo = {};
this.slider.addEventListener('mousedown', this._dragstart.bind(this));
this.slider.addEventListener('mousemove', this._dragmove.bind(this));
this.slider.addEventListener('mouseup', this._dragend.bind(this));
this.slider.addEventListener('mouseleave', this._dragend.bind(this));
},
_dragstart: function(ev) {
var dragInfo = this._dragInfo;
dragInfo.start = {
x: ev.pageX,
y: ev.pageY
};
},
_dragmove: function(ev) {
var dragInfo = this._dragInfo;
if (!dragInfo.start) return;
ev.preventDefault(); //手动拖拽时,图片的移动使用JS控制,不要有延迟
this.slider.style.transitionDuration = '0s';
var start = dragInfo.start;
// 清除恼人的选区
if (window.getSelection) {
window.getSelection().removeAllRanges();
} else if (window.document.selection) {
window.document.selection.empty();
}
// 加translateZ 分量是为了触发硬件加速
this.slider.style.transform =
'translateX(' + (-(this.offsetWidth * this.offsetAll - ev.pageX + start.x)) + 'px) translateZ(0)'
},
_dragend: function(ev) {
var dragInfo = this._dragInfo;
if (!dragInfo.start) return;
ev.preventDefault();
var start = dragInfo.start;
this._dragInfo = {};
var pageX = ev.pageX;
// 看走了多少距离
var deltX = pageX - start.x;
if (Math.abs(deltX) > this.breakPoint) {
this._step(deltX > 0 ? -1 : 1)
} else {
this._step(0)
}
}
})
window.Slider = Slider;
})(util);
全部拼起来,走两步~
调用关系大概是这样子的:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<style>
/*全局的样式*/
/*关于轮播头图的样式*/
/*关于控制条的样式*/
</style>
<script src="util.js" />
<script src="slider.js"/>
</head>
<body>
<div id="carousel-container">
<!--
<div class="m-slider" style="transition-duration: 0.5s; transform: translateX(-600%) translateZ(0px);">
<div class="slide z-active" style="left: 600%;"><img src="./imgs/pic01.jpg"></div>
<div class="slide" style="left: 700%;"><img src="./imgs/pic02.jpg"></div>
<div class="slide" style="left: 500%;"><img src="./imgs/pic06.jpg"></div>
</div>
-->
</div>
<ul class="m-cursor">
<li class='prev'><span><</span></li>
<li class='cursor'>1</li>
<li class='cursor'>2</li>
<li class='cursor z-active'>3</li>
<li class='cursor'>4</li>
<li class='cursor'>5</li>
<li class='cursor'>6</li>
<li class='next'><span>></span></li>
</ul>
<!-- 主页面逻辑,使用Slider -->
<script>
var $ = function(selector) {
return [].slice.call(document.querySelectorAll(selector))
}
var cursors = $('.m-cursor .cursor');
var prev = $('.m-cursor .prev')[0];
var next = $('.m-cursor .next')[0];
cursors.forEach(function(cursor, index) {
cursor.addEventListener('click', function() {
slider.nav(index);
})
})
prev.addEventListener('click', function() {
slider.prev()
})
next.addEventListener('click', function() {
slider.next()
})
var slider = new Slider({
container: $('#carousel-container')[0], //视口容器
images: [ // 图片列表
"./imgs/pic01.jpg",
"./imgs/pic02.jpg",
"./imgs/pic03.jpg",
"./imgs/pic04.jpg",
"./imgs/pic05.jpg",
"./imgs/pic06.jpg"
],
drag: true // 是否允许拖拽
});
// 通过监听`nav`事件来完成额外逻辑
slider.on('nav', function(ev) {
var pageIndex = ev.pageIndex;
cursors.forEach(function(cursor, index) {
if (index === pageIndex) {
cursor.className = 'z-active';
} else {
cursor.className = '';
}
})
})
// 3s 自动轮播
setInterval(function() {
slider.next();
}, 3000)
// 直接跳到第二页
slider.nav(2)
</script>
</body>
</html>
That’s it, DONE!