封装、插件化、模板化、抽象化思想 | 青训营

134 阅读9分钟

看了本次 JavaScript 编码原则,让我受益匪浅,想想之前自己刚开始利用 js 写轮播图时的惨状,故想借此写写本次课程组件封装、插件化、模板化、抽象化的笔记,加深和巩固自己组件封装、插件化、模板化、抽象化的思想!

tips:本文以轮播图为例进行阐述。

无封装思想

一、实现轮播图代码演示(无封装版)

ps:之前写轮播图的时候真的完全没有封装思想

二、无封装写法的缺点

以上代码实现了一个简单的轮播图功能,但在封装化思想上存在一些缺点:

  1. 缺乏模块化:整个轮播图的逻辑被直接写在 <script> 标签中,没有进行合适的模块化封装。这使得代码的可维护性和可扩展性较差,很难在其他页面或项目中复用该功能。

  2. 存在全局变量:代码中使用了许多全局变量,如 boxTargetaOneaTwospotsTargetsliderTarget 等。全局变量容易造成变量名冲突和命名污染,增加了代码的不稳定性。

  3. 代码重复和冗余:每次更改轮播图照片数量,都需要更新 css 样式和 js 方法中的相关计算,这导致了代码的重复和冗余。当轮播图中的照片数量很多时,手动更新和计算将变得非常繁琐和容易出错。

  4. 维护困难:由于 css 和 js 相互依赖,当调整了轮播图的照片数量后,必须同时更新两者,否则可能导致布局错乱或逻辑错误。这增加了维护代码的难度,并且容易出现错误,尤其在多人协作或长期维护的情况下。

image.png

image.png

封装和插件化写法

一、基本封装思想:

基本封装思想架构:

  1. 结构设计:html
<div class="slider-list" id="my-slider">
    <ul>
        <li class="slider-list__item--selected">
            <img src="https://p5.ssl.qhimg.com/t0119c74624763dd070.png" alt="">
        </li>
        <li class="slider-list__item">
            <img src="https://p5.ssl.qhimg.com/t01adbe3351db853eb3.png" alt="">
        </li>
        <li class="slider-list__item">
            <img src="https://p5.ssl.qhimg.com/t01645cd5ba0c3b60cb.png" alt="">
        </li>
        <li class="slider-list__item">
            <img src="https://p5.ssl.qhimg.com/t01331ac159b58f5478.png" alt="">
        </li>
    </ul>
    <a class="slider-list__next"></a>
    <a class="slider-list__pervious"></a>
    <div class="slider-list__control">
        <span class="slider-list__control-buttons--selected"></span>
        <span class="slider-list__control-buttons"></span>
        <span class="slider-list__control-buttons"></span>
        <span class="slider-list__control-buttons"></span>
    </div> 
   </div>
  1. 展现效果:css
  1. 行为设计:js
  • API(功能):API设计应保证原子操作,职责单一,满足灵活性
  • Event控制流行为:使用自定义事件 解耦
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(".slider-list__control");
    if (controller) {
      const buttons = this.container.querySelectorAll(
        ".slider-list__control-buttons, .slider-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(
          ".slider-list__control-buttons--selected"
        );
        if (selected) {
          selected.className = "slider-list__control-buttons";
        }
        buttons[idx].className = "slider-list__control-buttons--selected";
      });
    }

    // 上一张
    const previous = this.container.querySelector(".slider-list__pervious");
    if (previous) {
      previous.addEventListener("click", (evt) => {
        this.stop();
        this.slidePrevious();
        this.start();
        evt.preventDefault();
      });
    }

    // 下一张
    const next = this.container.querySelector(".slider-list__next");
    if (next) {
      next.addEventListener("click", (evt) => {
        this.stop();
        this.slideNext();
        this.start();
        evt.preventDefault();
      });
    }
  }

  // 获取被选中的节点
  getSelectedItem() {
    const selected = this.container.querySelector(
      ".slider-list__item--selected"
    );
    return selected;
  }

  // 获取被选中节点的下标
  getSelectedItemIndex() {
    // 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";
    }

    // 开发自定义事件
    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);
  }

  // 开启定时器
  start() {
    this.stop();
    this._timer = setInterval(() => this.slideNext(), this.cycle);
  }

  // 关闭定时器
  stop() {
    clearInterval(this._timer);
  }
}

let slider = new Slider("my-slider");
slider.start();[jcode](https://code.juejin.cn/pen/7262278134077554728)

基本封装思想的优缺点

  1. 优点:上述代码采用了面向对象的封装思想,将轮播图功能封装成了 Slider 类。这种封装思想带来了以下好处:
  • 更好的代码组织和可读性代码通过封装形成了一个独立的类,将相关的属性和方法放在一起,使代码结构更加清晰和易于理解。可以更容易地找到和修改相关逻辑,同时也便于其他开发人员理解和使用。

  • 提高代码的可维护性:通过封装成类,可以更方便地对轮播图的功能进行修改和扩展。相关的代码逻辑和属性被封装在一个地方,当需要添加新功能或修改现有功能时,只需要在类定义中进行修改,而不需要在多个地方进行修改,降低了维护代码的复杂度。

  • 提供可复用的组件:通过封装成类,可以创建多个独立的轮播图实例,每个实例具有各自的状态和行为。这样可以在同一页面中使用多个轮播图,或在其他页面和项目中复用该轮播图组件,提高了代码的可复用性和扩展性。

  • 封装了对外接口:通过构造函数和公共方法,提供了对外的接口来控制轮播图的行为。其他开发人员只需要实例化 Slider 类,并通过公共方法来启动、停止、切换轮播图,而无需关心具体的实现细节,提高了代码的易用性。

  1. 缺点:上述代码尽管使用了面向对象的封装思想,但是如果不需要某一需求或者功能时,仍需要在上述代码进行大量改动。比如如果不需要上一张和下一张的切换,需要更改的地方也是和别的方法混合的,改动起来也是比较麻烦的!

二、优化封装思想-继续解耦(插件化)

优化封装思想-插件化架构

image.png

  1. 将控制元素抽取成插件
// 圆点控制器
const pluginController = (slider) => {
  const controller = slider.container.querySelector(".slider-list__control");
  if (controller) {
    const buttons = controller.querySelectorAll(
      ".slider-list__control-buttons, .slider-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(
        ".slider-list__control-buttons--selected"
      );
      if (selected) {
        selected.className = "slider-list__control-buttons";
      }
      buttons[idx].className = "slider-list__control-buttons--selected";
    });
  }
};

// 上一张
const pluginPrevious = (slider) => {
  const previous = slider.container.querySelector(".slider-list__pervious");
  if (previous) {
    previous.addEventListener("click", (evt) => {
      slider.stop();
      slider.slidePrevious();
      slider.start();
      evt.preventDefault();
    });
  }
};

// 下一张
const pluginNext = (slider) => {
  const next = slider.container.querySelector(".slider-list__next");
  if (next) {
    next.addEventListener("click", (evt) => {
      slider.stop();
      slider.slideNext();
      slider.start();
      evt.preventDefault();
    });
  }
};
  1. 插件与组件之间通过依赖注入方式建立联系
let slider = new Slider("my-slider");
slider.registerPlugins(pluginController, pluginPrevious, pluginNext);
slider.start();

优化封装思想-插件化的优缺点:

  1. 优点
  • 解耦和模块化将控制元素抽取成插件后,可以将其作为独立的模块进行开发和维护。插件可以通过相应的接口来进行交互,组件只需要依赖插件提供的接口来使用其功能,而不需要关心插件的具体实现。这使得组件与插件之间的关系变得松耦合,方便独立调试和测试,同时也增强了代码的可维护性和可拓展性。

  • 可复用性和扩展性通过使用依赖注入的方式,组件可以动态地注入不同的插件实例来实现不同的功能。这样可以使得组件的功能更加灵活和可配置,可以根据具体需求选择不同插件的实现,而无需对组件的代码进行修改。这种设计思路提供了更高的可复用性和扩展性,使得组件可以应对不同的场景和需求。

  • 单一职责原则将控制元素抽取成插件后,组件只需要专注于自身的功能实现,而将控制元素的管理和操作交由插件来处理。这符合单一职责原则,使得组件和插件的功能边界更加清晰和易于维护。代码的变动和修复也更加集中和可控,减少了潜在的bug和错误。

  • 灵活性和可定制性通过插件的方式,可以根据需求选择不同的插件或为特定需求开发新的插件。这使得组件的功能可以根据具体场景和用户需求进行定制,提升了整体系统的灵活性和可定制性。同时,插件可以单独进行更新和升级,不会影响到组件的其他部分,提高了系统的可维护性和可扩展性。

  1. 缺点:在剔除某一特定需求时,虽然可以直接删除插件化的 js 功能代码,但是 html 仍需找到相应的代码进行剔除,仍有一定缺陷

三、实现轮播图代码演示(封装和插件化版)

重构-模板化、组织框架-抽象化

一、重构-模板化

  1. 将 HTML 模板化,即将每个功能的 html 代码抽离出来,更易于扩展和管理。
<!-- 主页内容 -->
<div class="slider-list" id="my-slider"></div>
// 功能 html 模板化

class Slider {
  //...
  
  render() {
    const images = this.options.images;
    const content = images.map((image) =>
      `
      <li class="slider-list__item">
        <img src="${image}" alt="">
      </li>
       `.trim()
    );

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

// 圆点控制器
const pluginController = {
  render(images) {
    return `
    <div class="slider-list__control">
        ${images.map((image, i) =>
          `
           <span class="slider-list__control-buttons${i === 0 ? "--selected" : ""}"></span>
          `
        ).join("")}
    </div> 
    `.trim();
  },
  action(slider) {
    // ....
  },
};

// 上一张
const pluginPrevious = {
  render() {
    return `
    <a class="slider-list__pervious"></a>
    `;
  },
  action(slider) {
    // ....
  },
};

// 下一张
const pluginNext = {
  render() {
    return `
    <a class="slider-list__next"></a>
    `;
  },
  action(slider) {
    // ....
  },
};

二、组织框架-抽象

  1. 将组件通用模型抽象出来
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) {
    return "";
  }
}

// 继承组件通用模型
class Slider extends Component {
  //...
}

三、模板化抽象化后的 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 = `.${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);
  }

  render() {
    const data = this.options.data;
    const content = data.map((image) =>
      `
      <li class="slider-list__item">
        <img src="${image}" alt="">
      </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.data);
      this.container.appendChild(pluginContainer);
      plugin.action(this);
    });
  }

  // 获取被选中的节点
  getSelectedItem() {
    const selected = this.container.querySelector(
      ".slider-list__item--selected"
    );
    return selected;
  }

  // 获取被选中节点的下标
  getSelectedItemIndex() {
    // 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";
    }

    // 开发自定义事件
    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();
    console.log(this.items);
    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(data) {
    return `
    <div class="slider-list__control">
        ${data
          .map(
            (image, i) =>
              `
           <span class="slider-list__control-buttons${
             i === 0 ? "--selected" : ""
           }"></span>
          `
          )
          .join("")}
    </div> 
    `.trim();
  },
  action(slider) {
    const controller = slider.container.querySelector(".slider-list__control");
    if (controller) {
      const buttons = controller.querySelectorAll(
        ".slider-list__control-buttons, .slider-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(
          ".slider-list__control-buttons--selected"
        );
        if (selected) {
          selected.className = "slider-list__control-buttons";
        }
        buttons[idx].className = "slider-list__control-buttons--selected";
      });
    }
  },
};

// 上一张
const pluginPrevious = {
  render() {
    return `
    <a class="slider-list__pervious"></a>
    `;
  },
  action(slider) {
    const previous = slider.container.querySelector(".slider-list__pervious");
    if (previous) {
      previous.addEventListener("click", (evt) => {
        slider.stop();
        slider.slidePrevious();
        slider.start();
        evt.preventDefault();
      });
    }
  },
};

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

let slider = new Slider("my-slider", {
  data: [
    "https://p5.ssl.qhimg.com/t0119c74624763dd070.png",
    "https://p5.ssl.qhimg.com/t01adbe3351db853eb3.png",
    "https://p5.ssl.qhimg.com/t01645cd5ba0c3b60cb.png",
    "https://p5.ssl.qhimg.com/t01331ac159b58f5478.png",
  ],
  cycle: 3000,
});
slider.registerPlugins(pluginController, pluginPrevious, pluginNext);
slider.start();

结语

  1. 组件设计的原则:封装性、正确性、扩展性、复用性
  2. 实现组件的步骤:结构设计、展现效果、行为设计
  3. 三次重构:(1)插件化 (2)模板化 (3)抽象化(组件框架)

~0v0~ 以上是有关封装、插件化、模板化、抽象化思想的笔记内容,如有错,还请指正!