这是我参与「第五届青训营 」伴学笔记创作活动的第 4 天

介绍

上篇文章经过一个深夜形式切换的示例,咱们知道了写好 JS 的一些准则:

  • 让 HTML、CSS 和 JS 功用别离。
  • 应当避免不必要的由 JS 直接操作款式和 HTML。
  • 用 CSS 类(或伪元素)来表明状况。
  • 纯展现类交互能够寻求零 JS 计划。

这样不仅便于后续代码的维护扩展,而且能够做到代码简洁、可读性高。在开发进程中,写好 JS 还有两个重要的准则:组件封装、进程笼统。

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

怎么完成一个电商网站的轮播图?

  1. HTML – 轮播图是一个典型的列表结构,咱们能够运用无序列表 ul 元素来完成。
  2. CSS – 运用肯定定位将图片重叠在同一个位置,添加 CSS 类表明轮播图切换的状况,轮播图的切换动画运用 CSS transition。
  3. JS – API 规划应确保原子操作、职责单一、灵活性,并运用自界说事情 CustomEvent 来解耦操控流。

版别一

界说一个轮播图类,将各功用经过在类中界说一系列办法完成:

  • getSelectedItem – 获取当时播映图片。
  • getSelectedItemIndex – 获取当时播映图片指针。
  • slideTo – 鼠标悬浮底部小圆点时,切换到指定位置图片。
  • slideNext – 切换到下一张图片。
  • slidePrevious – 切换到上一张图片。
  • start – 轮播图开端播映。
  • stop – 轮播图暂停播映。
  • constructor – 结构函数,在里面界说逻辑和绑定事情。
class Slider{
  constructor(id, cycle = 3000){
  }
  getSelectedItem(){
  }
  getSelectedItemIndex(){
  }
  slideTo(idx){
  }
  slideNext(){
  }
  slidePrevious(){
  }
  start(){
  }
  stop(){
  }
}
const slider = new Slider('my-slider');
slider.start();

这样就完成了轮播图的功用,可是类中的结构器实在是太臃肿了,做了许多原本不应该它要做的事,假如咱们要改需求,比如去除底部小圆点的鼠标悬浮切换图片的功用,那咱们要在结构函数里面查找这部分功用并删去相应的代码。

这样看来,咱们只做到了封装性和正确性,可是缺乏扩展性和复用性,所以咱们需求考虑插件化,将结构器进行简化,将各功用别离出来。

写好 JS 的原则 组件封装、过程抽象 | 青训营笔记

版别二

将操控元素抽取成插件,插件与组件之间经过依靠注入(将依靠目标传入插件初始化函数)的方式建立联络宽和耦,这样就进步了组件的可扩展性。

首先将小圆点的操控抽离成一个插件 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();
    });
    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();
    });
  }
}

然后,将下一张图片按钮操控抽离成一个插件 pluginPrevious。

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

最终,在轮播图类中界说一个注册插件的办法。

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

这样,咱们就能够经过注册插件 registerPlugins 来运用各种插件,假如咱们要去除底部小圆点的鼠标悬浮切换图片的功用,只需求调用 registerPlugins 办法时,不将 pluginController 传入即可。

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

可是这儿有了新的问题,下方小圆点尽管失效了,可是在页面上并没有消失,要将小圆点也去除就要手动去操作 HTML,所以咱们需求解耦 HTML。

写好 JS 的原则 组件封装、过程抽象 | 青训营笔记

版别三

将 HTML 模板化,也便是让 JavaScript 来烘托组件的 HTML,这样更易于扩展。

写好 JS 的原则 组件封装、过程抽象 | 青训营笔记

将 HTML 解耦后,咱们只需求一个轮播图容器标签即可。

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

首先,在轮播图类中界说 render() 烘托办法,根据图片列表回来一段包括 ul 列表元素的 HTML 代码。

render() {
  const images = this.options.images;
  const content = images.map(image => `
      <li class="slider-list__item">
        <img src="https://juejin.im/post/7191902215370932282/${image}"/>
      </li>    
    `.trim());
  return `<ul>${content.join('')}</ul>`;
}

然后,在轮播图类中界说一个注册插件的办法。

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

然后,界说各个插件,插件中也要界说 render() 烘托办法,用来回来该插件对应的 HTML 代码,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 需求由 JavaScript 来烘托,所以需求手动先调用 this.slideTo(0) 烘托第一张图片。

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

这样,咱们就能够经过注册插件 registerPlugins 来运用各种插件,假如咱们要去除底部小圆点的鼠标悬浮切换图片的功用,只需求调用 registerPlugins 办法时,不将 pluginController 传入即可,同时底部小圆点也会在页面中去除。

const slider = new Slider('my-slider', {
  images: ['https://p5.ssl.qhimg.com/t0119c74624763dd070.png', 'https://www.6hu.cc/files/2023/02/1676798058-22713ac4835c281.jpg',
    'https://www.6hu.cc/files/2023/02/1676798062-49611ffbd3f0c12.jpg', 'https://www.6hu.cc/files/2023/02/1676798066-ef0803c33dd7eda.jpg'
  ],
  cycle: 3000
});
slider.registerPlugins(/*pluginController, */pluginPrevious, pluginNext);
slider.start();

至此,扩展性有了,可是可复用性还不行,咱们持续重构,将组件笼统成一个组件框架,进步组件的复用性。

写好 JS 的原则 组件封装、过程抽象 | 青训营笔记

版别四

将通用的组件模型笼统出来,界说成一个通用组件类,这样咱们假如有多个组件,就能够在各个组件中承继和复用这个组件模型。

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 = `${this.options.name}__plugin`;
      pluginContainer.innerHTML = plugin.render(this.options.data);
      this.container.appendChild(pluginContainer);
      plugin.action(this);
    });
  }
  render(data) {
    /* 笼统办法 */
    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);
  }
}

其他完成和版别三相同,这样咱们就完成了一个小型的组件框架(还能够解耦 CSS)。、

写好 JS 的原则 组件封装、过程抽象 | 青训营笔记

总结

  1. 完成一个组件的过程:结构规划、展现效果、行为规划。
  2. 组件规划的准则:正确性、封装性、扩展性、复用性。
  3. 组件重构的准则:插件化(扩展性)、模板化(易于扩展)、笼统化(复用性)。