div显示图片_【前端冷知识】如何封装一个图片轮播组件

组件封装是一个前端工程师进阶的必经之路。组件封装是指Web页面上抽出来一个个包含模版(HTML)、功能(Javascript)和样式(CSS)的单元。所以,今天的内容,我们将带你了解组件封装的开发思路,让你的组件具备封装性正确性扩展性复用性

我们以实现一个首页轮播图的UI组件为例,这个组件的效果如下图所示:

cef33b5819992f5c38246fde3b70582c.png

上图中的组件实现了3个功能:

  1. 四张图片循环播放,每张图片停留若干时间;

  2. 当用户点击左右两边的小箭头时,图片分别切换到上一张/下一张;

  3. 当用户点击底部的小圆点的时候,则立即跳到小圆点顺序所对应的那张图片。

下面,我们就带你一步步实现这个组件:

第一步:确定UI组件的HTML结构。

根据效果图,这个组件包含了4张图片,4张图片就需要有4个HTML元素来封装。你可以采用4个块级元素来安排,比如div元素等。我们也可以将这4张图看作是一个图片列表,使用列表元素作为图片的容器:

<div class="slider">

  <ul>

    <li class="slider__item--selected">

      <img src="https://p5.ssl.qhimg.com/t0119c74624763dd070.png"/>

    li>

    <li class="slider__item">

      <img src="https://p4.ssl.qhimg.com/t01adbe3351db853eb3.jpg"/>

    li>

    <li class="slider__item">

      <img src="https://p2.ssl.qhimg.com/t01645cd5ba0c3b60cb.jpg"/>

    li>

    <li class="slider__item">

      <img src="https://p4.ssl.qhimg.com/t01331ac159b58f5478.jpg"/>

    li>

  ul>

  <a class="slider__next">a>

  <a class="slider__previous">a>

  <div class="slider__control">

    <span class="slider__control-buttons--selected">span>

    <span class="slider__control-buttons">span>

    <span class="slider__control-buttons">span>

    <span class="slider__control-buttons">span>

  div>

div>

上面的HTML结构中,首先是一个大的容器,div.slider,其中包含一个ul列表,列表中是包含四张图片的四个li元素。

这里,我们使用两个元素分别表示“下一张“和”上一张“的控制

<a class="slider__next">a>

<a class="slider__previous">a>

用四个元素表示底部的四个小圆点的控制:

<div class="slider__control">

  <span class="slider__control-buttons--selected">span>

  <span class="slider__control-buttons">span>

  <span class="slider__control-buttons">span>

  <span class="slider__control-buttons">span>

div>

当然这些控制你也可以使用其他的HTML元素来表示。

?注意:这里我们使用的CSS规则名有点特别,如果你是第一次见到可能会觉得有些奇怪。实际上这里的命名是一种CSS书写规范,叫做BEM,是英文Block-Element-Modifier的简写。

这一规范采用三个部分来描述规则,首先是Block表示组件名,这个任务是写轮播图,我们给这个组件起名字叫slider。然后是Element,比如对应的列表项li元素,表示item,所以它的class就是slider__item,这里Block和Element之间使用双下划线__连接。最后是Modifier表示状态,其中一个列表的状态是selected,所以最终的class是slider__item--selected,这里Element和Modifier之间使用双横杠--连接。

在比较复杂的UI组件中,使用BEM有几个好处:

  1. 让CSS规则保持相对简单,只用一个class就能定位对应的元素,这样优先级也相对扁平,管理起来不容易冲突。

  2. 阅读代码的人一眼可以知道一个元素是哪个组件的哪个部分。由于组件复杂的时候,HTML代码比较长,可能元素离组件容器比较远,如果使用普通层级关系,你看到一个.item元素,除非找到外层的.slider,你才能知道它属于.slider组件而不是其他组件的.item,那样在HTML代码很复杂的时候找起来就比较费劲。

第二步:设置元素的样式

然后根据效果图,我们给这段HTML代码添加CSS样式。

首先,我们给class=slider的div元素设置了宽度和高度,以及取消ul元素默认的列表样式:

.slider {

  position: relative;

  width: 790px;

  height: 340px;

}

.slider ul {

  list-style-type:none;

  position: relative;

  width: 100%;

  height: 100%;

  padding: 0;

  margin: 0;

}

然后,将class=slider__item和class=slider__item--selected的li元素的position属性设置为绝对定位(absolute),这样就能够将这4张图片重叠显示在同一个位置。如下代码所示:

.slider__item,

.slider__item--selected {

  position: absolute;

  transition: opacity 1s;

  opacity: 0;

  text-align: center;

}

.slider__item--selected {

  transition: opacity 1s;

  opacity: 1;

}

其中,transition: opacity 1s表示设置图片透明度变化的动画,时间为1秒。状态为slider__item时,显示为透明。状态为slider__item--selected时显示为不透明。

接着是控制元素的样式:

.slider__next,

.slider__previous{

  display: inline-block;

  position: absolute;

  top: 50%; /*定位在录播图组件的纵向中间的位置*/

  margin-top: -25px;

  width: 30px;

  height:50px;

  text-align: center;

  font-size: 24px;

  line-height: 50px;

  overflow: hidden;

  border: none;

  color: white;

  background: rgba(0,0,0,0.2); /*设置为半透明*/

  cursor: pointer; /*设置鼠标移动到这个元素时显示为手指状*/

  opacity: 0; /*初始状态为透明*/

  transition: opacity .5s; /*设置透明度变化的动画,时间为.5秒*/

}

.slider__previous {

  left: 0; /*定位在slider元素的最左边*/

}

.slider__next {

  right: 0; /*定位在slider元素的最右边*/

}

.slider:hover .slider__previous {

  opacity: 1;

}

.slider:hover .slider__next {

  opacity: 1;

}

.slider__previous:after {

  content: ';

}

.slider__next:after {

  content: '>';

}

上面的规则中,.slider__next, .slider__previous分别表示“向下一张”和“向上一张”的控制。初始状态下,这两个控制元素的背景色为半透明,字体颜色是白色。

.slider:hover .slider__previous 和 .slider:hover .slider__next这两条规则表示当鼠标悬停在class=slider的元素上时,显示左右两侧的控制元素。

最后,定义底部四个小点的样式:

.slider__control{

  position: relative;

  display: table; /* table 布局*/

  background-color: rgba(255, 255, 255, 0.5);

  padding: 5px;

  border-radius: 12px;

  bottom: 30px;

  margin: auto;

}

.slider__control-buttons,

.slider__control-buttons--selected{

  display: inline-block;

  width: 15px;

  height: 15px;

  border-radius: 50%;/*设置为圆形*/

  margin: 0 5px;

  background-color: white;

  cursor: pointer;

}

.slider__control-buttons--selected {

  background-color: red;

}

上面的规则中,

  • 第一条规则表示给四个小圆点设置一个灰色的背景。其中的position: relative; display: table;声明表示将它的子元素(也就是4个小圆点)采用相对定位和table布局,让它们固定显示在图片中部下方。

  • 第二条规则设置了小圆点的大小,形状,默认情况下,小圆点的颜色(白色),以及鼠标滑入后的状态(pointer)

  • 第三条规则表示,当选择后,小圆点的颜色变成红色。

第三步:设计API

页面的主体结构和样式完成之后,我们需要根据组件的功能,为该组件设计API。

我们回顾一下这个组件的需求:

  • 四张图片循环播放,每张图片停留若干时间;

  • 当用户点击左右两边的小箭头时,图片分别切换到上一张/下一张;

  • 当用户点击中下部的小圆点的时候,则立即跳到小圆点顺序所对应的那张图片。

根据上述的需求呢,我们设计了4个组件API:

  • slideTo(idx) - 切换显示idx指示位置的图片

  • slideNext() - 切换到下一张图

  • slidePrevious() - 切换到上一张图

  • getSelectedItem() - 获取选中的图片

  • getSelectedItemIndex() - 获取选中的图片的位置

    c2cdb52623b51d47a59fa9aacf29b576.png

  • 然后,将这个组件封装为一个类——slider:

class Slider {

  constructor({container}) {

    this.container = container;

    this.items = Array.from(container.querySelectorAll('.slider__item, .slider__item--selected'));

  }

  /*

    通过选择器`.slider__item--selected`获得被选中的元素

  */

  getSelectedItem() {

    const selected = this.container.querySelector('.slider__item--selected');

    return selected;

  }

  /*

    返回选中的元素在items数组中的位置。

  */

  getSelectedItemIndex() {

    return this.items.indexOf(this.getSelectedItem());

  }

  slideTo(idx) {

    const selected = this.getSelectedItem();

    if(selected) { // 将之前选择的图片标记为普通状态

      selected.className = 'slider__item';

    }

    const item = this.items[idx];

    if(item) { // 将当前选中的图片标记为选中状态

      item.className = 'slider__item--selected';

    }

  }

  /*

    将下一张图片标记为选中状态

  */

  slideNext() {

    const currentIdx = this.getSelectedItemIndex();

    const nextIdx = (currentIdx + 1) % this.items.length;

    this.slideTo(nextIdx);

  }

  /*

    将上一张图片标记为选中状态

  */

  slidePrevious() {

    const currentIdx = this.getSelectedItemIndex();

    const previousIdx = (this.items.length + currentIdx - 1) % this.items.length;

    this.slideTo(previousIdx);

  }

}

上面的代码中:Slider的构造器中的参数{container}表示放置这4张图片的父容器。在构造器中,我们获取了这个父容器下所有的元素。

然后,通过setInterval方法实现循环播放,间隔为3秒:

const container = document.querySelector('.slider');

const slider = new Slider({container});

setInterval(() => {

  slider.slideNext();

}, 3000);

这样,我们的轮播图就以每3秒的切换一次频率动起来了。

16015706bff60af68bf70b4c68cb987e.gif

第四步:实现用户控制

实现了组件的API后,我们还需要实现用户控制功能:

  • 当用户点击左右两边的小箭头时,图片分别切换到上一张/下一张,并点亮与该图片相对应的小圆点;

  • 当用户鼠标移进到底部小圆点时,则立即跳到小圆点顺序所对应的那张图片,停止轮播;

  • 当用户鼠标移出底部小圆点后,图片再次恢复轮播。

我们将构造器修改为下面这样:

constructor({container, cycle = 3000} = {}) {

  this.container = container;

  this.items = Array.from(container.querySelectorAll('.slider__item, .slider__item--selected'));

  this.cycle = cycle;

  const controller = this.container.querySelector('.slider__control');

  const buttons = controller.querySelectorAll('.slider__control-buttons, .slider__control-buttons--selected');

  controller.addEventListener('mouseover', (evt) => {

    const idx = Array.from(buttons).indexOf(evt.target);

    if(idx >= 0) {

      this.slideTo(idx);

      this.stop();

    }

  });

  controller.addEventListener('mouseout', (evt) => {

    this.start();

  });

  /*

    注册slide事件,将选中的图片和小圆点设置为selected状态

  */

  this.container.addEventListener('slide', (evt) => {

    const idx = evt.detail.index;

    const selected = controller.querySelector('.slider__control-buttons--selected');

    if(selected) selected.className = 'slider__control-buttons';

    buttons[idx].className = 'slider__control-buttons--selected';

  });

  const previous = this.container.querySelector('.slider__previous');

  previous.addEventListener('click', (evt) => {

    this.stop();

    this.slidePrevious();

    this.start();

    evt.preventDefault();

  });

  const next = this.container.querySelector('.slider__next');

  next.addEventListener('click', (evt) => {

    this.stop();

    this.slideNext();

    this.start();

    evt.preventDefault();

  });

}

如上代码所示,参数cycle表示循环播放的时间间隔,默认为3秒。然后是每个控制元素相应的事件处理。下面,我们来依次分析一下它们。

第一个用户事件:当鼠标移入或移出小圆点事件

const controller = this.container.querySelector('.slider__control');

const buttons = controller.querySelectorAll('.slider__control-buttons, .slider__control-buttons--selected');

controller.addEventListener('mouseover', (evt) =>{

  const idx = Array.from(buttons).indexOf(evt.target);

  if(idx >= 0){

    this.slideTo(idx);

    this.stop(); // 停止自动循环播放

  }

});

controller.addEventListener('mouseout', (evt) => {

  this.start(); // 开始自动循环播放

});

上面代码表示:当鼠标移动到小圆点上方的时候(mouseover),判断当前选中的是第几个小圆点,停止自动循环播放功能,然后切换到对应的图片。当鼠标移出controller元素后(mouseout),重启自动循环播放功能。

这里呢,我们先来看看stop()和start()的实现:

start() {

  this.stop();

  this._timer = setInterval(() => this.slideNext(), this.cycle);

}

stop() {

  clearInterval(this._timer);

}

如上代码所示:start()方法启动了一个定时器,每隔cycle秒执行一次slideNext()。stop()则是停止这个定时器。

第二个用户事件:点击上一张或下一张事件

const previous = this.container.querySelector('.slider__previous');

previous.addEventListener('click', evt => {

  this.stop();

  this.slidePrevious();

  this.start();

  evt.preventDefault();

});

const next = this.container.querySelector('.slider__next');

next.addEventListener('click', evt => {

  this.stop();

  this.slideNext();

  this.start();

  evt.preventDefault();

});

如上代码所示:当用户点击上一张时,程序停止定时器,然后执行slidePrevious()方法,让图片向前翻一张,然后重启定时器。类似的,当用户点击下一张时,先停止定时器,然后向后翻一张,再重启定时器。

第三个是处理一个自定义事件 —— slide事件:

this.container.addEventListener('slide', evt => {

  const idx = evt.detail.index

  const selected = controller.querySelector('.slider__control-buttons--selected');

  if(selected) selected.className = 'slider__control-buttons';

  buttons[idx].className = 'slider__control-buttons--selected';

});

对应地,修改slideTo方法,加入自定义事件触发。

slideTo(idx) {

  const selected = this.getSelectedItem();

  if(selected) {

    selected.className = 'slider__item';

  }

  const item = this.items[idx];

  if(item) {

    item.className = 'slider__item--selected';

  }

  const detail = {index: idx};

  const event = new CustomEvent('slide', {bubbles: true, detail});

  this.container.dispatchEvent(event);

}

这个自定义事件(CustomEvent),它的作用是让底部小圆点控件监听slideTo方法。当slideTo方法执行后,这个方法就会分发一次slide事件,然后在这个事件中,更新底部小圆点的状态,让小圆点的状态和各自的图片状态对应起来。

最后将调用过程改成:

const slider = new Slider({container});

slider.start();

到此,这个组件的全部功能就完成了。

通过轮播组件编写过程,我们可以总结一下组件设计的一般性步骤:

  1. 设计HTML结构

  2. 设计组件的API

  3. 设计用户控制流程

从上面的代码中,我们可以看到这个轮播组件实现了封装性正确性,但是缺少了可扩展性。这个组件只能满足自身的使用,它的实现代码很难扩展到其他的组件,当有功能变化时,也需要修改其自身内部的代码。

比如产品经理因为某种原因,希望将图片下方的小圆点暂时去掉,只保留左右箭头。那么在这个版本中,就需要这么做:

  1. 注释掉HTML中.slider__control相关的代码

  2. 修改Slider组件,注释掉与小圆点控制相关的代码

又或者,将来需要为这个组件添加新的用户控制,都需要对这个组件进行再修改。

那么,如何可以避免这样的修改,让组件具备可扩展性呢?

关于组件封装,你是否心中有其他想法,欢迎留言讨论。也可以关注小程序【君喻学堂】参与课程《前端进阶十日谈》,来一步步完善我们的组件,直到完成一个小而美的UI组件框架。

39f6f30962b14ce6fee05d91c7c6747f.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值