【青训营】月影老师告诉我写好JavaScript的三大原则——组件封装

2,167 阅读12分钟

参加了这次字节青训营的活动,见到了传说中的月影老师,关键还听他给我们上了两节如何写好JS的课! 太赚啦,今天我把上课学的东西分享出来,和大家一起学习学习!~

月影老师告诉我们写好JavaScript(包括其他语言)的三大原则 ① 各司其责 ② 组件封装 ③ 过程抽象

今天我们来学习写好JavaScript的另一个原则:组件封装

1. 起步

先来看看组件的概念

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

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

下面我们来看一个案例——轮播图

2. 轮播图案例

大家在刚接触前端的时候,一定都写过轮播图,还记得如何用原生JavaScript写一个电商网站的轮播图吗?

GIF 2021-8-25 9-45-08.gif

版本一:API无交互版

结构:HTML

轮播图是⼀个典型的列表结构,我们可以使⽤⽆序列表<ul>元素来实现。

这里类的命名有点讲究,是一种CSS规则名书命名规范,其中 slider 表示组件名,-list表示元素,__item表示具体元素项,--selected表示的是状态(看完CSS的代码你就知道为什么这样命名更好了~)

<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.png

表现:CSS

  • 使用 CSS 绝对定位将图片重叠在同一个位置
  • 轮播图切换的状态使用修饰符(modifier)这里是 --checked
  • 轮播图的切换动画使用 CSS transition

再回顾一下这种讲究的CSS规则名命名规范,其中 slider 表示组件名,-list表示元素,__item表示具体元素项,--selected表示的是状态

这样命名,当组件多了,CSS多起来的时候,很容易分辨清楚这段CSS是哪个组件哪个元素哪个状态的样式规则

#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;
}

image.png

最后我们需要通过JavaScript来控制页面的行为

行为:JS —— API

API 设计应保证原子操作,职责单一,满足灵活性。

image.png

// 创建一个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');
slider.slideTo(1);
slider.slideTo(2);
slider.slideNext();
slider.slidePrevious();

或者我们可以直接定义一个定时器,让他自动播放

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

GIF 2021-8-25 21-35-50.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

接下来我们来看看CSS样式

通过下面这段代码,你可以看出来这种CSS命名规范有很好~

其实这种命名规范有一个名字,叫做Block-Element-Modifier 简称为BEM

#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;
}

image.png

行为: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();

3. 总结:基本方法

  • 结构设计 HTML
  • 展现效果 CSS
  • 行为设计 JavaScript
    • API (功能)
    • Event (控制流)

对于我这样的新手来说,我觉得我已经完工了,但是我们再回想一下我们在1. 起步中关于好的组件的定义

具备封装性、正确性、扩展性、复用

这样看来我们只做到了封装性和正确性,但是扩展性和复用性还差点意思

也就是说上面的基本代码具有很大的改进空间,接下来我们准备来 重构 这个轮播图组件

4. 重构

重构一:解耦JS——插件化

上面解决方案的类中的构造器实在是太臃肿了,做了很多本来不应该它要做的事,所以我们考虑插件化,将构造器进行简化

先来看看之前的构造函数做了哪些事

  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){
      // 鼠标经过某个小圆点,就将此圆点对应的图片显示出来,并且停止循环轮播
      controller.addEventListener('mouseover', evt=>{
          // ...
      });
      
      // 鼠标移开小圆点,就继续开始循环轮播
      controller.addEventListener('mouseout', evt=>{
        this.start();
      });
      
      // 注册slide事件,将选中的图片和小圆点设置为selected状态
      this.container.addEventListener('slide', evt => {
          // ...
    }
    
    // 点击左边小箭头,翻到前一页
    const previous = this.container.querySelector('.slide-list__previous');
        // ...
    }
    
    // 点击右边小箭头,翻到后一页
    const next = this.container.querySelector('.slide-list__next');
        // ...
  }

image.png

解耦: 将控制元素抽取成插件; 插件与组件之间通过依赖注⼊方式建立联系

我们要将用户控制的操作从组件中抽离出来,做成插件,这样就提高了组件的可扩展性!!!

用户的控制组件分为三个部分可以抽离成三个插件。

首先将小圆点的控制抽离成一个插件pluginController

插件接收的参数就是组件的实例,将控制流中的事件写在这里,插件中的逻辑就是之前构造函数中的逻辑。

function pluginController(slider){
  // 对小圆点的操作控制流
  const controller = slider.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){
        slider.slideTo(idx);
        slider.stop();
      }
    });
    
    // 鼠标移开小圆点,就继续开始循环轮播
    controller.addEventListener('mouseout', evt=>{
      slider.start();
    });
    
    // 注册slide事件,将选中的图片和小圆点设置为selected状态
    slider.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';
    });
  }  
}

将左翻页的控制抽离成插件pluginPrevious

function pluginPrevious(slider){
  const previous = slider.container.querySelector('.slide-list__previous');
  if(previous){
    previous.addEventListener('click', evt => {
      slider.stop();
      slider.slidePrevious();
      slider.start();
      evt.preventDefault();
    });
  }  
}

将右翻页的控制抽离成插件pluginNext

function pluginNext(slider){
  const next = slider.container.querySelector('.slide-list__next');
  if(next){
    next.addEventListener('click', evt => {
      slider.stop();
      slider.slideNext();
      slider.start();
      evt.preventDefault();
    });
  }  
}

最后我们的组件就是这样定义的(省略重复API代码)

此时的构造函数已经精简了,我们将JS进行了解耦,通过注册插件registerPlugins来使用各种插件(控件)~

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;
  }
  registerPlugins(...plugins){
    // 这里的this就是组件的实例对象
    plugins.forEach(plugin => plugin(this));
  }
}

这种将依赖对象传入插件初始化函数的方式,叫做依赖注入,这是一种组件与插件解耦的基本思路

const container = document.querySelector('.slider'); 
const slider = new Slider({container}); 
// 注册三个插件
slider.registerPlugins(pluginController, pluginPrevious, pluginNext); 
slider.start();

GIF 2021-8-26 12-42-07.gif

进行插件化之后,我们可以任意组合我们想要的插件,比如我们将底部小圆点插件去除

slider.registerPlugins(pluginPrevious, pluginNext); 

GIF 2021-8-26 10-17-14.gif

可以看到下方的小圆点已经不生效了(注意看上面动图小圆点已经不动了),但是这里有了新的问题,下方小圆点虽然失效了,但是没有消失,我们要是将小圆点也去除就要手动去操作HTML了~

所以我们要继续对组件进行重构!我们解耦HTML,让JavaScript来渲染组件的结构 —— 模板化

重构二: 解耦HTML——模板化

将HTML模板化,也就是让JavaScript来渲染组件的HTML,这样更易于扩展

image.png

在组件中加入了render()渲染函数,用来渲染HTML

class Slider {
  constructor(id, opts = {
    images: [],
    cycle: 3000
  }) {
    this.container = document.getElementById(id);
    this.options = opts;
    this.container.innerHTML = this.render();
    this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected');
    this.cycle = opts.cycle || 3000;
    this.slideTo(0);
  }
  render() {
    const images = this.options.images;
    const content = images.map(image => `
      <li class="slider-list__item">
        <img src="${image}"/>
      </li>    
    `.trim());

    return `<ul>${content.join('')}</ul>`;
  }
}  

这里将图片放入一个images数组中,这样就可以让组件拓展成 指定任意多的图片的 轮播图

接下来定义下三个插件(插件从一个函数变成一个对象,对象中有两个函数,一个渲染HTML,一个注册自定义事件JS)

下部小圆点的插件是这样定义的,插件中也要定义render()渲染函数,action()用来注册自定义事件

const pluginController = {
  render(images) {
    return `
      <div class="slide-list__control">
        ${images.map((image, i) => `
            <span class="slide-list__control-buttons${i===0?'--selected':''}"></span>
         `).join('')}
      </div>    
    `.trim();
  },
  action(slider) {
    const controller = slider.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) {
          slider.slideTo(idx);
          slider.stop();
        }
      });

      controller.addEventListener('mouseout', evt => {
        slider.start();
      });

      slider.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 pluginPrevious = {
  render() {
    return `<a class="slide-list__previous"></a>`;
  },
  action(slider) {
    const previous = slider.container.querySelector('.slide-list__previous');
    if (previous) {
      previous.addEventListener('click', evt => {
        slider.stop();
        slider.slidePrevious();
        slider.start();
        evt.preventDefault();
      });
    }
  }
};

向后翻页的插件是这样定义的

const pluginNext = {
  render() {
    return `<a class="slide-list__next"></a>`;
  },
  action(slider) {
    const previous = slider.container.querySelector('.slide-list__next');
    if (previous) {
      previous.addEventListener('click', evt => {
        slider.stop();
        slider.slideNext();
        slider.start();
        evt.preventDefault();
      });
    }
  }
};

注册插件是这样定义的,渲染HTML结构,绑定JS事件行为

registerPlugins(...plugins) {
  plugins.forEach(plugin => {
    const pluginContainer = document.createElement('div');
    pluginContainer.className = '.slider-list__plugin';
    pluginContainer.innerHTML = plugin.render(this.options.images);
    this.container.appendChild(pluginContainer);
    plugin.action(this);
  });
}

将HTML解耦后,我们的HTML就只需要一个盒子就可以了

<div id="my-slider" class="slider-list"></div>

最后,我们是这样来使用这个组件的

const slider = new Slider('my-slider', {
  images: ['https://p5.ssl.qhimg.com/t0119c74624763dd070.png',
    'https://p4.ssl.qhimg.com/t01adbe3351db853eb3.jpg',
    'https://p2.ssl.qhimg.com/t01645cd5ba0c3b60cb.jpg',
    'https://p4.ssl.qhimg.com/t01331ac159b58f5478.jpg'
  ],
  cycle: 1000
});

slider.registerPlugins(pluginController, pluginPrevious, pluginNext);
slider.start();

GIF 2021-8-26 12-40-27.gif

这时,如果不想要下面小圆点的插件了,可以这样

slider.registerPlugins(pluginPrevious, pluginNext);

GIF 2021-8-26 12-39-20.gif

完美~其实还可以解耦CSS,但是这里课上就没有说了,以后有时间再探索探索吧~~

至此,拓展性有了,但是可复用性还不够,我们继续重构,将组件抽象成一个组件框架,提高组件的复用性

重构三:抽象——组件框架

将通⽤的组件模型抽象出来

image.png

定义一个通用组件类

class Component {
  constructor(id, opts = {
    name,
    data: []
  }) {
    this.container = document.getElementById(id);
    this.options = opts;
    this.container.innerHTML = this.render(opts.data);
  }
  registerPlugins(...plugins) {
    plugins.forEach(plugin => {
      const pluginContainer = document.createElement('div');
      pluginContainer.className = `.${name}__plugin`;
      pluginContainer.innerHTML = plugin.render(this.options.data);
      this.container.appendChild(pluginContainer);

      plugin.action(this);
    });
  }
  render(data) {
    /* abstract */
    return ''
  }
}

让轮播图组件 继承自 我们定义的通用组件

class Slider extends Component {
  constructor(id, opts = {
    name: 'slider-list',
    data: [],
    cycle: 3000
  }) {
    super(id, opts);
    this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected');
    this.cycle = opts.cycle || 3000;
    this.slideTo(0);
  }
  render(data) {
    const content = data.map(image => `
      <li class="slider-list__item">
        <img src="${image}"/>
      </li>    
    `.trim());

    return `<ul>${content.join('')}</ul>`;
  }
  getSelectedItem() {
    const selected = this.container.querySelector('.slider-list__item--selected');
    return selected
  }
  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';
    }

    const detail = {
      index: idx
    }
    const event = new CustomEvent('slide', {
      bubbles: true,
      detail
    })
    this.container.dispatchEvent(event)
  }
  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);
  }
  addEventListener(type, handler) {
    this.container.addEventListener(type, handler);
  }
  start() {
    this.stop();
    this._timer = setInterval(() => this.slideNext(), this.cycle);
  }
  stop() {
    clearInterval(this._timer);
  }
}

三个插件

小圆点插件

const pluginController = {
  render(images) {
    return `
      <div class="slide-list__control">
        ${images.map((image, i) => `
            <span class="slide-list__control-buttons${i===0?'--selected':''}"></span>
         `).join('')}
      </div>    
    `.trim();
  },
  action(slider) {
    let controller = slider.container.querySelector('.slide-list__control');

    if (controller) {
      let buttons = controller.querySelectorAll(
        '.slide-list__control-buttons, .slide-list__control-buttons--selected');
      controller.addEventListener('mouseover', evt => {
        var idx = Array.from(buttons).indexOf(evt.target);
        if (idx >= 0) {
          slider.slideTo(idx);
          slider.stop();
        }
      });

      controller.addEventListener('mouseout', evt => {
        slider.start();
      });

      slider.addEventListener('slide', evt => {
        const idx = evt.detail.index;
        let 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 pluginPrevious = {
  render() {
    return `<a class="slide-list__previous"></a>`;
  },
  action(slider) {
    let previous = slider.container.querySelector('.slide-list__previous');
    if (previous) {
      previous.addEventListener('click', evt => {
        slider.stop();
        slider.slidePrevious();
        slider.start();
        evt.preventDefault();
      });
    }
  }
};

向后翻页插件

const pluginNext = {
  render() {
    return `<a class="slide-list__next"></a>`;
  },
  action(slider) {
    let previous = slider.container.querySelector('.slide-list__next');
    if (previous) {
      previous.addEventListener('click', evt => {
        slider.stop();
        slider.slideNext();
        slider.start();
        evt.preventDefault();
      });
    }
  }
};

使用我们的组件,使用方式不变

const slider = new Slider('my-slider', {
  name: 'slide-list',
  data: ['https://p5.ssl.qhimg.com/t0119c74624763dd070.png',
    'https://p4.ssl.qhimg.com/t01adbe3351db853eb3.jpg',
    'https://p2.ssl.qhimg.com/t01645cd5ba0c3b60cb.jpg',
    'https://p4.ssl.qhimg.com/t01331ac159b58f5478.jpg'
  ],
  cycle: 1000
});

slider.registerPlugins(pluginController, pluginPrevious, pluginNext);
slider.start();

GIF 2021-8-26 12-46-28.gif

这,这就是一个小型的组件框架啊~

虽然现在有很多组件库比如Vue还有React中的组件模式,但是我们自己研究一下这里面的机制与原理对我们理解组件库以及JavaScript还是会很有帮助的!!

这样的不断解耦JS实现插件化,解耦HTML实现模板化,甚至还可以解耦CSS,这中思路提供了代码设计和抽象的一套通用规范,而遵循这套规范的基础库,实际上就是完整的UI组件框架!!!

5. 最佳实践:组件封装

  • 组件设计的原则:封装性、正确性、扩展性、复用性

  • 实现组件的步骤:结构设计、展现效果、行为设计 (封装性、正确性)

  • 三次重构:插件化(扩展性)、模板化(扩展性)、抽象化(复用性)

更多相关博文

【青训营】月影老师告诉我写好JavaScript的三大原则——各司其责

【青训营】月影老师告诉我写好JavaScript的三大原则——组件封装

【青训营】月影老师告诉我写好JavaScript的三大原则——过程抽象

【青训营】月影老师告诉我写好JavaScript的四大技巧——风格优先

【青训营】月影老师告诉我写好JavaScript的四大技巧——保证正确

【青训营】月影老师告诉我写好JavaScript的四大技巧——封装函数

【青训营】月影老师告诉我写好JavaScript的四大技巧——妙用特性

也可以关注专栏: 【青训营笔记专栏】