如何写好 JavaScript | 青训营笔记

52 阅读4分钟

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

写好JavaScript 的原则

  1. 各司其责
  2. 组件封装
  3. 过程抽象

各司其责

看下面这段用于切换白天/夜间模式的代码:

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 = '🌞';
    }
});

这段代码给按钮添加了一个监听,并且使用 JavaScript 对 body 中的背景色、文字颜色的样式进行了修改。

这种方法将「样式」的定义混入了 JavaScript,因此并没有做到各司其责。

于是我们将样式的定义抽离到 CSS 中去,在 JavaScript 中只留下控制元素 class 的代码,关键代码是这样的:

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

body.night {
    background-color: black;
    color: white;
    transition: all 1s;
}
const btn = document.getElementById('modeBtn');
btn.addEventListener('click', (e) => {
  const body = document.body;
  if(body.className !== 'night') {
    body.className = 'night';
  } else {
    body.className = '';
  }
});

这个版本 JavaScript 只负责控制元素的 class,而元素具体的样式是使用 CSS 来控制的。

事实上这种方式还有一定的优化的空间,可以做到完全不需要 JavaScript 来实现,下面是关键代码:

<input id="modeCheckBox" type="checkbox">
<div class="content">
    <header>
        <label id="modeBtn" for="modeCheckBox"></label>
        <h1>深夜食堂</h1>
    </header>
    <main>
        <div class="pic">
            <img src="https://p2.ssl.qhimg.com/t0120cc20854dc91c1e.jpg">
        </div>
        <div class="description">
            <p>
                ...
            </p>
        </div>
    </main>
</div>
.content {
    height: 100%;
    padding: 10px;
    transition: background-color 1s, color 1s;
}

div.pic img {
    width: 100%;
}

#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: '🌜';
}

通过上面的栗子,我们可以得出下面的结论:

  • HTML/CSS/JavaScript 各司其责
  • 应当避免不必要的由 JavaScript 直接操作方式
  • 可以使用 class 来表示状态
  • 纯展示类的交互可以寻求零 JavaScript 的实现方式(但是也不强求)

组件化

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

一个好的组件应当具备封装性、正确性、扩展性、复用性。

例如我们要做一个轮播图的组件,应该怎么实现呢?

最容易想到的方式是,先在 HTML 中定义好结构,然后使用 CSS 控制每张轮播图的展示,显示当前的轮播图图片,然后通过 JavaScript 来控制行为,如点击轮播图左右的切换按钮或者轮播图底部的小圆点。

封装-API

此时我们的轮播图还并不是一个组件,因为并没有封装性,在使用的时候也需要关注轮播图的具体实现,而我们在使用一个组件的时候希望的是不需要关注组件的具体实现,而只关注于使用组件,这时我们可以对组件进行封装。

我们可以创建一个 Slider 类,来封装这个组件并暴露一些 API 供用户使用。

class Slider {
  getSelectedItem() {}
  getSelectedItemIndex() {}
  slideTo(idx) {}
  slideNext() {}
  slidePrevious() {}
}

这样用户在使用组件的时候只需要调用暴露的 API 就可以了~

控制流

另外我们还需要在 API 的基础上添加控制流,让轮播图既可以自动播放,也可以手动控制。下面是事件相关代码:

constructor(id, cycle = 3000) {
    // ...
    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';
    })
    // ...
    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)
    }
    // ...
}

插件化

至此,这个轮播图组件已经是一个能用的组件了,但还不够灵活和自由,比如我们要更换左右的切换按钮为其他东西,或者是移除掉它,那么这时我们不仅需要修改轮播图的代码,而且还需要同时修改 HTML、CSS 和 JavaScript 三者。

我们可以将元素抽取成插件,插件与组件之间通过依赖注入的方式建立联系。

class Slider {
    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 = {};
const pluginPrevious = {};
const pluginNext = {};

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

我们单独编写左右的控制按钮和小圆点,然后作为插件注入到轮播图中就可以实现对应的功能,当我们不在需要这个功能的时候,只需要修改注册这一部分的代码。而当我们需要修改某个插件的功能的时候,也不需要去修改轮播图的代码,而是修改插件的代码。

模板化

在插件化的基础上,我们还可以将 HTML 模板化,也就是通过 JavaScript 来渲染 HTML 元素,使得组件更加易于扩展。如下面采用 render 函数来渲染 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>`;
    }
    // ...
}

抽象化

将组件的通用模型给抽取出来,定义一个 Component 类,拥有所有组件通用的部分,同时 Component 处理插件的注册与渲染逻辑(因为这是所有 Component 都有的部分)。

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

在以后如果需要编写一个新的组件,按照这个 Component 类实现即可。

总结

  • 组件设计的原则:封装性、正确性、扩展性、复用性
  • 实现组件的步骤:结构设计、展现效果、行为设计
  • 三次重构
    1. 插件化
    2. 模板化
    3. 抽象化(组件框架)