[字节青训营]前端方向Day6-前端入门 - 基础语言篇 - 如何写好JavaScript-组件封装-手撕简单的轮播图(4) | 豆包MarsCode AI刷题

57 阅读6分钟

源代码链接[code.juejin.cn/pen/7108191…]

image.png

  • HTML初始结构
    在HTML的<body>中只有一个空的<div id="my-slider" class="slider-list">元素。页面上的图片、按钮等内容完全通过JavaScript动态生成,不需要在HTML中事先编写结构代码。

image.png

  • Slider 类的基本结构与职责
    Slider类负责管理整个幻灯片的基本结构和功能,包括将图片渲染到页面、图片的切换、自动轮播等。构造方法保持简洁,实例化时只需传入容器id和参数opts,然后通过render()方法生成图片的HTML并插入页面。这样HTML结构的生成交由Slider类处理,初始化过程更清晰。

  • 插件的定义与作用
    通过插件的设计,实现了代码的模块化和复用性。每个插件本质上就是将某些功能从Slider类中分离出来,插件都包含以下两个方法:

    • render(): 用于生成该插件的HTML结构,并返回一段HTML字符串,这些内容将插入到页面上。
    • action(): 定义插件的具体功能和交互逻辑(比如点击、鼠标悬停等事件的绑定),用于控制插件的行为。
  • Slider类的插件注册
    插件注册方法registerPlugins()写在Slider类中,通过扩展运算符...一次性接收并注册多个插件。每次注册一个插件时,Slider类会创建一个<div>来包裹插件内容,将其插入到主容器中,同时调用action()为插件设置交互功能。插件的注册过程使用同一方法,形成统一的注册机制。

  • 实现插件和主功能的分离
    通过这种方式,核心功能与扩展功能完全分离——Slider类专注于幻灯片的核心逻辑,各种按钮、控制功能通过插件来实现。这样,如果需要新增或调整插件,只需添加或修改相应插件的代码,不影响Slider类的核心逻辑。最终,代码更加模块化、简洁,同时提升了代码的复用性和可维护性。

class Slider { // 定义一个Slider类
  constructor(id, opts = { images: [], cycle: 3000 }) { // 定义一个构造函数,参数为id和opts。在opts里有两个变量,images,cycle。
    this.container = document.getElementById(id); // 根据id获取DOM元素
    this.options = opts; // 获取opts参数,用options接收
    this.container.innerHTML = this.render(); // 将render()函数渲染到HTML上
    this.items = this.container.querySelectorAll(
      ".slider-list__item, .slider-list__item--selected",
    ); // 把所有照片取出来
    this.cycle = opts.cycle || 3000; // 取出参数opts里面的cycle变量,和3000进行“或”运算。如果不传的话默认值为3000。
    this.slideTo(0); // 默认是第一张照片
  }
  render() { // 渲染方法。JSX格式
    const images = this.options.images; // 从构造方法定义的options参数中,取出images变量
    // images是所有的图片,进行map遍历,每一个图片定义为image,然后写成HTML形式。
    // trim方法  去除前后空白,确保生成的 HTML 字符串不受多余空白影响。
    const content = images.map((image) =>
      ` 
        <li class="slider-list__item">
          <img src="${image}"/>
        </li>    
      `.trim()
    );
    // .join("") 将 content 数组(包含各个 li 元素的字符串片段)合并成一个整体的字符串,并且不加任何分隔符。
    return `<ul>${content.join("")}</ul>`; // 把定义的这些li元素插入到ul中
  }
  registerPlugins(...plugins) { // 注册插件,可以同时进行多个插件的注册,把要注册的插件写在一起就可以。
    plugins.forEach((plugin) => {
      const pluginContainer = document.createElement("div"); // 创建一个div元素,用来存放插件
      pluginContainer.className = "slider-list__plugin"; // 设置div的类名
      pluginContainer.innerHTML = plugin.render(this.options.images); // 插进进行渲染,插入到创建的div元素中
      this.container.appendChild(pluginContainer); // 给最大的容器加入一个子元素。

      plugin.action(this); // 调用插件的action函数
    });
  }
  getSelectedItem() { // 获取当前显示的图片元素
    const selected = this.container.querySelector(
      ".slider-list__item--selected",
    );
    return selected;
  }
  getSelectedItemIndex() { // 获取当前显示的图片元素的索引
    return Array.from(this.items).indexOf(this.getSelectedItem());
  }
  slideTo(idx) { // 切换到指定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) { // 事件监听函数。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) { // 定义一个action函数 转换 NodeList 为数组,便于查找事件目标的索引
    const controller = slider.container.querySelector(".slide-list__control"); // 获取按钮组

    if (controller) { // 如果获取到
      const buttons = controller.querySelectorAll( // 拿到所有的按钮,这是一个NodeList对象
        ".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) => { // 监听自定义事件slide 如果自定义事件slide触发,那么就把idx对于图片的按钮设置成选中
        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();

现在我们加入一个新的插件,插件是一个按钮,点击按钮可以随机的显示一张照片

  • 定义插件const pluginButton

  • render()函数 需要在页面中显示一个按钮,所以

    return '<button class="pluginButton1">随机显示一张照片</button>'

    有class的原因是我们需要进行点击事件的监听,需要通过class来找到需要监听事件的元素。

  • action()函数 需要一个参数slider,我们在插件中来对slider进行一系列操作

    通过class找到我们要监听事件的元素.

    const pluginButton1 = slider.container.querySeletor('.pluginButton1')

    现在我们取到了这个按钮元素,我们给这个按钮元素添加监听事件。

    pluginButton1.addEventListener('click',()=>{})

    现在定义好监听事件的框架了,我们在{}里面写我们需要实现的功能。随机切换照片。

    那么我们肯定就需要一个idx,切换到哪张照片。

    通过 slider.items.length 获取照片总数,然后 * Math.random() 可以得到一个有效的数,但是有可能不是整数,所以我们对结果再进行Math.floor() 向下取整。

    然后我们停下当前的自动切换,slideTo(idx),切换到idx,然后再开始自动切换。

    完整代码如下:

const pluginButton = {
  render(){
    return '<button class="pluginButton1">随机显示一张照片</button>'
  },
  action(slider){
    const pluginButton1 = slider.container.querySelector(".pluginButton1");
    pluginButton1.addEventListener('click',()=>{
      slider.stop();
      idx = Math.floor(slider.items.length * Math.random())
      slider.slideTo(idx);
      slider.start();
    })
  }
}

  • 现在就只需要最后一步了,注册我们新创建的插件。

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

image.png `

最后一种实现形式是在这一种的基础上,把Slider变得更为抽象,就不详细的介绍了,对应的代码在code.juejin.cn/pen/7108185…

image.png