Carousel

大佬太强了,教我撸了个轮播图 #

初衷 #

  1. CSS 基础布局的学习和练习
  2. JavaScript 语法和逻辑的学习和练习
  3. 学会封装组件

项目地址 #

JSRUN , gitee , Github

整体思路 #

使用 绝对定位 方法使 4 个 div 元素叠在一起

这只是 CSS 部分 #

div 部分 #

// parent element
.carousel-wrapper {
    position: relative;
    // 设置宽高
    width: 300px;
    height: 150px;
}
// children element
.carousel-item {
    position: absolute;
    display: none;
    //  parent 宽高保持一致
    width: 100%;
    height: 100%;
}
// 显示当前 div
.carousel-wrapper .carousel-item_current {
    display: block;
}

indicator 部分 #

  • 让 indicator 靠近底部,显示在 div 上面
.indicator-list {
    display: block;
    position: absolute;
    width: 100%;
    height: 10px;
    bottom: 15px;
    z-index: 999;
}

.indicator-item {
    background-color: green;
    width: 10px;
    height: 2.5px;
    display: inline-block;
}

.indicator-item_current {
    background-color: blue;
}
  • 水平居中,去掉换行符导致的间距
.indicator-list {
    text-align: center;
    font-size: 0;
}
  • 增加间距
.indicator-item {
    margin-left: 2px;
    margin-right: 2px;
}

arrow 部分 #

使用行内元素 span

  • 设置水平位置
.arrow {
    background-color: black;
    position: absolute;
    display: inline-block;
    font-size: 0;
    width: 25px;
    height: 25px;
}

.arrow-left {
    left: 15px;
}

.arrow-right {
    right: 15px;
}
  • 垂直居中
.arrow {
    margin: auto;
    top: 0;
    bottom: 0;
}
  • 形状改为圆形,设置不透明度
.arrow {
    border-radius: 50%;
    background-color: rgba(0, 0, 0, 0.2);
}
  • 改变 :hover 不透明度
.arrow:hover {
    background-color: rgba(0, 0, 0, 0.4);
}
  • 添加 cursor
.indicator-item {
    cursor: pointer;
}

.arrow {
    cursor: pointer;
}

右箭头的 onclick 事件绑定 #

选择元素 #

var elArrowRight = document.getElementsByClassName('arrow-right')[0];
var elListCarouselItem = document.getElementsByClassName('carousel-item');

事件绑定是发生在 element 身上的, document.getElementsByClassName() 返回值是 (array-like object) 类数组对象 HTMLCollection ,类名为 arrow-right 的元素是当前要绑定的元素,对象后面跟 [0] 即可。

console.log() 开始 #

var currentIndex = 0; // 注意变量作用域

elArrowRight.onclick = function handleClick () {
    var oldIndex = currentIndex;

    if (currentIndex >= 3) { // 点击最后一个 div 要切回第一个 div
      currentIndex = 0;
    } else {
      currentIndex = currentIndex + 1;
    }

    console.log('现在是第 ' + oldIndex + ' 图显示,应该改成第 ' + currentIndex + ' 图显示');
}

效果实现 #

div 的隐藏和显示已经用 css 实现了,逻辑实现 console.log() 描述的也很清楚,接下来接下来要做的就是具体效果,改变 elListCarouselItemcarousel-item_current 位置,即点击右箭头之后,该 class 在当前 <div> 中删除,添加至下一个 <div> 中,实现方法用 classList 即可:

elListCarouselItem[oldIndex].classList.remove('carousel-item_current');
elListCarouselItem[currentIndex].classList.add('carousel-item_current');

左箭头的实现方法同理,不再赘述。

优化 #

getElementsByClassName() vs querySelector() #

遇事不决,stackoverflow

论坛给出的回答是:querySelector 适用范围更广,比如选择 <li> 元素.

So,优化后的代码如下:

var elArrowRight = document.querySelector('.arrow-right');
var elListCarouselItem = document.querySelectorAll('.carousel-item');

handleClick() #

这里加上函数名多此一举,直接用匿名函数

elArrowRight.onclick = function () {...};

指示器的事件委托 #

现在要实现的功能是,点击指示器,切换至对应 div ,可以使用循环给 indicator-item 绑定事件,我太懒,直接用了事件委托的方法,参考这篇文档

用事件委托的方法,绑定的元素就该是 indicator-list ,这就是其优于第一种方法的地方。

var elIndicatorList = document.querySelector('.indicator-list');
var elIndicatorItem = document.querySelectorAll('.indicator-item');

同样,先做逻辑,再实现功能

console.log() 对应 indicator-item 数字 #

要获取对应数字,须在 .html 自定义 data-* 属性, 即:

<span data-index="0" class="indicator-item indicator-item_current"></span>
<span data-index="1" class="indicator-item"></span>
<span data-index="2" class="indicator-item"></span>
<span data-index="3" class="indicator-item"></span>

然后在 .js 文件中通过 dataset 获取属性。这里,我们需要的数字 index ,而不是字符串,需要将字符串转化为数字,有 parseInt()Number() 可供选择,查了一下 ,选了 parseInt()

elIndicatorList.onclick = function(event) {
    var target = event.target;
    var targetIndex = parseInt(target.dataset.index, 10);

    console.log(targetIndex);
}

这样,就可以获得所点击 indicator-item 对应的数字了。现在有一个 corner caseindicator-list 是一个和父元素等宽的元素,如果点击 indicator-item 之外的部分会有 bug,处理方式是事件委托仅对包含 indicator-item 类的元素起作用,点击其他位置即返回:

elIndicatorList.onclick = function(event) {
    var target = event.target;

    if (!target.classList.contains('indicator-item') {
      return;
    }

    var targetIndex = parseInt(target.dataset.index, 10);

    console.log(targetIndex);
}

功能实现 #

实现方式和左右箭头的一样,关键是怎么知道上个状态的 index ,当前 indextargetIndex ,这里先用 currentIndex ,函数执行之后,将 targetIndex 赋给 currentIndex 作为上个状态时的 oldIndex ,看能不能跑起来:

elIndicatorList.onclick = function(event) {
    var target = event.target;

    if (!target.classList.contains('indicator-item')) {
      return;
    }

    var targetIndex = parseInt(target.dataset.index, 10);
    var oldIndex = currentIndex; // 前面已经定义 currentIndex 初始为 0

    elListCarouselItem[oldIndex].classList.remove('carousel-item_current');
    elListCarouselItem[targetIndex].classList.add('carousel-item_current');
    elIndicatorItem[oldIndex].classList.remove('indicator-item_current');
    elIndicatorItem[targetIndex].classList.add('indicator-item_current');

    currentIndex = targetIndex;
};

整体优化 #

箭头和指示器同步 #

点击箭头或者指示器的时候, indicator-item 并不会发生变化,只需加如下两行代码即可:

elIndicatorItem[oldIndex].classList.remove('indicator-item_current');
elIndicatorItme[currentIndex].classList.add('indicator-item_current');

魔法数字问题 #

在箭头 onclick 事件绑定中,使用了数字 3 来表示最后一个 div 的索引,这个数字叫做魔法数字,可用 LIST_LENGTH - 1 表示:

const LIST_LENGTH = elCarouselItem.length;

消除重复代码( 高优先级 ) #

当前三个点击事件的代码如下:

elArrowRight.onclick = function() {
    var oldIndex = currentIndex;

    if (currentIndex >= LIST_LENGTH) { // currentIndex >= 4 即为 0
      currentIndex = 0;
    } else {
      currentIndex = currentIndex + 1;
    }

    elListCarouselItem[oldIndex].classList.remove('carousel-item_current');
    elListCarouselItem[currentIndex].classList.add('carousel-item_current');
    elIndicatorItem[oldIndex].classList.remove('indicator-item_current');
    elIndicatorItem[currentIndex].classList.add('indicator-item_current');
};

elArrowLeft.onclick = function() {
    var oldIndex = currentIndex;

    if (currentIndex <= 0) {
      currentIndex = LIST_LENGTH - 1;
    } else {
      currentIndex = currentIndex - 1;
    }

    elListCarouselItem[oldIndex].classList.remove('carousel-item_current');
    elListCarouselItem[currentIndex].classList.add('carousel-item_current');
    elIndicatorItem[oldIndex].classList.remove('indicator-item_current');
    elIndicatorItem[currentIndex].classList.add('indicator-item_current');
};

elIndicatorList.onclick = function(event) {
    var target = event.target;

    if (!target.classList.contains('indicator-item')) {
      return;
    }

    var targetIndex = parseInt(target.dataset.index, 10);
    var oldIndex = currentIndex;

    elListCarouselItem[oldIndex].classList.remove('carousel-item_current');
    elListCarouselItem[targetIndex].classList.add('carousel-item_current');
    elIndicatorItem[oldIndex].classList.remove('indicator-item_current');
    elIndicatorItem[targetIndex].classList.add('indicator-item_current');

    currentIndex = targetIndex;
};

观察发现,有大量很多可复用代码,现在要把这些代码抽取出来

changeCurrentClassName(oldIndex, currentIndex) #

function changeCurrentClassName(oldIndex, currentIndex) {
    elListCarouselItem[oldIndex].classList.remove('carousel-item_current');
    elListCarouselItem[currentIndex].classList.add('carousel-item_current');
    elIndicatorItem[oldIndex].classList.remove('indicator-item_current');
    elIndicatorItem[currentIndex].classList.add('indicator-item_current');
}

现在整体代码变成这样了:

elArrowRight.onclick = function() {
    var oldIndex = currentIndex;

    if (currentIndex >= LIST_LENGTH) {
      currentIndex = 0;
    } else {
      currentIndex = currentIndex + 1;
    }

    changeCurrentClassName(oldIndex, currentIndex);
};
elArrowLeft.onclick = function() {
    var oldIndex = currentIndex;

    if (currentIndex <= 0) {
      currentIndex = LIST_LENGTH - 1;
    } else {
      currentIndex = currentIndex - 1;
    }

    changeCurrentClassName(oldIndex, currentIndex);
};
elIndicatorList.onclick = function(event) {
    var target = event.target;

    if (!target.classList.contains('indicator-item')) {
      return;
    }

    var targetIndex = parseInt(target.dataset.index, 10);
    var oldIndex = currentIndex;

    changeCurrentClassName(oldIndex, targetIndex);

    currentIndex = targetIndex;
};

这里的 index 也是可以抽取的,下一步就是处理 index

changeCurrentIndex(targetIndex) #

function changeCurrentIndex(targetIndex) {
    if (targetIndex >= LIST_LENGTH) {
      currentIndex = 0;
    } else if (targetIndex < 0) {
      currentIndex = LIST_LENGTH - 1;
    } else {
      currentIndex = targetIndex;
    }
}

前面获取上个状态的索引是通过 oldIndex ,其实用 querySelector() 更简单:

elArrowRight.onclick = function() {
    var targetIndex = currentIndex + 1;

    changeCurrentIndex(targetIndex);
    changeCurrentClassName();
};

elArrowLeft.onclick = function() {
    var targetIndex = currentIndex - 1;

    changeCurrentIndex(targetIndex);
    changeCurrentClassName();
};

elIndicatorList.onclick = function(event) {
    var targetIndex = parseInt(target.dataset.index, 10);

    changeCurrentIndex(targetIndex);
    changeCurrentClassName();
};

function changeCurrentClassName() {
    document.querySelector('.carousel-item_current').classList.remove('carousel-item_current');
    elListCarouselItem[currentIndex].classList.add('carousel-item_current');
    document.querySelector('.indicator-item_current').classList.remove('indicator-item_current');
    elIndicatorItem[currentIndex].classList.add('indicator-item_current');

handleIndexChange(targetIndex) #

function handleIndexChange(targetIndex) {
    changeCurrentIndex(targetIndex);
    changeCurrentClassName();
}

处理一下 corner case :如果当前索引和目标索引相等,即返回

function handleIndexChange(targetIndex) {
    if (targetIndex === currentIndex) { // 注意类型比较
      return;
    }

    changeCurrentIndex(targetIndex);
    changeCurrentClassName();
}

最终代码如下:

elArrowRight.onclick = function() {
    var targetIndex = currentIndex + 1;
    handleIndexChange(targetIndex);
};

elArrowLeft.onclick = function() {
    var targetIndex = currentIndex - 1;
    handleIndexChange(targetIndex);
};

elIndicatorList.onclick = functin(event) {
    var target = event.target;
    if (!target.classList.contains('indicator-item')) {
      return;
    }

    var targetIndex = parseInt(target.dataset.index, 10);
    handleIndexChange(targetIndex);
};

添加图片 #

前面的操作都是基于不同背景色的 <div> ,现在可以将其替换为真实的图片,使用了图床外链,设置一下样式:

.carousel-item-img {
    width: 100%;
    height: 100%;
}

优化 indicator-item 效果 #

::after::css3 为区分伪类和伪元素的写法,不兼容 IE8

.indicator-item::after {
    content: '';
    position: absolute;
    top: -3px;
    bottom: -3px;
    left: 0;
    right: 0;
}

indicator-item 添加 transform 效果,优化视觉体验:

.indicator-item:hover {
    transform: scale(1.3);
}

.indicator-item_current {
    background-color: blue;
    transform: scale(1.3);
}

代码规范 #

  1. width 在前, height 在后
  2. CSS bem 命名规范,eg: .carousel-item_current , .indicator-item_current
  3. JavaScript 的 camelCase 命名法
  4. 提炼函数,消除重复代码
  5. 去除魔法数字
Fork me on GitHub