这是我参与「第四届青训营」笔记创作活动的的第 3 天
写好JavaScript 的原则
- 各司其责
- 组件封装
- 过程抽象
各司其责
看下面这段用于切换白天/夜间模式的代码:
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
类实现即可。
总结
- 组件设计的原则:封装性、正确性、扩展性、复用性
- 实现组件的步骤:结构设计、展现效果、行为设计
- 三次重构
- 插件化
- 模板化
- 抽象化(组件框架)