【字节前端青训营】跟着月影学JavaScript—— 前端代码优化三大原则之组件封装,用原生JS写一个轮播图并封装

前言

组件封装对于学过React或者Vue框架的同学应该都不陌生,就我个人而言我之前的原生JS基础并不太是太好,一开始接触前端是因为参加学校的实验室做React Native项目,我是稍微了解一下原生JS之后就直接学React了,对于React组件的封装已经是比较熟悉了,而封装原生JS组件对我来说还是很新鲜的。上完这次课,自己跟着把代码敲了一遍真的是受益匪浅,让我对框架的理解又加深了一步,希望大家有时间的话都好好做一下这个轮播图组件,很棒。

什么是组件封装

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

深入探讨前端组件化开发
这篇文章里概况的介绍了什么是组件,组件化的好处,如何设计组件等等,如果对这些概念还不是很清楚的可以先看看这篇文章,接下来我们就上个小例子。

轮播图小例子

效果

轮播图.gif

线上预览地址:

https://code.h5jun.com/vata/edit?html,css,js,output

分析

1.结构设计:HTML

轮播图是一个典型的列表结构,我们可以使用无序列表<ul>元素来实现

2.表现:CSS

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

3.行为:JS

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

3.行为:控制流

  • 使用自定义事件来解耦

版本1 (API)

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

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      #my-slider {
        position: relative;
        width: 790px;
      }
      .slider-list ul {
        list-style: none;
        position: relative;
        padding: 0;
        margin: 0;
      }
      .slider-list__item,
      .slider-list__item--selected {
        /* 经典子绝父相,子元素利用绝对定位重叠在一起 */
        position: absolute;
        transition: opacity 1s;
        opacity: 0;
        text-align: center;
      }
      /* 这行上下opacity一个为0一个为1 意思是选中的不透明 未选中的透明 */
      .slider-list__item--selected {
        transition: opacity 1s;
        opacity: 1;
      }
    </style>
  </head>
  <body>
    <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>
    <script>
      // 轮播图类 里面封装一些api
      class Slider {
        constructor(id) {
          this.container = document.getElementById(id);
          this.items = this.container.querySelectorAll(
            ".slider-list__item, .slider-list__item--selected"
          );
        }
        // 获取选中的图片元素
        getSelectedItem() {
          const selected = this.container.querySelector(
            ".slider-list__item--selected"
          );
          return selected;
        }
        //获取选中图片的索引值,返回其在items数组中的位置
        getSelectedItemIndex() {
          // Array.from() 方法对一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例。
          // querySelectorAll返回NodeList 对象,NodeList 不是一个数组,是一个类似数组的对象(Like Array Object)。
          // 虽然 NodeList 不是一个数组,但是可以使用 forEach() 来迭代。你还可以使用 Array.from() 将其转换为数组。
          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);
        }
      }
      const slider = new Slider("my-slider");
      setInterval(() => {
        slider.slideNext();
      }, 1000);
    </script>
  </body>
</html>

版本2 (控制流)

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      #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;
      }
      /* 这行上下opacity一个为0一个为1 意思是选中的不透明 未选中的透明 */
      .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%; /*经典居中定位 绝对定位在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 0.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: ">";
      }

      /*四个小圆点的样式*/
      .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;
      }
    </style>
  </head>
  <body>
    <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>
    <script>
      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();
    </script>
  </body>
</html>

你以为到这里就结束啦???不不不,能优化的地方还有很多,我们接着往下看

插件化解耦JavaScript

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      #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;
      }

      /* 这行上下opacity一个为0一个为1 意思是选中的不透明 未选中的透明 */
      .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%;
        /*经典居中定位 绝对定位在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 0.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: ">";
      }

      /*四个小圆点的样式*/
      .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;
      }
    </style>
  </head>

  <body>
    <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>
    <script>
      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) {
          plugins.forEach((plugin) => plugin(this));
        }
        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);
        }
      }
      // 注意 这里把之前代码中的this都替换成了参数slider
      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";
          });
        }
      }

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

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

      const slider = new Slider("my-slider");
      slider.registerPlugins(pluginController, pluginPrevious, pluginNext);
      slider.start();
    </script>
  </body>
</html>

既然能解耦JavaScript那能不能给HTML解耦呢?当然可以啦,接下来我们将对代码优化的方式是进行HTML模板化

模板化解耦HTML

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      #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;
      }

      /* 这行上下opacity一个为0一个为1 意思是选中的不透明 未选中的透明 */
      .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%;
        /*经典居中定位 绝对定位在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 0.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: ">";
      }

      /*四个小圆点的样式*/
      .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;
      }
    </style>
  </head>

  <body>
    <div id="my-slider" class="slider-list"></div>
    <script>
      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) {
          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);
          });
        }
        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";
          }
          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() {
          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) {
          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();
            });
          }
        },
      };

      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();
    </script>
  </body>
</html>

现在再看这代码是不是有内味了,最后我们可以再进行一次优化来提高复用性。

组件框架

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      #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;
      }

      /* 这行上下opacity一个为0一个为1 意思是选中的不透明 未选中的透明 */
      .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%;
        /*经典居中定位 绝对定位在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 0.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: ">";
      }

      /*四个小圆点的样式*/
      .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;
      }
    </style>
  </head>

  <body>
    <div id="my-slider" class="slider-list"></div>
    <script>
      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: 3000,
      });

      slider.registerPlugins(pluginController, pluginPrevious, pluginNext);
      slider.start();
    </script>
  </body>
</html>

至此我们就优化完这个轮播图组件了,真正的实现了封装性正确性拓展性复用性

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值