插件封装
问题:如何封装一个轮播图插件。
先来看代码,代码如下:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>封装轮播图组件</title> <style> #my-slider{ position: relative; display: inline-block; 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; } .slide-list__control{ position: relative; display: table; background-color: rgba(255, 255, 255, 0.5); padding: 5px; border-radius: 12px; bottom: 30px; margin: auto; } .slide-list__next, .slide-list__previous{ display: inline-block; position: absolute; top: 50%; margin-top: -25px; width: 30px; height:50px; text-align: center; font-size: 24px; line-height: 50px; overflow: hidden; border: none; background: transparent; color: white; background: rgba(0,0,0,0.2); cursor: pointer; opacity: 0; transition: opacity .5s; } .slide-list__previous { left: 0; } .slide-list__next { right: 0; } #my-slider:hover .slide-list__previous { opacity: 1; } #my-slider:hover .slide-list__next { opacity: 1; } .slide-list__previous:after { content: '<'; } .slide-list__next:after { content: '>'; } .slide-list__control-buttons, .slide-list__control-buttons--selected{ display: inline-block; width: 15px; height: 15px; border-radius: 50%; margin: 0 5px; background-color: white; cursor: pointer; } .slide-list__control-buttons--selected { background-color: red; } </style> </head> <body> <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> <script> 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(...plugins){ plugins.forEach(plugin => plugin(this)); } getSelectedItem(){ const selected = this.container.querySelector('.slider-list__item--selected'); return selected } getSelectedItemIndex(){ return Array.from(this.items).indexOf(this.getSelectedItem()); } 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) } slideNext(){ const currentIdx = this.getSelectedItemIndex(); const nextIdx = (currentIdx + 1) % this.items.length; this.slideTo(nextIdx); } slidePrevious(){ const currentIdx = this.getSelectedItemIndex(); const previousIdx = (this.items.length + currentIdx - 1) % this.items.length; this.slideTo(previousIdx); } addEventListener(type, handler){ this.container.addEventListener(type, handler) } start(){ this.stop(); this._timer = setInterval(()=>this.slideNext(), this.cycle); } stop(){ clearInterval(this._timer); } } 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(); }); } } const slider = new Slider('my-slider'); slider.registerPlugins(pluginController, pluginPrevious, pluginNext); slider.start(); </script> </body> </html>
封装-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
类实现即可。
最终组件化代码:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>组件化轮播图插件</title> <style> #my-slider{ position: relative; display: inline-block; 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; } .slide-list__control{ position: relative; display: table; background-color: rgba(255, 255, 255, 0.5); padding: 5px; border-radius: 12px; bottom: 30px; margin: auto; } .slide-list__next, .slide-list__previous{ display: inline-block; position: absolute; top: 50%; margin-top: -25px; width: 30px; height:50px; text-align: center; font-size: 24px; line-height: 50px; overflow: hidden; border: none; background: transparent; color: white; background: rgba(0,0,0,0.2); cursor: pointer; opacity: 0; transition: opacity .5s; } .slide-list__previous { left: 0; } .slide-list__next { right: 0; } #my-slider:hover .slide-list__previous { opacity: 1; } #my-slider:hover .slide-list__next { opacity: 1; } .slide-list__previous:after { content: '<'; } .slide-list__next:after { content: '>'; } .slide-list__control-buttons, .slide-list__control-buttons--selected{ display: inline-block; width: 15px; height: 15px; border-radius: 50%; margin: 0 5px; background-color: white; cursor: pointer; } .slide-list__control-buttons--selected { background-color: red; } </style> </head> <body> <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> <script> 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>`; } getSelectedItem(){ const selected = this.container.querySelector('.slider-list__item--selected'); return selected } getSelectedItemIndex(){ return Array.from(this.items).indexOf(this.getSelectedItem()); } 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) } slideNext(){ const currentIdx = this.getSelectedItemIndex(); const nextIdx = (currentIdx + 1) % this.items.length; this.slideTo(nextIdx); } slidePrevious(){ const currentIdx = this.getSelectedItemIndex(); const previousIdx = (this.items.length + currentIdx - 1) % this.items.length; this.slideTo(previousIdx); } addEventListener(type, handler){ this.container.addEventListener(type, handler); } start(){ this.stop(); this._timer = setInterval(()=>this.slideNext(), this.cycle); } stop(){ clearInterval(this._timer); } } 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){ let controller = slider.container.querySelector('.slide-list__control'); if(controller){ let buttons = controller.querySelectorAll('.slide-list__control-buttons, .slide-list__control-buttons--selected'); controller.addEventListener('mouseover', evt=>{ var 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; let 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){ let 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){ let 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', {name: 'slide-list', data: ['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(); </script> </body> </html>
总结:
- 组件设计的原则:封装性、正确性、扩展性、复用性
- 实现组件的步骤:结构设计、展现效果、行为设计
- 三次重构
- 插件化
- 模板化
- 抽象化(组件框架)