JS之组件封装
Table of Contents
遥想当初第一次用js写的功能组件就轮播图,在掘金看到大佬怎样一步步封装抽象轮播图,实在是太优雅了,终于体会到看人家敲代码是种享受的感觉。
初步实现 #
大佬是以京东商城的轮播图为例子实现的。 我特意去淘宝首页看了一眼,过去我的轮播图效果和淘宝的是一样的,都是滚动切换, 而京东的是设置透明度实现切换效果。
图片切换 #
首先是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>
然后加上css样式
#my-slider{
position: relative;
width: 790px;
height: 340px;
}
.slider-list ul{
list-style-type:none;
position: relative;
width: 100%;
height: 100%;
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;
}
js代码控制轮播
class Slider {
constructor(id) {
// 轮播图的dom容器
this.container = document.getElementById(id)
// 轮播图中的所有轮播对象dom
this.items = this.container.querySelectorAll('.slider-list__item--selected, .slider-list__item')
}
// 获取当前选中的轮播对象
getSelectedItem() {
const selected = this.container.querySelector('.slider-list__item--selected')
return selected
}
// 获取当前选中的轮播对象的下角标
getSelectedItemIndex() {
return Array.from(this.items).indexOf(this.getSelectedItem())
}
// 滚动至指定下角标的图片
slideTo(inx) {
const selected = this.getSelectedItem()
if(selected) {
selected.className = 'slider-list__item'
}
const item = this.items[inx]
if(item) {
item.className = 'slider-list__item--selected'
}
}
// 滚动到下一张图
slideNext() {
const currentInx = this.getSelectedItemIndex()
// 通过取余的方式获得下长图的下角标
const nextInx = (currentInx + 1) % this.items.length
this.slideTo(nextInx)
}
// 滚动到上一张图
slidePrevious() {
const currentInx = this.getSelectedItemIndex()
// 为了防止下角标为负的,前面再加一个所有轮播对象的总数再取余
const previousInx = (this.items.length + currentInx - 1) % this.items.length
this.slideTo(previousInx)
}
}
// 调用构造函数,每2s切换一次图片
const mySlider = new Slider('my-slider')
setInterval(() => {
mySlider.slideNext()
}, 2000)
写了一个构造函数,设计的方法遵守指责单一和可扩展性的原则,定义了几个方法,简单明了。 这样图片就能自动切换了,但功能完整的轮播图上还应该有两个小组件
控制流 #
<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>
<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>
</div>
js代码主要的变化是加了两个方法控制轮播的开始和结束
start() {
this.stop()
this._timer = setInterval(() => {
this.slideNext()
}, this.cycle)
}
stop() {
clearInterval(this._timer)
}
原方法里只有一个地方发生了改变
slideTo(inx) {
const selected = this.getSelectedItem()
if(selected) {
selected.className = 'slider-list__item'
}
const item = this.items[inx]
if(item) {
item.className = 'slider-list__item--selected'
}
// 设置自定义事件,方便监听图片切换
const detail = {index: inx}
const event = new CustomEvent('slide', {bubbles:true, detail})
this.container.dispatchEvent(event)
}
之所以用自定义事件是为了解耦。 然后是新添加的组件控制流相关代码
constructor(id, cycle = 3000) {
this.container = document.getElementById(id)
this.items = this.container.querySelectorAll('.slider-list__item--selected, .slider-list__item')
this.cycle = cycle
const controller = this.container.querySelector('.slide-list__control')
if (controller) {
const buttons = controller.querySelectorAll('.slide-list__control-buttons--selected, .slide-list__control-buttons')
controller.addEventListener('mouseover', evt => {
const idx = Array.from(buttons).indexOf(evt.target)
if (idx >= 0) {
this.slideTo(idx)
this.stop()
}
})
controller.addEventListener('moseout', () => {
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';
})
}
const previous = this.container.querySelector('.slide-list__previous')
if(previous) {
previous.addEventListener('click', evt => {
this.stop()
this.slidePrevious()
this.start()
evt.preventDefault()
})
}
const next = this.container.querySelector('.slide-list__next')
if (next) {
next.addEventListener('click', evt => {
this.stop()
this.slideNext()
this.start()
evt.preventDefault()
})
}
}
都是监听组件上的鼠标移入移出事件和点击事件,实现图片的切换以及组件样式的变化。 以上,一个轮播图就实现了。 但对于这个组件来说,添加和删除控制按钮需要修改的代码太多了,对于后期扩展和维护来说体验都很不好, 接下来就要重构代码,让它更易扩展。
重构 #
将之前添加的控制按钮都插件化。
插件化 #
第一步是把控制按钮相关的方法都从构造函数中抽出来
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';
});
}
}
function pluginPrevious(slider){
const previous = slider.container.querySelector('.slide-list__previous');
if(previous){
previous.addEventListener('click', evt => {
slider.stop();
slider.slidePrevious();
slider.start();
evt.preventDefault();
});
}
}
function pluginNext(slider){
const next = slider.container.querySelector('.slide-list__next');
if(next){
next.addEventListener('click', evt => {
slider.stop();
slider.slideNext();
slider.start();
evt.preventDefault();
});
}
}
插件和组件之间,通过依赖注入
的方式建立联系,也就是在构造函数中写一个注册插件的方法
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;
}
registerPlugins(...pulgins) {
plugins.forEach(plugin => plugin(this))
}
// ...其它方法
}
调用时
const slider = new Slider('my-slider')
slider.registerPlugins(pluginController, pluginPrevious, pluginNext)
slider.start()
如果后续需要添加新的插件,就无需修改构造函数,直接在外部写函数,然后调用registerPlugins
方法注册到组件上就好了。
但仍然有个问题,那就是修改组件不仅需要修改js代码,还需要改动html代码,还可以继续封装,做到扩展时只需要添加删除js部分的代码。
模板化 #
将html那部分模板化,那么实际调用组件时的html代码就只剩一行
<div id="my-slider" class="slider-list" />
js代码部分,既然要模板化html,那就是动态生成html,给构造函数传入的参数会多一个images,添加render方法生成dom,注册插件的方法也会有所不同。
class Slider{
constructor(id, opts = {images:[], cycle: 3000}){
this.container = document.getElementById(id);
this.options = opts;
// 动态生成html内容
this.container.innerHTML = this.render();
this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected');
this.cycle = opts.cycle || 3000;
// 动态生成dom后需要调用slideTo方法,设置选中的第一张图片,否则直到调用下一个slideTo方法后,会一直是空白的
this.slideTo(0);
}
render(){
const images = this.options.images;
// 根据传入的图片自动生成html代码
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);
});
}
// ...其它方法
}
插件注册的方法变了,现在的设计是,传入的插件必须有两个方法:render
和action
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';
});
}
}
};
const pluginPrevious = {
render(){
return `<a class="slide-list__previous"></a>`;
},
action(slider){
const previous = slider.container.querySelector('.slide-list__previous');
if(previous){
previous.addEventListener('click', evt => {
slider.stop();
slider.slidePrevious();
slider.start();
evt.preventDefault();
});
}
}
};
const pluginNext = {
render(){
return `<a class="slide-list__next"></a>`;
},
action(slider){
const previous = slider.container.querySelector('.slide-list__next');
if(previous){
previous.addEventListener('click', evt => {
slider.stop();
slider.slideNext();
slider.start();
evt.preventDefault();
});
}
}
};
插件不再是函数,而是个对象,值是个函数。 调用方法:
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();
如果是以前的我,能做到这里就已经很牛逼了,然后迫不及待地上线。 但对于大佬说,这并不是终点。
抽象通用组件框架 #
registerPlugins(...plugins)
:注册插件render()
:渲染html
抽象出来的通用组件代码如下:
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 ''
}
}
Slider
继承自Component
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函数
render(data){
const content = data.map(image => `
<li class="slider-list__item">
<img src="${image}"/>
</li>
`.trim());
return `<ul>${content.join('')}</ul>`;
}
// ...其它方法
}
上面的Component
类已经是一个组件框架了,它支持定义一个组件和注册插件的功能。
总结 #
根据上面几个步奏,我们已经完成了组件的插件化、模板化和抽象化。
这个抽象出来的框架虽小,但对于日常业务来说完全够用了。
但它并没有考虑到组件嵌套,这里直接将插件分离出来了。
如果想要进阶完善,可以将插件作为子组件整合进框架里。
另外,模板化这里只考虑到了html,如果要修改组件还要手动修改css,可以思考如何将css也进行模板化。