前端与JavaScript【写好JS的三大原则——组件封装】 | 青训营笔记

68 阅读5分钟

前端与JavaScript【写好JS的三大原则——组件封装】 | 青训营笔记

这是我参与【第四届青训营】笔记创作活动的第3天

一、前言

组件的定义:

组件是指Web页面上抽出来的一个个包含模板(HTML)、功能(JS)和样式(CSS)的单元。

好的组件具备封装性正确性扩展性复用性

老师通过轮播图的案例讲解了组件封装

二、轮播图案例
版本一

轮播图由列表结构构成,使用无序列表来实现

结构:HTML

<div id="my-slider" class="slider-list">
    <ul>
      <li class="slider-list__item--selected">
        <img src="https://p5.ssl.qhimg.com/t0119c74624763dd070.png"/>
      </li>
      <li class="slider-list__item">
        <img src="https://p4.ssl.qhimg.com/t01adbe3351db853eb3.jpg"/>
      </li>
      <li class="slider-list__item">
        <img src="https://p2.ssl.qhimg.com/t01645cd5ba0c3b60cb.jpg"/>
      </li>
      <li class="slider-list__item">
        <img src="https://p4.ssl.qhimg.com/t01331ac159b58f5478.jpg"/>
      </li>
    </ul>
  </div>

image-20220801114155152

表现:CSS

  • 使用CSS绝对定位将图片重叠在同一个位置
  • 轮播图切换的状态使用修饰符
  • 轮播图的切换动画使用CSS transition
#my-slider{
    position: relative;
    width: 790px;
  }

  .slider-list ul{
    list-style-type:none;
    position: relative;
    padding: 0;
    margin: 0;
  }

  .slider-list__item,
  .slider-list__item--selected{
    position: absolute;
    transition: opacity 1s;
    opacity: 0;
    text-align: center;
  }

  .slider-list__item--selected{
    transition: opacity 1s;
    opacity: 1;
  }

/* slider 表示组件名,
-list表示元素,
__item表示具体元素项,
--selected表示的是状态 */

image-20220801114725208

行为:JS

// 创建一个Slider类,封装一些API
class Slider{
  constructor(id){
    this.container = document.getElementById(id);
    this.items = this.container
    .querySelectorAll('.slider-list__item, .slider-list__item--selected');
  }
  
  // 获取选中的图片元素:通过选择器`.slider__item--selected`获得被选中的元素
  getSelectedItem(){
    const selected = this.container
      .querySelector('.slider-list__item--selected');
    return selected
  }
  
  // 获取选中图片的索引值:返回选中的元素在items数组中的位置。
  getSelectedItemIndex(){
    return Array.from(this.items).indexOf(this.getSelectedItem());
  }
  
  // 跳转到指定索引的图片
  slideTo(idx){
    const selected = this.getSelectedItem();
    if(selected){ 
      // 将之前选择的图片标记为普通状态
      selected.className = 'slider-list__item';
    }
    const item = this.items[idx];
    if(item){
      // 将当前选中的图片标记为选中状态
      item.className = 'slider-list__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);  
  }
}

因此可以调用API来实现轮播图

const slider = new Slider('my-slider');
setInterval(() => { 
    slider.slideNext(); 
}, 1000);

动画.gif

版本二

为了让用户能够对轮播图进行控制,需要设计一套控制流

结构:HTML

加入控制轮播图前后翻图的箭头和控制选图的圆点

<div id="my-slider" class="slider-list">
  <ul>
    <li class="slider-list__item--selected">
      <img src="https://p5.ssl.qhimg.com/t0119c74624763dd070.png"/>
    </li>
    <li class="slider-list__item">
      <img src="https://p4.ssl.qhimg.com/t01adbe3351db853eb3.jpg"/>
    </li>
    <li class="slider-list__item">
      <img src="https://p2.ssl.qhimg.com/t01645cd5ba0c3b60cb.jpg"/>
    </li>
    <li class="slider-list__item">
      <img src="https://p4.ssl.qhimg.com/t01331ac159b58f5478.jpg"/>
    </li>
  </ul>
  <a class="slide-list__next"></a>
  <a class="slide-list__previous"></a>
  <div class="slide-list__control">
    <span class="slide-list__control-buttons--selected"></span>
    <span class="slide-list__control-buttons"></span>
    <span class="slide-list__control-buttons"></span>
    <span class="slide-list__control-buttons"></span>
  </div>
</div>

表现:CSS

#my-slider{
  position: relative;
  width: 790px;
  height: 340px;
}

.slider-list ul{
  list-style-type:none;
  position: relative;
  width: 100%;
  height: 100%;
  padding: 0;
  margin: 0;
}

.slider-list__item,
.slider-list__item--selected{
  position: absolute;
  transition: opacity 1s;
  opacity: 0;
  text-align: center;
}

.slider-list__item--selected{
  transition: opacity 1s;
  opacity: 1;
}

.slide-list__control{
  position: relative;
  display: table;
  background-color: rgba(255, 255, 255, 0.5);
  padding: 5px;
  border-radius: 12px;
  bottom: 30px;
  margin: auto;
}

.slide-list__next,
.slide-list__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;
  background: transparent;
  color: white;
  background: rgba(0,0,0,0.2); /*设置为半透明*/
  cursor: pointer; /*设置鼠标移动到这个元素时显示为手指状*/
  opacity: 0; /*初始状态为透明*/
  transition: opacity .5s; /*设置透明度变化的动画,时间为.5秒*/
}

.slide-list__previous {
  left: 0; /*定位在slider元素的最左边*/
}

.slide-list__next {
  right: 0; /*定位在slider元素的最右边*/
}

#my-slider:hover .slide-list__previous {
  opacity: 1;
}


#my-slider:hover .slide-list__next {
  opacity: 1;
}

.slide-list__previous:after {
  content: '<';
}

.slide-list__next:after {
  content: '>';
}

/*下面是四个小圆点的样式,其实通过这种BEM命名规则你也能看出来*/
.slide-list__control-buttons,
.slide-list__control-buttons--selected{
  display: inline-block;
  width: 15px;
  height: 15px;
  border-radius: 50%;
  margin: 0 5px;
  background-color: white;
  cursor: pointer; /*设置鼠标移动到这个元素时显示为手指状*/
}
/*当选择后,小圆点的颜色变成红色*/
.slide-list__control-buttons--selected {
  background-color: red;
}

行为:JS——控制流

在API的基础上,加入控制流,实现自动轮播额手动轮播

通过自定义事件来解耦

class Slider{
  constructor(id, cycle = 3000){
  
    this.container = document.getElementById(id);
    this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected');
    this.cycle = cycle;

    const controller = this.container.querySelector('.slide-list__control');
    if(controller){
      const buttons = controller.querySelectorAll('.slide-list__control-buttons, .slide-list__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('.slide-list__control-buttons--selected');
        if(selected) selected.className = 'slide-list__control-buttons';
        buttons[idx].className = 'slide-list__control-buttons--selected';
      })
    }
    
    // 点击左边小箭头,翻到前一页
    const previous = this.container.querySelector('.slide-list__previous');
    if(previous){
      previous.addEventListener('click', evt => {
        this.stop();
        this.slidePrevious();
        this.start();
        evt.preventDefault();
      });
    }
    // 点击右边小箭头,翻到后一页
    const next = this.container.querySelector('.slide-list__next');
    if(next){
      next.addEventListener('click', evt => {
        this.stop();
        this.slideNext();
        this.start();
        evt.preventDefault();
      });
    }
  }
  getSelectedItem(){
    let selected = this.container.querySelector('.slider-list__item--selected');
    return selected
  }
  getSelectedItemIndex(){
    return Array.from(this.items).indexOf(this.getSelectedItem());
  }
  slideTo(idx){
    let selected = this.getSelectedItem();
    if(selected){ 
      selected.className = 'slider-list__item';
    }
    let item = this.items[idx];
    if(item){
      item.className = 'slider-list__item--selected';
    }
  
    const detail = {index: idx}
    const event = new CustomEvent('slide', {bubbles:true, detail})
    this.container.dispatchEvent(event)
  }
  slideNext(){
    let currentIdx = this.getSelectedItemIndex();
    let nextIdx = (currentIdx + 1) % this.items.length;
    this.slideTo(nextIdx);
  }
  slidePrevious(){
    let currentIdx = this.getSelectedItemIndex();
    let previousIdx = (this.items.length + currentIdx - 1) % this.items.length;
    this.slideTo(previousIdx);  
  }
  // 定义一个定时器,循环播放
  start(){
    this.stop();
    this._timer = setInterval(()=>this.slideNext(), this.cycle);
  }
  // 停止循环播放(用户在自己操作的时候要停止自动循环)
  stop(){
    clearInterval(this._timer);
  }
}

const slider = new Slider('my-slider');
slider.start();
三、小结
  • 结构化设计HTML
  • 展现效果CSS
  • 行为设计JavaScript
    • API(功能)
    • Event(控制流)
四、重构
插件化

不需要圆点和箭头时可以直接删除HTML部分,而不需要修改JS,将图片轮播和按钮部分分离开,实现控制轮播和圆点解耦

解耦

  • 将控制元素抽取成插件
  • 插件与组件之间通过依赖注入建立联系

将控制元素抽取成插件,插件与组件之间通过依赖注入建立联系,在构造器中把圆点和轮播功能抽离出来作为插件函数,然后在组件中添加注册函数,轮播调用时,只需将插件函数通过函数注册进去,去除就是把函数注销,通过这种方法就可以将用户控制的操作从组件抽离,做成插件,提高组件扩展性

模板化

解耦

  • 将HTML模板化,更易于扩展

    在老师的例子中,JS使用render()的API来实现HTML模块化,使用数组存储图片路径。

    通过HTML模块化,即便注释圆点部分插件,也不影响其他功能。

抽象化
  • 将组件通用模型抽象出来

将模板化中的registerPlugins(...plugins)、render()组件抽离出来放在Component类中,然后通过继承的方式将其继承该子类Slider,这样就能抽象出一个Component类,该类体系较为完整,相当于小的组件。