如何写好JavaScript—组件封装| 青训营笔记

75 阅读4分钟

这是我参与「第四届青训营 」笔记创作活动的第1天

如何写好JavaScript

由于课程内容较多,决定分开写,这篇主要介绍组封装。对于封装的不断改善,能够帮助我们逐层了解组件封装的主要思想,并且对于我们下面进行组件式开发提供了许多思考。

组件封装

用原生JS实现录播图,展示如下

57add660e58db0a3e4ec34cca10e90f.png

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

行为API

创建slider类用来实现图片的轮播效果,并且在类的原型方法中定义了一些行为API如getSelectedItem、getSelectedItemIndex、slideTo、slideNext、slidePrevious。其中slideNext与slidePrevious这两个方法没有涉及到页面状态的改变(即css样式没有变化),而slideTo方法当点击底下按钮的同时按钮会出现样式上的改变(由白色变成红色)。鼠标点击圆按钮的同时会出发两种变化,因此需要对于其进行解耦。

行为控制流

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();
    });
    
    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';
    })
}

上述代码实现了行为的控制,切换图片由之前定义的方法slideTo来实现。如上述所说,slideTo在切换下方按钮的时候既要切换图片还需要改变按钮的css样式,这里我们需要重写上述slideTo方法,加上自定义事件slide,并将两种行为解耦。此时在控制流中可以监听slide事件,手动的去改变目标的样式。

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)                                     //把事件派发出去
}

重构:插件化

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();
      });

      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';
      });
    }  
  }

之前行为的控制都是放在class的constructor里实现的,这会导致class内容太过臃肿,此时我们将控制元素抽取成插件,插件与组件之间通过依赖注入方式建立联系,并在类的原型方法中添加registerPlugins,在调用registerPlugins时将插件依次注册。这样可以在外部手动选择注册需要的插件,这实现了各个控制行为之间的解耦,这就使封装变得更加灵活。

slider.registerPlugins(pluginController, pluginPrevious, pluginNext);

重构:模板化

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>`;
    }
    ...
  }

将HTML模板化,更易于扩展

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';
     });
   }    
 }
};

页面的变化都是由数据变化来驱动,数据要从外部输入的,而之前的插件中,HTML作为独立的部分,其中的数据是嵌在HTML中,不便于从外部传递数据,更提现不了封装的思想。所以就要引入HTML的模块化。在实现方法上,与之前不同的是pluginController这里是作为一种对象,其中保存两种方法render与action,render根据接受到的images参数来动态渲染HTML,action则是控制行为与之前的function pluginController函数作用类似。将HTML模块化更易于扩展。实例化对象的同时将images参数传入,展现了对于组件的更好的控制性。

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:3000});

抽象:组件化

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>`;
  }
  ...
}

Slider可以通过对于通用组件的继承实现方法以及一些属性的复用。麻雀虽小,五脏俱全,虽然只是很简单的轮播图的组件,但是我们可以看出组件式开发的思想,从行为控制、插件化、模块化、组件化,其实就是一步一步解耦合的过程,使原先的封装更具有可扩展性与可复用性。想必目前流行的框架:React、Vue的组件都是类似与此的思想。

总结

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

  • 实现组件的步骤:结构设计、展现效果、行为设计

  • 三次重构

    1. 插件化
    2. 模板化
    3. 抽象化(组件框架)

    组件是指Web页面上抽出来一个个包含模版(HTML)、功能(JS)和样式(CSS)的单元。好的组件具备封装性、正确性、扩展性、复用性。