这是我参与「第四届青训营 」笔记创作活动的第1天
如何写好JavaScript
由于课程内容较多,决定分开写,这篇主要介绍组封装。对于封装的不断改善,能够帮助我们逐层了解组件封装的主要思想,并且对于我们下面进行组件式开发提供了许多思考。
组件封装
用原生JS实现录播图,展示如下
<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>
行为API
创建slider类用来实现图片的轮播效果,并且在类的原型方法中定义了一些行为API如getSelectedItem、getSelectedItemIndex、slideTo、slideNext、slidePrevious。其中slideNext与slidePrevious这两个方法没有涉及到页面状态的改变(即css样式没有变化),而slideTo方法当点击底下按钮的同时按钮会出现样式上的改变(由白色变成红色)。鼠标点击圆按钮的同时会出发两种变化,因此需要对于其进行解耦。
行为控制流
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();
});
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来实现。如上述所说,slideTo在切换下方按钮的时候既要切换图片还需要改变按钮的css样式,这里我们需要重写上述slideTo方法,加上自定义事件slide,并将两种行为解耦。此时在控制流中可以监听slide事件,手动的去改变目标的样式。
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';
}
const detail = {index: idx}
const event = new CustomEvent('slide', {bubbles:true, detail}) //开发自定义事件
this.container.dispatchEvent(event) //把事件派发出去
}
重构:插件化
function pluginController(slider){
const controller = slider.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){
slider.slideTo(idx);
slider.stop();
}
});
controller.addEventListener('mouseout', evt=>{
slider.start();
});
slider.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';
});
}
}
之前行为的控制都是放在class的constructor里实现的,这会导致class内容太过臃肿,此时我们将控制元素抽取成插件,插件与组件之间通过依赖注入方式建立联系,并在类的原型方法中添加registerPlugins,在调用registerPlugins时将插件依次注册。这样可以在外部手动选择注册需要的插件,这实现了各个控制行为之间的解耦,这就使封装变得更加灵活。
slider.registerPlugins(pluginController, pluginPrevious, pluginNext);
重构:模板化
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>`;
}
...
}
将HTML模板化,更易于扩展
![]()
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 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){
slider.slideTo(idx);
slider.stop();
}
});
controller.addEventListener('mouseout', evt => {
slider.start();
});
slider.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';
});
}
}
};
页面的变化都是由数据变化来驱动,数据要从外部输入的,而之前的插件中,HTML作为独立的部分,其中的数据是嵌在HTML中,不便于从外部传递数据,更提现不了封装的思想。所以就要引入HTML的模块化。在实现方法上,与之前不同的是pluginController这里是作为一种对象,其中保存两种方法render与action,render根据接受到的images参数来动态渲染HTML,action则是控制行为与之前的function pluginController函数作用类似。将HTML模块化更易于扩展。实例化对象的同时将images参数传入,展现了对于组件的更好的控制性。
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});
抽象:组件化
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 ''
}
}
将组件通用模型抽象出来
![]()
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>`;
}
...
}
Slider可以通过对于通用组件的继承实现方法以及一些属性的复用。麻雀虽小,五脏俱全,虽然只是很简单的轮播图的组件,但是我们可以看出组件式开发的思想,从行为控制、插件化、模块化、组件化,其实就是一步一步解耦合的过程,使原先的封装更具有可扩展性与可复用性。想必目前流行的框架:React、Vue的组件都是类似与此的思想。
总结
-
组件设计的原则:封装性、正确性、扩展性、复用性
-
实现组件的步骤:结构设计、展现效果、行为设计
-
三次重构
- 插件化
- 模板化
- 抽象化(组件框架)
组件是指Web页面上抽出来一个个包含模版(HTML)、功能(JS)和样式(CSS)的单元。好的组件具备封装性、正确性、扩展性、复用性。