【青训营】- 如何写好JS (中)

193 阅读3分钟

组件封装

让我们来看一个电商网站轮播图的例子。用原生JS写一个电商网站的轮播图,应该怎么实现?

Snipaste_2021-08-23_16-47-01.png

基本方法

依旧是HTML、CSS、JS各司其职。轮播图是一个典型的列表结构,用无序列表<ul>元素来实现。通过使用绝对定位将图片重叠在一个位置,用transition来做切换效果。

Snipaste_2021-08-23_18-37-59.png

Snipaste_2021-08-23_18-38-15.png

接下来我们为轮播图的行为设计一组API,API设计应保证原子操作,职责单一,满足灵活性。包括以下几种:

  • Slider
  • +getSelectedItem()获取被选中节点
  • +getSelectedItemIndex()获取被选中节点在所有节点中的位置
  • +slideTo()切换第几张
  • +slideNext()切换到下一张
  • +slidePrevious()切换到上一张
class Slider{
  constructor(id){
    this.container = document.getElementById(id);
    this.items = this.container
    .querySelectorAll('.slider-list__item, .slider-list__item--selected');
  }
  getSelectedItem(){...}
  getSelectedItemIndex(){...}
  slideTo(idx){...}
  slideNext(){...}
  slidePrevious(){...}
}
//创建一个滑动组件,并切换到第二张
const slider = new Slider('my-slider');
slider.slideTo(2);

在此基础上我们加上控制流,用来监听事件。

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){
      controller.addEventListener('mouseover', evt=>{});      
      controller.addEventListener('mouseout', evt=>{});  
      this.container.addEventListener('slide', evt => {})
    }
    //切换下一张
    const previous = this.container.querySelector('.slide-list__previous');
    if(previous){
      previous.addEventListener('click', evt => {});
    }
    //切换上一张
    const next = this.container.querySelector('.slide-list__next');
    if(next){
      next.addEventListener('click', evt => {});
    }
  }
  getSelectedItem(){...}
  getSelectedItemIndex(){...}
  slideTo(idx){...}
  slideNext(){...}
  slidePrevious(){...}
  start(){...}
  stop(){...}
}

const slider = new Slider('my-slider');
slider.start();

插件化

观察上一版的构造函数,我们可以发现代码写的太过臃肿,除了初始化以外,我们还注册了按钮的点击、切换、滑动的事件。因此我们考虑将这些控制元素分离出去,通过抽取成插件,凭借依赖注入的方式建立联系。

class Slider{
  constructor(){
  }
  registerPlugins(...plugins){
    plugins.forEach(plugin => plugin(this));
  }
}

function pluginController(slider){
}
function pluginPrevious(slider){
}
function pluginNext(slider){
}

const slider = new Slider('my-slider');
slider.registerPlugins(pluginController, pluginPrevious, pluginNext);
slider.start();

模板化

尽管使用插件化,已经实现了组件和不同插件之间的解耦。但我们发现HTML有继续简化的空间,有些时候我们不需要某部分的插件,还需要手动的去删除。但通过模板化,就能简化HTML删改的过程。

<div id="my-slider" class="slider-list"></div>
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>`;
  }
  registerPlugins(...plugins){...}
  getSelectedItem(){...}
  getSelectedItemIndex(){...}
  slideTo(idx){...}
  slideNext(){...}
  slidePrevious(){...}
  start(){...}
  stop(){...}
}

const pluginController = {
  render(images){...},
  action(slider){...}
};

const pluginPrevious = {
  render(){...},
  action(slider){...}
};

const pluginNext = {
  render(){...},
  action(slider){...}  
};

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

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

组件框架

更一般的,我们能将通用组件模型抽象出来,这样我们在使用其他组件的时候都能继承该模型,而不仅仅只是Slider

class Component{
  constructor(id, opts = {name, data:[]}){}
  registerPlugins(...plugins){}
  render(data) {
    /* abstract */
    return ''
  }
}

总结

这里我们只将HTML实现了模板化,并没有将CSS也纳入其中。比较常见的框架如React、Vue都实现了HTML、CSS、JS的组件化开发。

使用组件化开发的模式,能避免大量重复代码、逻辑、功能在多个页面使用,避免了复杂的维护。通过功能化拆分、组件封装不仅增加了代码的可读性,也降低了运维成本。

在组件设计中应遵守以下基本原则:

  • 封装性
  • 正确性
  • 扩展性
  • 复用性

组件实现的基本步骤包括:HTML结构设计、CSS展现效果设计、JS行为设计。在此基础上,通过插件化模板化抽象化,对组件进行重构优化。