写好JS的一些原则

176 阅读13分钟

这是我参与「第四届青训营 」笔记创作活动的的第6天

写好JS的一些原则(1) | 青训营笔记

image.png

TIPS:函数式编程是现代编程语言一种非常重要的编程范式

1.结构、样式和行为各司其职

案例1:页面日间夜间模式切换

image.png

  • 让JS直接去控制CSS
  • 后续CSS样式跟随项目需求发生变化的时候,会需要修改JS代码
  • 后续程序员很难直接理解需求

image.png

  • JS只控制class,做到了各司其职
  • 是专业前端工程师应该坚持的风格

image.png 72fc2e02c41504d833a357f8acdf914.png

  • 纯CSS切换的事情CSS单独就可以完成,所以其实不需要JS参与
  • 这里单击label就可以切换checkbox,label已经和checkbox绑定,这也是JS的特性

image.png

总结

  • HTML/CSS/JS 各司其责
  • 应当避免不必要的由 JS 直接操作样式
  • 可以用 class 来表示状态
  • 纯展示类交互寻求零 JS 方案

2.组件封装

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

案例:电商轮播图

解决方案1

HTML部分

<div id="my-slider" class="slider-list">
  <ul>
    <li class="slider-list__item--selected">
      <img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b1ab54a94ea44449854f721ed05546bd~tplv-k3u1fbpfcp-zoom-1.image"/>
    </li>
    <li class="slider-list__item">
      <img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dc5098a647fa400f9a89825efcbfce90~tplv-k3u1fbpfcp-zoom-1.image"/>
    </li>
    <li class="slider-list__item">
      <img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2aa22af0e6af4a858af8565a4ae3fe58~tplv-k3u1fbpfcp-zoom-1.image"/>
    </li>
    <li class="slider-list__item">
      <img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/500e41dabcb746569f1fc47d77f75613~tplv-k3u1fbpfcp-zoom-1.image"/>
    </li>
  </ul>
</div>
复制代码

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

4602613a074e2226ddbe9f56ebf21ee.png

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

A Little Diversion

关于opacity

  • opacity 属性指定了一个元素的不透明度

  • 换言之,opacity 属性指定了一个元素后面的背景的被覆盖程度。

    释义
    0元素完全透明 (即元素不可见).
    任何一个位于 0.0-1.0 之间的 <number>元素半透明 (即元素后面的背景可见).
    1元素完全不透明 (即元素后面的背景不可见).

关于transition

CSS transitions 提供了一种在更改 CSS 属性时控制动画速度的方法。其可以让属性变化成为一个持续一段时间的过程,而不是立即生效的。比如,将一个元素的颜色从白色改为黑色,通常这个改变是立即生效的,使用 CSS transitions 后该元素的颜色将逐渐从白色变为黑色,按照一定的曲线速率变化。这个过程可以自定义。

CSS transitions 可以决定:

  • 哪些属性发生动画效果 (明确地列出这些属性)
  • 何时开始 (设置 delay)
  • 持续多久 (设置 duration)
  • 如何动画 (定义timing function,比如匀速地或先快后慢)

标准语法

div { transition: <property> <duration> <timing-function> <delay>; }

常见用例:当鼠标悬停在菜单上时高亮菜单,使用 transition 效果更佳。

关于修饰符

块、元素、修饰符方法(通常称为BEM——Block, Element, Modifier methodology )是HTML和CSS中常用的类命名约定。它由Yandex团队开发,目的是帮助开发人员更好地理解给定项目中HTML和CSS之间的关系。

在这种CSS方法中

  • 块是新组件的顶层抽象,例如按钮:.btn{}。这个块应该被视为父块。
  • 子项或元素可以放在内部,它们由两个下划线在块名后面表示,比如.btn__price{}
  • 最后,修饰符可以对块进行操作,这样我们就可以对特定组件进行主题化或样式化,而不会对完全不相关的模块造成影响。这是通过在块的名称后面添加两个连字符来完成的,就像btn--orange一样。

End of Diversion

JS部分

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
  }
  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';
    }
  }
  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');
slider.slideTo(3);
复制代码

A Little Diversion

1.querySelectorAll

  • document.querySelectorAll() will always return a NodeList, of 0 or more DOM elements.

  • NodeList is a collection of DOM elements. This means that it is a variable that contains several DOM elements.

  • The NodeList object is similar to an array, but it's not an array.

  • A NodeList is not an array. However, there are some similarities:

    • they both have a length property (you can access it with .length)
    • you can access items at a specific index with the square brackets. For example, [0] and [1].
    • you can iterate through both of them with .forEach().
const paragraphs = document.querySelectorAll("p");

console.log(paragraphs.length); // 2
console.log(paragraphs[0]); // <p>First paragraph</p>
console.log(paragraphs[1]); // <p>Second paragraph</p>

paragraphs.forEach(paragraph => {
    console.log(paragraph); // logs every paragraph element one by one
});
复制代码

But You cannot call .filter on a NodeList

// ❌ this does NOT work
document.querySelectorAll(".link").filter();
复制代码

How to convert the NodeList into an Array

const divs = document.querySelectorAll("div"); // NodeList
const items = [...divs]; // Array
复制代码

use the array spread syntax (...) which spreads every single item of the NodeList, into a new array

const items = [...document.querySelectorAll("div")]; // Array
复制代码

2.Array.from()

Array.from()  方法对一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例

Array.from 是 ES6 中引入的另一种数组工厂方法

它期望一个可迭代的或类似数组的对象iterable or array-like object作为它的第一个参数,并返回一个包含该对象元素的新数组

使用可迭代参数,Array.from(iterable) 就像扩展运算符 [...iterable] 一样工作,这也是一种复制数组的简单方法

let copy = Array.from(original);
复制代码

类数组对象array-like object是具有数值长度属性numeric length property的非数组对象non-array objects,并且其值存储在名称恰好是整数的属性中。 Array.from()也接受第二个可选参数。

如果您将一个函数作为第二个参数传递,那么在构建新数组时,源对象中的每个元素都将传递给您指定的函数,函数的返回值将存储在数组中,而不是原始值。

与数组的map()方法非常相似,但在构建数组时执行映射比构建数组然后将其映射到另一个新数组更有效

but it is more efficient to perform the mapping while the array is being built than it is to build the array and then map it to another new array

3.IndexOf()

indexOf() 方法返回在数组中可以找到一个给定元素的第一个索引,如果不存在,则返回-1。

const beasts = ['ant', 'bison', 'camel', 'duck', 'bison'];

console.log(beasts.indexOf('bison'));
// expected output: 1

// start from index 2
console.log(beasts.indexOf('bison', 2));
// expected output: 4

console.log(beasts.indexOf('giraffe'));
// expected output: -1
复制代码

End of Diversion

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();
        // 开始自动播放
      });
      
      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);
   // 通过setInterval设置自动播放和停止
  }
}

const slider = new Slider('my-slider');
slider.start();
复制代码

A Little Diversion

1.Mouse事件

The mouseover event is fired at an Element when a pointing device (such as a mouse or trackpad) is used to move the cursor onto the element or one of its child elements.

The mouseout event is fired at an Element when a pointing device (usually a mouse) is used to move the cursor so that it is no longer contained within the element or one of its children.

2.自定义事件

事件本质是一种通信方式,是一种消息,只有在多对象多模块时,才有可能需要使用事件进行通信。在多模块化开发时,可以使用自定义事件进行模块间通信。

实现自定义事件的两种主要方式是:

  • JS 原生的 Event() 构造函数
  • CustomEvent() 构造函数

结合Vue的components相互之间是怎么联系的理解

这二者的区别是:

  • Event() 适合创建简单的自定义事件
  • 而 CustomEvent() 支持参数传递的自定义事件,它支持 detail 参数,作为事件中需要被传递的数据,并在 EventListener 获取

使用场景: 事件本质是一种消息,事件模式本质上是观察者模式的实现,即能用观察者模式的地方,自然也能用事件模式

  1. 场景1:单个目标对象发生改变,需要通知多个观察者一同改变。  例如:微博列表页点击关注,粉丝列表页和微博首页推荐同时发生变化
  2. 场景2:解耦多模块开协作。  例如:1.流程控制(Index.js)\2.产品设计(Production.js)\3.UI设计(Design.js)\4.程序员开发(Develop.js)这四个模块分别独立开发,前一个完成了才能进行下一个
    const detail = {index: idx}
    const event = new CustomEvent('slide', {bubbles:true, detail})
    this.container.dispatchEvent(event)
    //我们自定义了slide事件
    
    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';
      })
    }
复制代码

这里为何要引入slide事件,是因为用户点击任意小圆点之后,有两个地方都需要观察到,第一个是图片本身的class状态需要切换,第二个是小圆点按钮的class状态也需要进行切换

Elements can listen for events that haven't been created yet

<form>
  <textarea></textarea>
</form>
const form = document.querySelector('form');
const textarea = document.querySelector('textarea');

form.addEventListener('awesome', (e) => console.log(e.detail.text()));

textarea.addEventListener('input', function() {
  // Create and dispatch/trigger an event on the fly
  // Note: Optionally, we've also leveraged the "function expression" (instead of the "arrow function expression") so "this" will represent the element
  this.dispatchEvent(new CustomEvent('awesome', { bubbles: true, detail: { text: () => textarea.value } }))
});
复制代码

End of Diversion

总结:组件封装的基本方法

  • 结构设计

  • 展现效果

  • 行为设计

    1. API (功能)
    2. Event (控制流)

改进1:插件化

首先可以观察到,我们目前的构造函数太复杂了,而且占据了较大的篇幅

在未来如果我们在新的项目里面并不需要这左右按钮或者这五个圆点,那么我们想要的效果是直接在HTML中直接删除或者换掉这部分HTML代码,而不需要去动JS中的核心逻辑代码,因此我们需要把图片轮播和这几个按钮的主体部分分离出来,也就是将左右两个按钮和五个圆点解耦出来

解耦

  • 将控制元素抽取成插件
  • 插件与组件之间通过依赖注入方式建立联系
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));
  }
  //class里有regisrerPlugins方法
  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();
复制代码

constructor中把控制圆点和上下页移动轮播图的功能抽离出来成为一个个插件函数,然后再在组件里面添加一个注册函数registerPlugins()API,轮播图调用时候,只需要把每个插件函数通过这个函数注册进去就行了,去除就是把插件函数注销掉就行了

注册函数API:

  registerPlugins(...plugins){
    plugins.forEach(plugin => plugin(this));
  }
  //class里有regisrerPlugins方法
  //典型的依赖注入
复制代码

把控制圆点按钮和上下页的插件函数分离成插件

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();
    });
  }  
}
复制代码
  • 调用的时候,就只需要去注册需要的插件即可
  • 不需要的则注释掉相应的插件,然后从HTML中删除掉相应的结构代码即可
  • 如果需要扩展新的插件,则只需要重新写好插件然后再注册到这个组件里面去
slider.registerPlugins(pluginController, pluginPrevious, pluginNext);
slider.registerPlugins(/*pluginController, */pluginPrevious, pluginNext);//注册圆点, 上一页,下一页事件
复制代码

改进2:模板化

数据驱动,通过JS来生成HTML模板,而不要把HTML写死 如果我们以后需要更多的图呢,如果我们需要删掉一些按钮呢

image.png

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();
复制代码

我们通过render方法和innerHTMl来生成图片部分的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>`;
复制代码

现在明确通过render()来生成plugin部分的HTML代码,通过action()来初始化plugin的行为

 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 = {
 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';
     });
   }    
 }
};
复制代码

在slider构造函数中,我们只要把图片作为参数传进去就可以了

 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});
复制代码

这么做的好处是:如果我们不需要那个小圆点,我们直接注册掉插件,那么小圆点的HTML模板和它的行为功能会一起消息,这样就会非常灵活

改进3:抽象化

  • 将组件通用模型抽象出来

image.png

  • 可以看到模板化中的registerPlugins(...plugins)、render()方法是通用的组件,可以基于这些抽象出一个componenet类
  • Slider通过继承的方式继承该类
  • 这个componenet类体系相对完整,可以相当于是一个很小的组件框架

825c99cb5e3934edb67e40c4cf3ce1c.png

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();
复制代码

麻雀虽小,五脏俱全的组件框架

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 ''
  }
}
复制代码

这里还用到了抽象方法

总结

  • 组件设计的原则:封装性、正确性、扩展性、复用性

  • 实现组件的步骤:结构设计、展现效果、行为设计

  • 三次重构

    1. 插件化
    2. 模板化
    3. 抽象化

思考改进空间

  1. 抽象化方面:我们的组件和插件是分开的,未来可以考虑子组件,将子组件作为父组件的插件来使用
  2. 模板化方面:只考虑了HTML的模板化,由插件来生成HTML模板并控制行为,但我们没有去做CSS的模板化,CSS还是写死的;很多组件库和组件框架里面有CSS-in-JS方案,把CSS也作做了组件化