(三)跟着月影学JavaScript(上) | 青训营笔记

87 阅读5分钟

“这是我参与「第五届青训营」伴学笔记创作活动的第 3 天”

主要内容

JavaScript编码原则:

(1)HTML/CSS/JS各司其责

(2)组件封装 —— 结构/展现/行为设计+三次重构(插件化、模板化、抽象化)

1 写好JavaScript的一些原则

1.1 各司其责

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

1.1.1 案例 —— 深夜食堂(白天/夜间模式切换)

案例描述:写一段JS,控制一个网页支持浅色和深色两种浏览模式,如何你来实现,你会怎么做?

版本一 —— 使用JS控制样式

这一版代码没有满足刚才提到的各司其职的原则,而直接使用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 = '🌞';
  }
});

版本二 —— 用class来表示状态,由JS对class进行切换

这也是我第一反应想到的方法。对夜间模式的CSS样式进行了类的封装,点击切换按钮即对标签的类进行增加或删除。

#modeBtn {
  font-size: 2rem;
  float: right;
  border: none;
  outline: none;
  cursor: pointer;
  background: inherit;
}
body.night {
  background-color: black;
  color: white;
  transition: all 1s;
}
#modeBtn::after { content: '🌞'; }
body.night #modeBtn::after { content: '🌜'; }
const btn = document.getElementById('modeBtn');
btn.addEventListener('click', (e) => {
  const body = document.body;
  if(body.className !== 'night') {
    body.className = 'night';
  } else {
    body.className = '';
  }
});

版本三 —— 寻求零JS方案

然而应该想到的是,白天/夜间模式的切换应当是属于对样式的切换,完全可以仅由CSS进行实现。这一版本的代码核心是使用checkbox:checked伪类实现点击前后的样式改变,而不是在JS中对点击事件进行监听。

.content {
  padding: 10px;
  transition: background-color 1s, color 1s;
}
#modeCheckBox { display: none; }
#modeCheckBox:checked + .content {
  background-color: black;
  color: white;
  transition: all 1s;
}
#modeBtn { font-size: 2rem; float: right; }
#modeBtn::after { content: '🌞'; }
#modeCheckBox:checked + .content #modeBtn::after { content: '🌜'; }

1.2 组件封装(以轮播图为例)

  • 组件,是Web页面上抽出来一个个包含模板(HTML)、样式(CSS)、功能(JS)的单元。好的组件具备封装性正确性扩展性复用性
  • 实现组件的三个主要步骤:结构设计(HTML)、展现效果(CSS)、行为设计(JS)
  • 提高代码质量的三次重构:插件化、模板化、抽象化(组件框架)

案例描述:用原生JS写一个电商网站的轮播图,应该怎么实现?

1.2.1 结构:HTML

  • 轮播图是一个典型的列表结构,我们可以使用无序列表<ul>元素来实现。
<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>
</div>

1.2.2 表现:CSS

  • 使用CSS绝对定位将图片重叠在同一个位置
  • 轮播图切换的状态使用修饰符(modifier)
    • 这里使用的修饰符是selected,表示被选中展示的图片
  • 轮播图的切换动画使用CSS transition
#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;
}

1.2.3 行为:JS

  • 功能(API):应保证原子操作、职责单一、满足灵活性。
    • class Slider
      • +getSelectItem():获取选中的项
      • +getSelectItemIndex():获取所选项的索引
      • +slideTo():跳转到所选的页面
      • +slideNext():跳转到下一张图片
      • +slidePrevious():跳转到上一张图片
  • 控制流(Event):使用自定义事件来解耦。选择图片的小圆点是有状态的跟相对应的图片有一个状态绑定的过程,使用自定义事件可以进行实现。
    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';
    })
    

1.2.4 重构(进阶)

通过以上的结构设计、样式表现、行为设计(功能+控制流)可以实现一个基本的组件,接下来通过重构寻求轮播组件的改进。

划重点:重构不改变HTML/CSS/JS各司其责的原则,三者写在同一个文件里不代表它们不分别做各自的事!

现有组件中存在的问题:

  • 不够灵活。功能虽然实现了,但当我想删去或修改左右箭头对应的控件时,不得不修改HTML、CSS、JS三部分的代码。可以将其抽象出来成为插件以提高灵活性。

解决方案

  • 插件化(解耦):无需修改组件本身的实现代码,即可对组件功能实现扩展。
    • 将控制元素抽取成插件
    • 插件与组件之间通过依赖注入方式建立联系
class Slider {
  constructor(id, cycle = 3000) { ... }
  getSelectedItem() { ... }
  getSelectedItemIndex() { ... }
  slideTo(idx) { ... }
  slideNext() { ... }
  slidePrevious() { ... }
  start() { ... }
  stop(){ ... }
  
  registerPlugins(...plugins) { // 注册插件
    plugins.forEach(plugin => plugin(this));
  }
  addEventListener(type, handler) { // 事件监听
    this.container.addEventListener(type, handler)
  }
}
// 将轮播组件的三项控件进行了分离和插件化
// 将slider传入方法,进行依赖注入
function pluginController(slider) { // 底端控件,选择性跳转到某一张图片
  const controller = slider.container.querySelector('.slide-list__control');
  if(controller) { ... }  
}

function pluginPrevious(slider) { // 跳转到前一页
  const previous = slider.container.querySelector('.slide-list__previous');
  if(previous) { ... }  
}

function pluginNext(slider) { // 跳转到后一页
  const next = slider.container.querySelector('.slide-list__next');
  if(next) { ... }  
}

const slider = new Slider('my-slider');
slider.registerPlugins(pluginController, pluginPrevious, pluginNext);
slider.start();
  • 模板化:将HTML模板化,更易于扩展,这一步善用好JS里的render方法。 image.png
<div id="my-slider" class="slider-list"></div>
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() { ... }
  getSelectedItemIndex() { ... }
  slideTo(idx) { ... }
  slideNext() { ... }
  slidePrevious() { ... }
  addEventListener(type, handler) { ... }
  start() { ... }
  stop() { ... }
}

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 pluginPrevious = {
  render() {
    return `<a class="slide-list__previous"></a>`;
  },
  action(slider) {
    const previous = slider.container.querySelector('.slide-list__previous');
    if(previous) { ... }
  }
};

const pluginNext = {
  render() {
    return `<a class="slide-list__next"></a>`;
  },
  action(slider) {
    const next = slider.container.querySelector('.slide-list__next');
    if(next) { ... }
  }
};

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();
  • 组件框架:在前述方法实现之后的进一步优化,将组件通用模型模抽象出来。 image.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 => { ... });
  }
  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(){ ... }
  getSelectedItemIndex() { ... }
  slideTo(idx){ ... }
  slideNext() { ... }
  slidePrevious() { ... }
  addEventListener(type, handler) { ... }
  start() { ... }
  stop() { ... }
}

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

const slider = new Slider(...);

slider.registerPlugins(pluginController, pluginPrevious, pluginNext);
slider.start();
  • 改进空间:
    • 现在组件和插件是相互独立的,子组件不能够注册为父组件的插件,后续可以考虑设计一种更通用的模型框架,从而将二者组合起来。
    • CSS的代码尚未实现模板化
    • ……