组件封装
让我们来看一个电商网站轮播图的例子。用原生JS写一个电商网站的轮播图,应该怎么实现?
基本方法
依旧是HTML、CSS、JS各司其职。轮播图是一个典型的列表结构,用无序列表<ul>元素来实现。通过使用绝对定位将图片重叠在一个位置,用transition来做切换效果。
接下来我们为轮播图的行为设计一组API,API设计应保证原子操作,职责单一,满足灵活性。包括以下几种:
- Slider
- +getSelectedItem()获取被选中节点
- +getSelectedItemIndex()获取被选中节点在所有节点中的位置
- +slideTo()切换第几张
- +slideNext()切换到下一张
- +slidePrevious()切换到上一张
class Slider{
constructor(id){
this.container = document.getElementById(id);
this.items = this.container
.querySelectorAll('.slider-list__item, .slider-list__item--selected');
}
getSelectedItem(){...}
getSelectedItemIndex(){...}
slideTo(idx){...}
slideNext(){...}
slidePrevious(){...}
}
//创建一个滑动组件,并切换到第二张
const slider = new Slider('my-slider');
slider.slideTo(2);
在此基础上我们加上控制流,用来监听事件。
class Slider{
constructor(id, cycle = 3000){
this.container = document.getElementById(id);
this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected');
this.cycle = cycle;
//小圆点切换
const controller = this.container.querySelector('.slide-list__control');
if(controller){
controller.addEventListener('mouseover', evt=>{});
controller.addEventListener('mouseout', evt=>{});
this.container.addEventListener('slide', evt => {})
}
//切换下一张
const previous = this.container.querySelector('.slide-list__previous');
if(previous){
previous.addEventListener('click', evt => {});
}
//切换上一张
const next = this.container.querySelector('.slide-list__next');
if(next){
next.addEventListener('click', evt => {});
}
}
getSelectedItem(){...}
getSelectedItemIndex(){...}
slideTo(idx){...}
slideNext(){...}
slidePrevious(){...}
start(){...}
stop(){...}
}
const slider = new Slider('my-slider');
slider.start();
插件化
观察上一版的构造函数,我们可以发现代码写的太过臃肿,除了初始化以外,我们还注册了按钮的点击、切换、滑动的事件。因此我们考虑将这些控制元素分离出去,通过抽取成插件,凭借依赖注入的方式建立联系。
class Slider{
constructor(){
}
registerPlugins(...plugins){
plugins.forEach(plugin => plugin(this));
}
}
function pluginController(slider){
}
function pluginPrevious(slider){
}
function pluginNext(slider){
}
const slider = new Slider('my-slider');
slider.registerPlugins(pluginController, pluginPrevious, pluginNext);
slider.start();
模板化
尽管使用插件化,已经实现了组件和不同插件之间的解耦。但我们发现HTML有继续简化的空间,有些时候我们不需要某部分的插件,还需要手动的去删除。但通过模板化,就能简化HTML删改的过程。
<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){...}
getSelectedItem(){...}
getSelectedItemIndex(){...}
slideTo(idx){...}
slideNext(){...}
slidePrevious(){...}
start(){...}
stop(){...}
}
const pluginController = {
render(images){...},
action(slider){...}
};
const pluginPrevious = {
render(){...},
action(slider){...}
};
const pluginNext = {
render(){...},
action(slider){...}
};
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();
组件框架
更一般的,我们能将通用组件模型抽象出来,这样我们在使用其他组件的时候都能继承该模型,而不仅仅只是Slider。
class Component{
constructor(id, opts = {name, data:[]}){}
registerPlugins(...plugins){}
render(data) {
/* abstract */
return ''
}
}
总结
这里我们只将HTML实现了模板化,并没有将CSS也纳入其中。比较常见的框架如React、Vue都实现了HTML、CSS、JS的组件化开发。
使用组件化开发的模式,能避免大量重复代码、逻辑、功能在多个页面使用,避免了复杂的维护。通过功能化拆分、组件封装不仅增加了代码的可读性,也降低了运维成本。
在组件设计中应遵守以下基本原则:
- 封装性
- 正确性
- 扩展性
- 复用性
组件实现的基本步骤包括:HTML结构设计、CSS展现效果设计、JS行为设计。在此基础上,通过插件化、模板化、抽象化,对组件进行重构优化。