插件封装

问题:如何封装一个轮播图插件。

先来看代码,代码如下:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>封装轮播图组件</title>
        <style>
            #my-slider{
              position: relative;
              display: inline-block;
              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;
            }
            
            .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%;
              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 .5s;
            }
            
            .slide-list__previous {
              left: 0;
            }
            
            .slide-list__next {
              right: 0;
            }
            
            #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);
              }
            }
            
            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>

封装-API

我们可以创建一个 Slider 类,来封装这个组件并暴露一些 API 供用户使用。

class Slider {
  getSelectedItem() {}
  getSelectedItemIndex() {}
  slideTo(idx) {}
  slideNext() {}
  slidePrevious() {}
}

这样用户在使用组件的时候只需要调用暴露的 API 就可以了~

控制流

另外我们还需要在 API 的基础上添加控制流,让轮播图既可以自动播放,也可以手动控制。下面是事件相关代码:

constructor(id, cycle = 3000) {
    // ...
    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';
    })
    // ...
    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)
    }
    // ...
}

插件化

至此,这个轮播图组件已经是一个能用的组件了,但还不够灵活和自由,比如我们要更换左右的切换按钮为其他东西,或者是移除掉它,那么这时我们不仅需要修改轮播图的代码,而且还需要同时修改 HTML、CSS 和 JavaScript 三者。

class Slider {
    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);
        });
    }
}

const pluginController = {};
const pluginPrevious = {};
const pluginNext = {};

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

我们单独编写左右的控制按钮和小圆点,然后作为插件注入到轮播图中就可以实现对应的功能,当我们不在需要这个功能的时候,只需要修改注册这一部分的代码。而当我们需要修改某个插件的功能的时候,也不需要去修改轮播图的代码,而是修改插件的代码。

模板化

在插件化的基础上,我们还可以将 HTML 模板化,也就是通过 JavaScript 来渲染 HTML 元素,使得组件更加易于扩展。如下面采用 render 函数来渲染 HTML:

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>`;
    }
    // ...
}

抽象化

将组件的通用模型给抽取出来,定义一个 Component 类,拥有所有组件通用的部分,同时 Component 处理插件的注册与渲染逻辑(因为这是所有 Component 都有的部分)。

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 ''
    }
}

在以后如果需要编写一个新的组件,按照这个 Component 类实现即可。

最终组件化代码:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>组件化轮播图插件</title>
        <style>
            #my-slider{
              position: relative;
              display: inline-block;
              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;
            }
            
            .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%;
              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 .5s;
            }
            
            .slide-list__previous {
              left: 0;
            }
            
            .slide-list__next {
              right: 0;
            }
            
            #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 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>

 

总结:

  • 组件设计的原则:封装性、正确性、扩展性、复用性
  • 实现组件的步骤:结构设计、展现效果、行为设计
  • 三次重构
    1. 插件化
    2. 模板化
    3. 抽象化(组件框架)
posted @ 2022-07-29 23:07  grigeorge  阅读(117)  评论(0编辑  收藏  举报