是月影老师主讲的课,受益匪浅,特此记录

写好JS的一些原则

各司其责

HTML描述结构,CSS描述展现形式,JS描述行为

并不是单指代码位置上的分离,而是指本身承担的职责

应当避免不必要的由JS直接操作样式,可以用class来表示状态,纯展示类交互寻求零JS方案

例:写一段JS,控制网页切换深色模式

方案1. 在JS中直接修改样式(不便于后期维护)
方案2. 在JS中修改class名,而具体class内容的实现交给CSS(依然耦合)
方案3. 修改HTML结构,利用纯CSS实现。使用checkbox状态选择展示夜间或白天样式

组件封装

组件是指Web页面上抽出来一个个包含模板(HTML)、功能(JS)和样式(CSS)的单元

好的组件具备封装性、正确性、扩展性、复用性

例:封装一个轮播图

初步想法
  1. 结构设计:HTML

轮播图是一个典型的列表结构,可以使用

    实现

    <div>
      <ul>
        <li class="slider-list__item--selected"></li>
        <li class="slider-list__item"></li>
        <li class="slider-list__item"></li>
      </ul>
    </div>
    1. 表现:CSS

    使用CSS绝对定位将图片重叠在同一位置

    轮播图切换的状态使用修饰符(modifier)

    轮播图的切换动画使用CSS transition

    1. JS

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

    一个Slider类可以含有以下API:

    • getSelectedItem()
    • getSelectedItemIndex()
    • slideTo()
    • slideNext()
    • slidePrevious()
    • start()
    • end()
    处理状态耦合

    解耦的过程,就是将控制元素抽取成插件,插件与组件之间通过依赖注入的方式建立联系。

    插件化

    轮播图有时会由一排controller控制播放切换,但controller并非必须的,而且可能会存在形式的不同(如可以水平、竖直排放;或者对样式进行调整)因此对controller的设计是需要被解耦的。此时我们可以利用自定义事件:如果存在controller时,令其监听scroll事件,并实现相关控制api。这样轮播图的核心功能不会依赖controller,使controller可以被简单插拔。

    一般希望代码行数不超过14行,最多20行,当超出这个范围,可能就需要考虑重构代码。

    插件化之后的JS示例如下:

    class Slider{
      constructor(id, cycle = 3000){
        this.container = document.getElementById(id);
        this.items = this.container.querySelectorAll('.slider-list__item--selected','.slider-list__item--selected')
        this.cycle = cycle;
      }
      // 不让slider知道插件的存在,于是通过依赖注入的方式注册插件。遍历运行插件构造的方法
      // 这样插件就从实例中独立出来
      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"
        }
      }
      slideNext(){
        const currentIdx = this.getSelectedItemIndex();
        const nextIdx = (this.items.length+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)
      }
    }
    
    // 实现插件
    // 实现一排controller
    function pluginController(slider){
      const controller = slider.container.querySelector('.slider-list__item')
      if(controller){
        const buttons = controller.querySelectorAll('.slide-list__controller,.slide-list__controller--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__controller');
          if(selected){
            selected.className="slide-list__controller"
          }
          buttons[idx].className="slide-list__controller--selected"
        })
      }
    }
    // 实例化slider
    const slider = new Slider('my-slider')
    // 注册插件
    slider.registerPlugins(pluginController)
    // 启动slider
    slider.start()
    HTML模板化

    之前只是解耦了JS代码,但是HTML的内容还需要手动删除。此时可以用JS模板化渲染HTML。

    class Slider{
      constructor(id,opts={images:[],cycle:3000}){
        // 初始化
      }
      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>`
        )
      }
    }

    过程抽象

    将通用的组件模型抽象出来,用来处理局部细节控制的一些方法。是函数式编程思想的基础应用。

    比如slider中,可以看到插件和slider类都有render方法,用于初始化HTML模板。因此可以抽象出一个Component类,将registerPlugin和render方法抽象出来,然后让slider类和插件都继承这个Component类,再交由具体类重写render方法

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

    总结

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

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

    三次重构:插件化、模板化、抽象化(组件框架)