如何写好JavaScript(1) | 青训营笔记

126 阅读10分钟

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

如何写好JavaScript(1) | 青训营笔记

写好JS的一些原则

  • 各司其职:让HTML、CSS和JavaScript职能分离。
  • 组件封装:好的UI组件具备正确性、扩展性、复用性。
  • 过程抽象:应用函数式编程思想。

各司其职

让 HTML、CSS和 JavaScript 职能分离,HTML是页面的骨架,CSS是页面的皮肤,JavaScript是页面的动作了。我们在写代码的时候应该他们各自负责自己该负责的部分,尽可能的少用 JS 干扰 CSS 和 HTML ,这样的代码才是高质量的。

例子:深浅模式切换

实现效果如下:

image.png

image.png

为了对比代码的优劣,下面给出了三种版本的代码做分析

  • 版本一
//html
 <header>
        <button id="modeBtn">🌞</button> //给按钮绑定鼠标点击事件
        <h1>深浅色模式切换</h1>
 </header>
 
 //css
 body,
    html {
      width: 100%;
      height: 100%;
      padding: 0;
      margin: 0;
      overflow: hidden;
    }

    body {
      padding: 10px;
      box-sizing: border-box;
    }

    #modeBtn {
      font-size: 2rem;
      float: right;
      border: none;
      background: transparent;
    }

//js
window.onload=function(){
    document.getElementById("file-btn") 
    const btn = document.getElementById('modeBtn');

    btn.addEventListener('click', (e) => {
      const body = document.body; //获取页面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 = '🌞';
      }
    });
}

我们在js中操作了body的style,修改了元素的样式和结构,显得结构混乱,这样的写法明显不符合我们的各司其职的原则!

我们的代码不仅要写给自己看,更是要写给队友看,于是经过优化,我们写出了下面的版本二 ↓

  • 版本二
//html
 <header>
        <button id="modeBtn"></button> //给按钮绑定鼠标点击事件,与版本一不同的是去掉了🌞
        <h1>深浅色模式切换</h1>
 </header>
 
 //css
body,
    html {
      width: 100%;
      height: 100%;
      max-width: 600px;
      padding: 0;
      margin: 0;
      overflow: hidden;
    }

    body {
      padding: 10px;
      box-sizing: border-box;
      transition: all 1s;
    }

    #modeBtn {
      font-size: 2rem;
      float: right;
      border: none;
      outline: none;
      cursor: pointer;
      background: inherit;
    }

    body.night {
      background-color: black;
      color: white;
      transition: all 1s; //美观上做了一些调整,切换时有1秒的延时
    }

    #modeBtn::after { //各司其职,让css来实现图标的切换
      content: '🌞';
    }

    body.night #modeBtn::after {
      content: '🌜';
    }
    
//js
window.onload=function(){
    document.getElementById("file-btn") 
   const btn = document.getElementById('modeBtn');
        btn.addEventListener('click', (e) => {
        const body = document.body;
        if (body.className !== 'night') { //通过className的'night'来显示深色模式,判断上更直观
            body.className = 'night';
        } else {
            body.className = '';
        }
        });
}

当body元素的class属性不等于night时,表示浅色模式,所以现在需要将它的状态修改为夜间模式,于是我们只要将它的class属性设置为night,页面就会呈现夜间模式的样式。同理,当body元素的class属性等于night时,表示body元素是深色模式,所以需要将这个元素的状态修改为浅色模式,即class属性等于空(默认状态)。

优点:

  • 代码可读性提高
  • 需求变更时,只需要更改body.night的样式规则即可

继续寻找更优化的方案,版本三的优化甚至不需要写 js !我们来看看

  • 版本三
<input id="modeCheckBox" type="checkbox"> //通过点击,记住勾选和不勾选的两个状态,刚好对应浅色模式和深色模式两种状态
  <div class="content">
    <header>
      <label id="modeBtn" for="modeCheckBox"></label>
      <h1>深浅色模式切换</h1>
    </header>
  </div>
  
//css
body,
    html {
      width: 100%;
      height: 100%;
      padding: 0;
      margin: 0;
      overflow: hidden;
    }

    body {
      box-sizing: border-box;
    }

    .content {
      padding: 10px;
      transition: background-color 1s, color 1s;
    }

    #modeCheckBox {
      display: none; //将大盒子外面的checkbox隐藏起来
    }

    #modeCheckBox:checked+.content { //通过 checkbox 的伪类选择器checked,点击checkbox就会触发这个伪类
      background-color: black;
      color: white;
      transition: all 1s;
    }

    #modeBtn {
      font-size: 2rem;
      float: right;
    }

    #modeBtn::after {
      content: '🌞';
    }

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

版本三中,我们通过 CSS 来完成了本来应该 JS 完成的切换模式并记住状态的功能。我们让选中状态用伪类选择器#modeCheckBox:checked来标记

结论:

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

通过以上三种不同的版本,使得代码一次一次得到优化,更简练也更直观,这是我们今后学习前端需要努力的目标!

组件封装

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

例子:轮播图

实现效果如下:

image.png

为了对比代码的优劣,下面给出了两种版本的代码做分析

  • 版本一 —— API无交互

html结构:

  • 轮播图是⼀个典型的列表结构,我们可以使⽤⽆序列表ul元素来实现
//html
<div id="my-slider" class="slider-list">
      <ul> 
        <li class="slider-list__item--selected"> 
        //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>
    </div>

css表现:

  • 使用 CSS 绝对定位将图片重叠在同一个位置
  • 轮播图切换的状态使用修饰符(modifier)
  • 轮播图的切换动画使用 CSS transition
//css
#my-slider{
    position: relative;
    width: 790px;
  }

  .slider-list ul{
    list-style-type:none;
    position: relative;
    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;
  }
  

js行为:API

image.png

//js
class Slider{
    constructor(id){
      this.container = document.getElementById(id);
      this.items = this.container
      .querySelectorAll('.slider-list__item, .slider-list__item--selected');
    }
    getSelectedItem(){ 
    //得到当前轮播图正在显示的li
      const selected = this.container
        .querySelector('.slider-list__item--selected');
      return selected
    }
    getSelectedItemIndex(){ 
    //获取当前轮播图的显示li的下标
      return Array.from(this.items).indexOf(this.getSelectedItem());
    }
    slideTo(idx){ 
    //轮播到指定的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);
  
//这样就可以通过手动调用API来使用轮播图了

const slider = new Slider('my-slider');
slider.slideTo(1);
slider.slideTo(2);
slider.slideNext();
slider.slidePrevious();

//还可以直接定义一个定时器,让他自动播放

const slider = new Slider('my-slider');
setInterval(() => { 
    slider.slideNext(); 
}, 1000);

接下来就是去添加下面圆点控制左右移动按钮,但是这些控制的按钮和组件的状态是有状态耦合的,可是联系很紧密的设计如果未来更改需求修改起来很复杂,因此在设计的时候轮播图片和这些按钮是独立的,因此就在控制流部分需要进行解耦

  • 版本二 —— 控制流交互

使用自定义事件来解耦

html结构:

为了能让用户自己选择需要的操作,设置了控制前后翻图的箭头,下面控制选图的小圆点

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


css表现:

在版本一代码的基础上增加了下面四个小圆点和箭头,实现交互

/*下面是四个小圆点的样式*/ 
.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; 
}

js行为:控制流

在版本一代码基础上,加入控制流,让轮播图可以自动轮播,也可以手动控制,实现交互效果

使用自定义事件来解耦

      // 鼠标经过某个小圆点,就将此圆点对应的图片显示出来,并且停止循环轮播
      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();
      });
      
      // 注册slide事件,将选中的图片和小圆点设置为selected状态
      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();
      });
    }


2、结论:基本方法

  • 结构设计 HTML

  • 展现效果 CSS

  • 行为设计 JavaScript

    • API (功能)
    • Event (控制流)

以上的优化只做到了封装性和正确性,缺少扩展性和复用

我们将上面的基本代码进行改进,完成 重构 轮播图组件

3、重构:插件化

当后期不需要下面的原点和箭头时,直接在HTML中直接删除或者换掉这部分HTML代码,而不需要去动JS中的核心逻辑代码,可以更方便

把图片轮播和这几个按钮的主体部分分离出来,也就是将左右两个按钮和五个圆点解耦出来

解耦

  • 将控制元素抽取成插件
  • 插件与组件之间通过依赖注入方式建立联系(将依赖对象传入插件初始化函数的方式)

对于以上的例子,将控制元素抽取成插件,插件与组件之间通过依赖注入方式建立联系,在constructor中把控制圆点和上下页移动轮播图的功能抽离出来成为一个个插件函数,然后再在组件里面添加一个注册函数registerPlugins()API,轮播图调用时候,只需要把每个插件函数通过这个函数注册进去就行了,去除就是把插件函数注销掉就行了。

通过这样的方法,将用户控制的操作从组件中抽离出来,做成插件,这样就提高了组件的可扩展

4、重构:模板化

解耦

  • 将HTML模板化,更易于扩展

对于以上的例子,JS代码中使用一个render()的API来把HTML模块化,使用一个数组来存图片的路径。通过这样HTML模块化后,如果我们注释掉圆点部分的插件,相其他部分的功能也不会受到影响,那么在HTML中相应的圆点也不会有,需要新的插件,我们重新写好插件直接往里面注册就行了

5、重构:抽象化(组件框架)

抽象

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

我们将模板化中的registerPlugins(...plugins)render()组件抽离出来放在Component类中,然后通过继承的方式将其继承该子类Slider,这样就能抽象出一个Component类,这个类体系相对完整,可以相当于是一个很小的组件框架

6、总结

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

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

  • 三次重构

    1. 插件化
    2. 模板化
    3. 抽象化(组件框架)

过程抽象

  • 用来处理局部细节控制的一些方法
  • 函数式编程思想的基础应用

image.png

例子:

操作次数限制

  • 一些异步交互
  • 一次性的HTTP请求
 const list = document.querySelector('ul');
  const buttons = list.querySelectorAll('button');
  buttons.forEach((button) => {
    button.addEventListener('click', (evt) => {
      const target = evt.target;
      target.parentNode.className = 'completed';
      setTimeout(() => {
        list.removeChild(target.parentNode);
      }, 2000);
    });
  });

image.png

高阶函数

Once

为了能够让“只执行一次“的需求覆盖不同的事件处理,我们可以将这个需求剥离出来。这个过程我们称为过程抽象

function once(fn) {
    return function(...args) {
      if(fn) {
        const ret = fn.apply(this, args);
        fn = null;
        return ret;
      }
    }
  }

image.png

HOF

  • 以函数作为参数
  • 以函数作为返回值
  • 常用于作为函数装饰器

image.png

function HOF0(fn) {
    return function(...args) {
      return fn.apply(this, args);
    }
  }

HOF

function HOF0(fn) {
    return function(...args) {
      return fn.apply(this, args);
    }
  }

image.png

编程范式

命令式与声明式

image.png

let list = [1, 2, 3, 4];
  let mapl = [];
  for(let i = 0; i < list.length; i++) {
    mapl.push(list[i] * 2);
  }
 let list = [1, 2, 3, 4];
  const double = x => x * 2;
  list.map(double);

例子

image.png

总结

  • 过程抽象 / HOF / 装饰器
  • 命令式 / 声明式

image.png