概念
组件是指Web页面上抽出来一个个包含模版(HTML)、功能(JS)和样式(CSS)的单元
好的组件具备封装性、正确性、扩展性、复用性。
轮播图案例
版本1 -- 无交互版
HTML
<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
- 通过
绝对定位将图片重叠到一个位置 - 通过切换不同图片的样式类名,以改变当前显示图片的样式
--selected表示的是选中状态【即展示图片】
#my-slider{
position: relative;
width: 790px;
}
.slider-list ul{
list-style-type:none;
position: relative;
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;
}
JavaScript
api:
- getSelectedItem() -- 获取当前选中的图片
- getSelectedItemIndex() -- 获取当前选中图片的索引
- slideTo() -- 跳转到指定索引的图片
- slideNext() -- 跳转到下一索引的图片:将下一张图片标记为选中状态
- slidePrevious() -- 跳转到上一索引的图片:将上一张图片标记为选中状态
// 创建一个Slider类,封装一些API
class Slider {
constructor(id) {
this.container = document.getElementById(id);
this.items = this.container
.querySelectorAll('.slider-list__item, .slider-list__item--selected');
}
// 获取选中的图片元素:通过选择器`.slider__item--selected`获得被选中的元素
getSelectedItem() {
const selected = this.container
.querySelector('.slider-list__item--selected');
return selected
}
// 获取选中图片的索引值:返回选中的元素在items数组中的位置。
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';
}
}
// 跳转到下一索引的图片:将下一张图片标记为选中状态
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);
}
}
//自动播放
const slider = new Slider('my-slider');
setInterval(() => {
slider.slideNext();
}, 2000);
版本2 -- 交互版
HTML
<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>
CSS
<style>
#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;
}
.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;
/*设置透明度变化的动画,时间为.5秒*/
}
.slide-list__previous {
left: 0;
/*定位在slider元素的最左边*/
}
.slide-list__next {
right: 0;
/*定位在slider元素的最右边*/
}
#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: '>';
}
/*下面是四个小圆点的样式,其实通过这种BEM命名规则你也能看出来*/
.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>
JavaScript
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) {
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();
});
// 注册slide事件,将选中的图片和小圆点设置为selected状态
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();
});
}
}
getSelectedItem() {
let selected = this.container.querySelector('.slider-list__item--selected');
return selected
}
getSelectedItemIndex() {
return Array.from(this.items).indexOf(this.getSelectedItem());
}
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)
}
slideNext() {
let currentIdx = this.getSelectedItemIndex();
let nextIdx = (currentIdx + 1) % this.items.length;
this.slideTo(nextIdx);
}
slidePrevious() {
let currentIdx = this.getSelectedItemIndex();
let previousIdx = (this.items.length + currentIdx - 1) % this.items.length;
this.slideTo(previousIdx);
}
// 定义一个定时器,循环播放
start() {
this.stop();
this._timer = setInterval(() => this.slideNext(), this.cycle);
}
// 停止循环播放(用户在自己操作的时候要停止自动循环)
stop() {
clearInterval(this._timer);
}
}
const slider = new Slider('my-slider');
slider.start();
总结
-
结构设计 HTML
-
展现效果 CSS
-
行为设计 JavaScript
- API (功能)
- Event (控制流)
对于该轮播图的组件基本功能算是实现了,但是对于组件封装的概念,似乎还没有体现出来。
接下来,让我们一起思考,对于组件的封装性、正确性、扩展性、复用性 我们如何实现呢?
改进
解耦JS -- 插件化
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();
});
}
}
function pluginRandomGet(slider) {
randomGet.addEventListener('click', () => {
const idx = Math.floor(slider.items.length * Math.random());
slider.stop();
slider.slideTo(idx);
slider.start();
})
}
const slider = new Slider('my-slider');
slider.registerPlugins(pluginController, pluginPrevious, pluginNext, pluginRandomGet);
slider.start();
这样就可以自行配置需要哪些小组件或者不需要(比如左右切换按钮、底部的小圆点等)
但是目前依然还有一个缺点: 当不需要对应插件的时候,还得手动移出对应的dom结构,因此,还需要将HTML模板化,进行解耦。
解耦HTML -- 模板化
将HTML模板化,也就是让JavaScript来渲染组件的HTML,这样更易于扩展。
HTML
<div id="my-slider" class="slider-list"></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;
}
.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;
}
JavaScript
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){
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);
});
}
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';
}
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)
}
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){
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();
此时,多了一个render()函数,现在你不需要手动删除(添加)对应的dom结构进行自定义轮播图组件了,并且你还可以自己定义使用几张图片以及哪几张图片进行轮播,满足了扩展性。
那么还有一点就是复用性没有完全的体现,接下来继续进行重构,将其抽象为一盒组件框架,增加其复用性
抽象 -- 组件框架
定义一个通用组件类
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 ''
}
}
总结
对于上述的通用组件类,还有一定的优化空间,比如CSS的抽象,比如CSS in JS 的方式,或是其他方式。
- 组件设计的原则:封装性、正确性、扩展性、复用性
- 实现组件的步骤:结构设计、展现效果、行为设计 (封装性、正确性)
- 三次重构:插件化(扩展性)、模板化(扩展性)、抽象化(复用性)