如何写好JS——各司其职和组件封装的思想 | 青训营笔记

63 阅读5分钟

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

一、重点内容如何写好JS

  1. html,css,js各司其职
  2. 组件的封装
  3. 抽象思想

二、详细知识点介绍(各司其职,组件封装,抽象思想)

1. 各司其职

核心是各自做各自的任务,html负责结构,css负责表现,js负责行为。

例子:想通过按钮来实现网页深色模式和浅色模式切换的效果

  • 做法一(不推荐,新手容易犯,前端爱好者水平)
const btn = document.getElementById('modeBtn');
  btn.addEventListener('click', (e) => {
    const body = document.body;
    if(e.target.innerHTML === '🌞') {
      body.style.backgroundColor = 'black';
      body.style.color = 'white';
      e.target.innerHTML = '🌜';
    } else {
      body.style.backgroundColor = 'white';
      body.style.color = 'black';
      e.target.innerHTML = '🌞';
    }
  });

做法一通过element.style的方式去修改了样式细则,虽然同样可以达到目的,单从各司其职的角度上js已经越界了,js应该关注的是这个行为,行为后的具体表现如何仍是css考虑的。并且这样写的代码十分复杂,如果是其他人来的话不能直观明白js做了什么工作。

  • 做法二(改进,前端工程师水平,团队更易接受)
const btn = document.getElementById('modeBtn');
  btn.addEventListener('click', (e) => {
    const body = document.body;
    if(body.className !== 'night') {
      body.className = 'night';
    } else {
      body.className = '';
    }
  });
body.night {
  background-color: black;
  color: white;
  transition: all 1s;
}
#modeBtn::after {
  content: '🌞';
}
body.night #modeBtn::after {
  content: '🌜';
}

同样是通过注册事件,if和else的逻辑,做法二明显比做法一更加清晰明了,能从代码中看出js负责的行为。通过改变类名的方式来达到目的,是开发中常用的手段,css和js各司其职。灵活运用css的伪元素来切换图标的显示内容,甚至可以再进一步,完全不用js,如做法三

  • 做法三(进一步精简) 白天和夜晚的切换本质上就是改变样式,没有其他的行为逻辑在内,只有一个行为逻辑就是click切换。一般来说控制样式的代码是可以用纯CSS来实现的
<input id="modeCheckBox" type="checkbox">
<div class="content">
    <header>
      <label id="modeBtn" for="modeCheckBox"></label>
      <h1>深夜食堂</h1>
    </header>
    ...
</div>
#modeCheckBox {
    display: none;
  }
#modeCheckBox:checked + .content {
    background-color: black;
    color: white;
    transition: all 1s;
  }
#modeBtn::after {
  content: '🌞';
}

#modeCheckBox:checked + .content #modeBtn::after {
  content: '🌜';
}

通过对css的高级应用,伪类选择器根据checked的状态的添加样式,从而实现相同效果,同时将原本的图标的部分改写为label,通过for绑定到被隐藏的input上达到相同点击切换的效果。

总结思考:我们在面对此类纯样式的问题时,应该多思考如何用零js实现,避免不必要的由JS直接操控样式。应用时经常记得各司其职的编程原则

2. 组件封装

组件的封装应该包括三个部分:

  1. 结构设计

  2. 展示效果

  3. 行为设计

例子:轮播图的纯js实现,此处主要讲js行为设计的封装

  • 轮播图自动播放部分(行为:api)
 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);  
    }
  }

实现的是一个轮播图切换行为的api封装,代码看起来可能比较多,但其实内容并不复杂,下面依次说一下Slider类方法

constructor(id) 构造器,传入要操作节点的id,并获取该节点和该节点下的全部节点(slider-list__item和slider-list__item--selected,其中带selected的单独区分表示当前选中的)

getSelectedItem()获取当前轮播图里的焦点,并返回该节点

getSelectedItemIndex() 获取当前选中节点的索引

slideTo(idx) 传入给定索引值,跳转到对应的轮播图

slideNext() 切换到下一张

slidePrevious() 切换到上一张

在此不过多赘述其内部具体实现,主要是理解 行为:Api 的思想

亮点:将轮播图所具备的基本功能都给实现并封装好了,根据需求可以直接调用这些api,如下

  const slider = new Slider('my-slider');
  setInterval(()=>{
      slider.sliderNext();
  },2000)
  • 轮播图事件部分,手动切换(行为:控制流)

使用自定义事件来解耦,在已有的api里面添加事件监听器来控制行为,理解思路。

下列代码为构造器里的部分改动,controller为轮播图下方的小圆点,cycle为轮播图切换的时间间隔,增加自定义事件,鼠标经过圆点时,轮播图切换到对应图片并停止自动播放,鼠标离开时继续自动播放

  start(){
    this.stop();
    this._timer = setInterval(()=>this.slideNext(), this.cycle);
  }
  stop(){
    clearInterval(this._timer);
  }
  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();
      });
     ...
  }

并且从代码中可知,自定义的事件大多在构造器里,不会过多影响原有的api。 但以上的代码仍具备改进的空间,对于小圆点部分的后续修改仍不方便,构造函数很复杂

解决思路:思考将构造函数插件化,将控制元素插件化,然后依赖注入的方式使用。这样就可以不改动slider的代码,通过让slider注册对应的插件来丰富原本的轮播图行为,并且如果不需要这些行为了也可以方便管理直接去掉,无需改动slider。

image.png

还可以将html模板化更易于扩展,此处就不展开

总结: 在组件封装部分,行为设计这一块采用API(功能)+Event(控制流)的方式实现封装。 好的组件应该具备封装性,复用性,正确性和扩展性。 通过这节课,循循渐进的理解封装的必要性,理解为什么要这样封装,收获相当的大,如何写好js真的是一门相当大的学问。